Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions .github/smoke-fatjar-cli.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env bash

# SPDX-FileCopyrightText: 2026 Bernard Ladenthin <bernard.ladenthin@gmail.com>
#
# 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 <jar> <args>` 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 <jar-dir> <jar-glob> <work-dir> <success-marker> [args...]
# <jar-dir> directory to search for the jar (searched recursively, so a downloaded
# multi-module artifact that preserved its <module>/target/ layout works)
# <jar-glob> 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)
# <work-dir> working directory for the run; example configs use relative paths, so this
# is what makes the smoke reproduce the documented invocation
# <success-marker> extended regex that must appear in the program's output
# [args...] passed to the program after `-jar <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 <jar-dir> <jar-glob> <work-dir> <success-marker> [args...]}"
JAR_GLOB="${2:?usage: smoke-fatjar-cli.sh <jar-dir> <jar-glob> <work-dir> <success-marker> [args...]}"
WORK_DIR="${3:?usage: smoke-fatjar-cli.sh <jar-dir> <jar-glob> <work-dir> <success-marker> [args...]}"
MARKER="${4:?usage: smoke-fatjar-cli.sh <jar-dir> <jar-glob> <work-dir> <success-marker> [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"
47 changes: 45 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading