From 49504b1563070c0fe19c7459d4fc70f49bdf2016 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 23:43:22 +0000 Subject: [PATCH] ci: run the CLI fat jar before publishing it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The srcmorph-cli fat jar is a GitHub-Release asset, built and GPG-signed on the publish path — and never once launched. Nothing else in the build touches it either: unit tests, PIT, SpotBugs and ArchUnit all run off target/classes, and `mvn package` only asserts that the assembly plugin wrote a file. So a missing Main-Class, a shade-mangled resource or an absent SLF4J binding would ship signed and unrunnable. Signing proves who built it, not that it works. java-llama.cpp shipped a corrupt native library through three releases for exactly this reason. The new smoke-fatjar job (needs: [build], in both publish jobs' needs) downloads plugin-jars and runs the CLI from examples/ against config_Plan.json, asserting exit 0 plus `Main#run end.` — the exit code alone is satisfied by a JVM that starts and does nothing. `Plan` with the mock provider needs no GGUF, no GPU and no network. .github/smoke-fatjar-cli.sh is kept byte-identical with BitcoinAddressFinder (both CLIs derive from the same cli.Main pattern and log the same marker); sync any edit to both copies and to the checksum table in workspace/crossrepostatus.md. The rule and the per-repo assertion table live in workspace/policies/fat-jar-release-assets.md. Deliberately not `All` over a real source tree: the example configs are tuned for a small demo tree, so All against this repo's own sources fails by design (19 files exceed the demo model's context window with onOversize=fail) — non-deterministic, not more thorough. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Tzo7Yi8SXP6WxhqZXXbsco --- .github/smoke-fatjar-cli.sh | 82 +++++++++++++++++++++++++++++++++++ .github/workflows/publish.yml | 47 +++++++++++++++++++- CLAUDE.md | 14 ++++++ 3 files changed, 141 insertions(+), 2 deletions(-) create mode 100755 .github/smoke-fatjar-cli.sh diff --git a/.github/smoke-fatjar-cli.sh b/.github/smoke-fatjar-cli.sh new file mode 100755 index 0000000..1bf6ee1 --- /dev/null +++ b/.github/smoke-fatjar-cli.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: 2026 Bernard Ladenthin +# +# SPDX-License-Identifier: MIT OR Apache-2.0 + +# Cross-repo shared script — kept BYTE-IDENTICAL in BitcoinAddressFinder and srcmorph (sync any +# edit to both, and to the checksum table in workspace/crossrepostatus.md). Smoke-tests a runnable +# fat jar (jar-with-dependencies) by actually launching it: `java -jar ` must exit 0 +# and print an expected success marker. +# +# Why: the fat jar is a GitHub-Release asset (workspace/policies/fat-jar-release-assets.md), and +# the convention is that no release asset is attached that CI has not run. An uber jar can be built +# and GPG-signed perfectly while being unrunnable — a missing Main-Class, a shade-mangled or +# duplicated resource, an absent SLF4J binding, a native library that will not load. None of that +# is visible to `mvn package` or to the unit tests, which run off target/classes and never touch +# the assembled artifact. Signing an artifact proves who built it, not that it works. +# +# The exit code alone is a weak assertion (a JVM that starts and does nothing also exits 0), so a +# success marker from the program's own output is required too. +# +# Usage: smoke-fatjar-cli.sh [args...] +# directory to search for the jar (searched recursively, so a downloaded +# multi-module artifact that preserved its /target/ layout works) +# filename glob; must match EXACTLY ONE jar (an ambiguous match is an error, +# not a "pick the first" — that is how the wrong artifact gets tested) +# working directory for the run; example configs use relative paths, so this +# is what makes the smoke reproduce the documented invocation +# extended regex that must appear in the program's output +# [args...] passed to the program after `-jar ` +# +# Output goes to smoke-out.log / smoke-err.log in the CALLER's working directory (uploaded by the +# CI job on failure). Deliberately a plain `java -jar` with no extra JVM flags: the contract under +# test is that the published artifact runs as-is. + +set -euo pipefail + +JAR_DIR="${1:?usage: smoke-fatjar-cli.sh [args...]}" +JAR_GLOB="${2:?usage: smoke-fatjar-cli.sh [args...]}" +WORK_DIR="${3:?usage: smoke-fatjar-cli.sh [args...]}" +MARKER="${4:?usage: smoke-fatjar-cli.sh [args...]}" +shift 4 + +# Generous ceiling: this bounds a hang (a CLI waiting on stdin, a server that never returns), it is +# not a performance budget. A healthy run of either repo's smoke config finishes in seconds. +TIMEOUT_SECONDS=600 + +OUT_LOG="$(pwd)/smoke-out.log" +ERR_LOG="$(pwd)/smoke-err.log" + +fail() { + echo "::error::$*" >&2 + [ -s "$OUT_LOG" ] && { echo "--- smoke-out.log (tail) ---" >&2; tail -50 "$OUT_LOG" >&2; } + [ -s "$ERR_LOG" ] && { echo "--- smoke-err.log (tail) ---" >&2; tail -50 "$ERR_LOG" >&2; } + exit 1 +} + +[ -d "$JAR_DIR" ] || fail "jar directory '$JAR_DIR' does not exist" +[ -d "$WORK_DIR" ] || fail "working directory '$WORK_DIR' does not exist" + +jars=() +while IFS= read -r j; do jars+=("$j"); done < <(find "$JAR_DIR" -type f -name "$JAR_GLOB" | sort) +[ "${#jars[@]}" -eq 1 ] \ + || fail "expected exactly 1 jar matching '$JAR_GLOB' under '$JAR_DIR', got ${#jars[@]}: ${jars[*]:-none}" +JAR="$(cd "$(dirname "${jars[0]}")" && pwd)/$(basename "${jars[0]}")" + +echo "smoke jar : $JAR" +echo "work dir : $WORK_DIR" +echo "arguments : $*" + +set +e +(cd "$WORK_DIR" && timeout "$TIMEOUT_SECONDS" java -jar "$JAR" "$@") > "$OUT_LOG" 2> "$ERR_LOG" +STATUS=$? +set -e + +[ "$STATUS" -ne 124 ] || fail "the fat jar did not terminate within ${TIMEOUT_SECONDS}s" +[ "$STATUS" -eq 0 ] || fail "the fat jar exited with status $STATUS (expected 0)" + +grep -hqE "$MARKER" "$OUT_LOG" "$ERR_LOG" \ + || fail "success marker '$MARKER' not found in the output — the jar started but did not complete its run" + +echo "smoke test PASSED" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 66db813..a0d98f8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -367,6 +367,49 @@ jobs: path: srcmorph-maven-plugin/target/vmlens-report/ if-no-files-found: ignore + # --------------------------------------------------------------------------- + # Fat-jar smoke test — cross-repo standard job, kept in the same shape in + # BitcoinAddressFinder and java-llama.cpp (see workspace/policies/fat-jar-release-assets.md, + # "No release asset is attached that CI has not run"). The fat jar is a GitHub-Release asset, + # and unit tests never touch it: they run off target/classes, so an unrunnable uber jar — no + # Main-Class, a shade-mangled resource, a missing SLF4J binding — is built and GPG-signed + # without anything noticing. Signing proves who built it, not that it works. + # + # srcmorph-specific: config_Plan.json uses the `mock` generation provider, so this needs no + # GGUF model, no GPU and no network — it is the cheapest possible real launch of the CLI. + # `Plan` is the command that cannot fail for configuration reasons (planOnly, nothing written); + # `All` over a real source tree fails by design when files exceed the demo model's context + # window, which would make the smoke non-deterministic rather than more thorough. + # --------------------------------------------------------------------------- + + smoke-fatjar: + name: Smoke test fat jar + needs: [build] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/download-artifact@v8 + with: + name: plugin-jars + path: fatjar/ + - uses: actions/setup-java@v5 + with: + java-version: '21' + distribution: temurin + - name: Run fat-jar smoke test + run: | + .github/smoke-fatjar-cli.sh fatjar 'srcmorph-cli-*-jar-with-dependencies.jar' \ + examples 'Main#run end\.' config_Plan.json + - name: Upload smoke logs + if: failure() + uses: actions/upload-artifact@v7 + with: + name: smoke-fatjar-logs + path: | + smoke-out.log + smoke-err.log + if-no-files-found: warn + report: name: Report needs: [test] @@ -421,7 +464,7 @@ jobs: publish-snapshot: name: Publish Snapshot to Central - needs: [check-snapshot, code-style] + needs: [check-snapshot, code-style, smoke-fatjar] if: needs.check-snapshot.result == 'success' && inputs.publish_to_central runs-on: ubuntu-latest environment: maven-central @@ -550,7 +593,7 @@ jobs: publish-release: name: Publish Release to Central - needs: [check-tag, code-style] + needs: [check-tag, code-style, smoke-fatjar] if: needs.check-tag.result == 'success' && inputs.publish_to_central runs-on: ubuntu-latest environment: maven-central diff --git a/CLAUDE.md b/CLAUDE.md index 6ae0af7..19f4d2d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -474,6 +474,20 @@ classifier) and signs them via the cross-repo shared `.github/sign-fatjars.sh` ( java-llama.cpp). The convention + per-repo shapes + the classifier keep-in-sync rule are documented in [`../workspace/policies/fat-jar-release-assets.md`](../workspace/policies/fat-jar-release-assets.md). +**srcmorph-specific smoke.** The cross-repo rule "no release asset is attached that CI has not run" +is implemented here by the `smoke-fatjar` job (`needs: [build]`, gates both publish jobs): it +downloads the `plugin-jars` artifact and runs the **byte-identical shared** +`.github/smoke-fatjar-cli.sh` (synced with BAF — see the checksum table in `crossrepostatus.md`) +from `examples/` against `config_Plan.json`, asserting exit 0 plus `Main#run end.` in the output. +`Plan` with the `mock` provider needs no GGUF, no GPU and no network, which makes this the cheapest +possible real launch of the CLI. **Do not "strengthen" it to `config_All.json` over a real source +tree:** the example configs are tuned for a small demo tree, so `All` against this repo's own +sources fails by design (19 files exceed the demo model's context window with `onOversize=fail`) — +that would make the smoke non-deterministic, not more thorough. Note the shipped `Plan` example +plans 0 files when run from `examples/` (its `subtrees: ["src/main/java"]` does not exist there); +that is fine for a smoke, which is testing that the artifact launches and completes, not the +indexer. + ## Dependency Convergence Pinning `dependencyConvergence` is enabled (maven-enforcer) in each of the 3 reactor modules;