From e3066e3cbc6f4f4552eb51e5832d4f71c4380abd Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 05:10:05 +0000 Subject: [PATCH 1/4] ci(publish): GPG-sign the all-backends fat jars (.asc) on GitHub Release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The all-backends server fat jars (jar-with-dependencies) were attached to the GitHub Release / snapshot pre-release with only a .sha256 checksum — no signature, unlike the thin jars (which maven-gpg signs at deploy) and unlike the BAF / srcmorph sibling fat jars. Add .github/sign-fatjars.sh (imports the release key into an ephemeral keyring and produces a detached, armored .asc for every *-jar-with-dependencies*.jar in a directory, verifying each) and call it from github-release-signed and github-snapshot after they download llama-fatjars, before the upload. Both jobs now declare environment: maven-central (where GPG_PRIVATE_KEY/GPG_PASSPHRASE are scoped; it has no approval gate) and check out the repo so the script is present. The .sha256 files are kept (integrity); the .asc adds authenticity. Signed in these dispatch-gated attach jobs, not in package-fatjars, because only this path receives the key. Verified locally end-to-end with a throwaway key: both fat jars signed + verified, the thin jar left untouched. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01TJzCezSnQ8FxpFdeVxYxQY --- .github/sign-fatjars.sh | 69 +++++++++++++++++++++++++++++++++++ .github/workflows/publish.yml | 32 ++++++++++++++++ CLAUDE.md | 16 ++++++-- 3 files changed, 113 insertions(+), 4 deletions(-) create mode 100755 .github/sign-fatjars.sh diff --git a/.github/sign-fatjars.sh b/.github/sign-fatjars.sh new file mode 100755 index 000000000..c2d087a24 --- /dev/null +++ b/.github/sign-fatjars.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: 2026 Bernard Ladenthin +# +# SPDX-License-Identifier: MIT + +# GPG-signs the all-backends server fat jars (jar-with-dependencies) with a detached, +# armored .asc signature — the authenticity counterpart to the .sha256 integrity files +# that package-fatjars.sh already emits. Signature parity with the thin jars (which +# maven-gpg signs at deploy time) and with the BAF / srcmorph sibling fat jars. +# +# Signed in the GitHub-Release attach jobs (github-snapshot / github-release-signed), +# not in package-fatjars, because only that dispatch-gated path receives the signing +# key: the GPG_PRIVATE_KEY / GPG_PASSPHRASE secrets are scoped to the `maven-central` +# environment. The cross-repo mechanism is documented in +# workspace/policies/fat-jar-release-assets.md. +# +# Usage: sign-fatjars.sh +# directory holding the downloaded fat jars (and the thin jars); every +# *-jar-with-dependencies*.jar in it is signed in place (-> .asc). +# +# Env: GPG_PRIVATE_KEY armored secret key (required) +# GPG_PASSPHRASE passphrase for the key (may be empty) +# +# Fail-loud: aborts if the key is absent, if no fat jar is found, or if any signature +# fails to verify. + +set -euo pipefail + +DIR="${1:?usage: sign-fatjars.sh }" + +if [ -z "${GPG_PRIVATE_KEY:-}" ]; then + echo "::error::GPG_PRIVATE_KEY is empty — cannot sign the fat jars. The maven-central environment did not deliver the secret to this ref." >&2 + exit 1 +fi +if [ -n "${GPG_PASSPHRASE:-}" ]; then echo "::add-mask::${GPG_PASSPHRASE}"; fi + +# Ephemeral, private keyring; removed on exit (do NOT touch the runner's default one). +GNUPGHOME="$(mktemp -d)" +export GNUPGHOME +chmod 700 "$GNUPGHOME" +cleanup() { gpgconf --kill gpg-agent >/dev/null 2>&1 || true; rm -rf "$GNUPGHOME"; } +trap cleanup EXIT + +printf '%s\n' "$GPG_PRIVATE_KEY" | gpg --batch --import +KEYID="$(gpg --list-secret-keys --with-colons --fixed-list-mode | awk -F: '$1=="sec"{print $5; exit}')" +if [ -z "$KEYID" ]; then + echo "::error::No secret key imported from GPG_PRIVATE_KEY." >&2 + exit 1 +fi + +shopt -s nullglob +jars=("$DIR"/*-jar-with-dependencies*.jar) +shopt -u nullglob +if [ "${#jars[@]}" -eq 0 ]; then + echo "::error::No *-jar-with-dependencies*.jar found in '$DIR' to sign." >&2 + exit 1 +fi + +for f in "${jars[@]}"; do + # Skip a signature file itself if the glob ever catches one. + case "$f" in *.asc) continue ;; esac + printf '%s' "${GPG_PASSPHRASE:-}" | gpg --batch --yes --pinentry-mode loopback \ + --passphrase-fd 0 --local-user "$KEYID" --detach-sign --armor "$f" + gpg --batch --verify "$f.asc" "$f" + echo "signed + verified: $(basename "$f") -> $(basename "$f").asc" +done + +echo "Signed ${#jars[@]} fat jar(s) in '$DIR'." diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2de8caad0..8ea8e2ec4 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -3188,9 +3188,15 @@ jobs: # the GitHub pre-release assets must not be lost in that case. if: ${{ !cancelled() && (needs.publish-snapshot.result == 'success' || needs.publish-snapshot.result == 'failure') && needs.package-fatjars.result == 'success' }} runs-on: ubuntu-latest + # maven-central so the GPG_PRIVATE_KEY / GPG_PASSPHRASE secret is delivered (it is + # scoped to this environment) for signing the fat jars below. This environment has + # no approval gate (the standalone verify-signing-key jobs use it on every run), so + # declaring it here does not block the release. + environment: maven-central permissions: contents: write steps: + - uses: actions/checkout@v7 - uses: actions/download-artifact@v8 with: name: signed-snapshot-assets @@ -3203,6 +3209,16 @@ jobs: with: name: llama-fatjars path: snapshot-assets/ + # GPG-sign the fat jars so each carries a detached .asc signature alongside its + # .sha256 checksum — signature parity with the thin jars (which maven-gpg signs at + # deploy) and with the BAF / srcmorph sibling fat jars. The .sha256 files (integrity) + # are kept; the .asc adds authenticity. Signed here (not in package-fatjars) because + # only this dispatch-gated path has the key. See workspace/policies/fat-jar-release-assets.md. + - name: GPG-sign the fat jars (.asc) + env: + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + run: bash .github/sign-fatjars.sh snapshot-assets - name: Update snapshot pre-release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -3386,9 +3402,15 @@ jobs: # the GitHub release assets must not be lost in that case. if: ${{ !cancelled() && (needs.publish-release.result == 'success' || needs.publish-release.result == 'failure') && needs.package-fatjars.result == 'success' }} runs-on: ubuntu-latest + # maven-central so the GPG_PRIVATE_KEY / GPG_PASSPHRASE secret is delivered (it is + # scoped to this environment) for signing the fat jars below. This environment has + # no approval gate (the standalone verify-signing-key jobs use it on every run), so + # declaring it here does not block the release. + environment: maven-central permissions: contents: write steps: + - uses: actions/checkout@v7 - uses: actions/download-artifact@v8 with: name: signed-release-assets @@ -3401,6 +3423,16 @@ jobs: with: name: llama-fatjars path: release-assets/ + # GPG-sign the fat jars so each carries a detached .asc signature alongside its + # .sha256 checksum — signature parity with the thin jars (which maven-gpg signs at + # deploy) and with the BAF / srcmorph sibling fat jars. The .sha256 files (integrity) + # are kept; the .asc adds authenticity. Signed here (not in package-fatjars) because + # only this dispatch-gated path has the key. See workspace/policies/fat-jar-release-assets.md. + - name: GPG-sign the fat jars (.asc) + env: + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + run: bash .github/sign-fatjars.sh release-assets - name: Upload release assets uses: softprops/action-gh-release@v3 with: diff --git a/CLAUDE.md b/CLAUDE.md index a203058d2..5ca68a949 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -348,8 +348,11 @@ are all git-ignored (staged by CI, never committed). Every pipeline run assembles **per-OS multi-backend server fat jars** and, on the release paths, attaches them to GitHub: `llama--all---jar-with-dependencies.jar` for `linux-x86-64`, `linux-aarch64`, `windows-x86-64`, `windows-aarch64`, plus the default -CPU fat jar — each with a `.sha256` file. They are **download assets only**: the Central -deploy invocations run without the `assembly` profile and are untouched. +CPU fat jar — each with a `.sha256` **and** a detached GPG `.asc` signature. They are +**download assets only**: the Central deploy invocations run without the `assembly` profile +and are untouched. (The cross-repo "fat jar → GitHub Release, never Central, signed `.asc`" +convention shared with BAF and srcmorph is documented in +[`../workspace/policies/fat-jar-release-assets.md`](../workspace/policies/fat-jar-release-assets.md).) Mechanism (three pieces): @@ -379,8 +382,13 @@ Mechanism (three pieces): `--chat-template chatml`): poll `/health` to 200, assert a `/v1/chat/completions` choice, and require the loader's backend-selection log line. `publish-snapshot`/`publish-release` `need` `package-fatjars` + both smokes (fail-loud gating); `github-release-signed` and - `github-snapshot` additionally download `llama-fatjars` into their asset directory so the - fat jars land on the tag release and the rolling `snapshot` pre-release. + `github-snapshot` additionally download `llama-fatjars` into their asset directory, then + **GPG-sign each fat jar** via `.github/sign-fatjars.sh` (a detached `.asc` alongside the + `.sha256`), so the fat jars land signed on the tag release and the rolling `snapshot` + pre-release. Both jobs declare `environment: maven-central` (where the signing key secret is + scoped; it has no approval gate) and `checkout` the repo so the script is present. Signing + happens in these attach jobs — not in `package-fatjars` — because only this dispatch-gated + release path receives the key. A backend loading successfully but finding **zero usable devices** (e.g. CUDA toolkit installed, no NVIDIA GPU) is benign: ggml's backend registry contributes no devices and From c116357a934f5bf71bc1295501d8128c7f546714 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 05:20:13 +0000 Subject: [PATCH 2/4] ci(publish): make sign-fatjars.sh a byte-identical cross-repo shared script Neutralize the header (repo-agnostic) and relicense to MIT OR Apache-2.0 so the exact same sign-fatjars.sh can live byte-identical in java-llama.cpp and srcmorph (same pattern as the verify-signing-key job). Signing logic is unchanged. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01TJzCezSnQ8FxpFdeVxYxQY --- .github/sign-fatjars.sh | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/sign-fatjars.sh b/.github/sign-fatjars.sh index c2d087a24..c430c39a6 100755 --- a/.github/sign-fatjars.sh +++ b/.github/sign-fatjars.sh @@ -2,21 +2,21 @@ # SPDX-FileCopyrightText: 2026 Bernard Ladenthin # -# SPDX-License-Identifier: MIT +# SPDX-License-Identifier: MIT OR Apache-2.0 -# GPG-signs the all-backends server fat jars (jar-with-dependencies) with a detached, -# armored .asc signature — the authenticity counterpart to the .sha256 integrity files -# that package-fatjars.sh already emits. Signature parity with the thin jars (which -# maven-gpg signs at deploy time) and with the BAF / srcmorph sibling fat jars. +# Cross-repo shared script — kept BYTE-IDENTICAL in java-llama.cpp and srcmorph (sync any +# edit to both). GPG-signs the fat jars (jar-with-dependencies) in a directory with a +# detached, armored .asc signature — the authenticity counterpart to any .sha256 integrity +# file. The caller builds/collects the fat jars; this only signs every +# *-jar-with-dependencies*.jar it finds in (thin jars are left untouched). # -# Signed in the GitHub-Release attach jobs (github-snapshot / github-release-signed), -# not in package-fatjars, because only that dispatch-gated path receives the signing -# key: the GPG_PRIVATE_KEY / GPG_PASSPHRASE secrets are scoped to the `maven-central` -# environment. The cross-repo mechanism is documented in +# Signed in the GitHub-Release attach path, not at Maven build time, because only that +# dispatch-gated path receives the signing key: GPG_PRIVATE_KEY / GPG_PASSPHRASE are scoped +# to the `maven-central` GitHub Environment. Cross-repo convention + per-repo shapes: # workspace/policies/fat-jar-release-assets.md. # # Usage: sign-fatjars.sh -# directory holding the downloaded fat jars (and the thin jars); every +# directory holding the fat jars (and possibly thin jars); every # *-jar-with-dependencies*.jar in it is signed in place (-> .asc). # # Env: GPG_PRIVATE_KEY armored secret key (required) From f2c30cd3a20d80180fdea9e491466f0c6661a4d1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 05:35:01 +0000 Subject: [PATCH 3/4] ci: unify signing-selftest SPDX to MIT OR Apache-2.0 (byte-identical cross-repo) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unify the throwaway signing-selftest Gradle project (build.gradle.kts + settings.gradle.kts) to the dual SPDX-License-Identifier "MIT OR Apache-2.0" so the files are TRULY byte-identical across all four sibling repos (java-llama.cpp used MIT, the others Apache-2.0 — same body, only the header drifted). This matches the cross-repo-synced-file convention already used for sign-fatjars.sh and makes the verify-signing-key-gradle preflight project checksum-verifiable via workspace. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01TJzCezSnQ8FxpFdeVxYxQY --- .github/signing-selftest/build.gradle.kts | 2 +- .github/signing-selftest/settings.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/signing-selftest/build.gradle.kts b/.github/signing-selftest/build.gradle.kts index 6524c02df..e165c9f67 100644 --- a/.github/signing-selftest/build.gradle.kts +++ b/.github/signing-selftest/build.gradle.kts @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: 2026 Bernard Ladenthin // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: MIT OR Apache-2.0 // Throwaway signing project used ONLY by the `verify-signing-key-gradle` // preflight job in .github/workflows/publish.yml. It signs a tiny throwaway Zip diff --git a/.github/signing-selftest/settings.gradle.kts b/.github/signing-selftest/settings.gradle.kts index e420db00a..211f25338 100644 --- a/.github/signing-selftest/settings.gradle.kts +++ b/.github/signing-selftest/settings.gradle.kts @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: 2026 Bernard Ladenthin // -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: MIT OR Apache-2.0 rootProject.name = "signing-selftest" From d2fa10c07a2dacc13db361771de725800c1eadae Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 05:46:28 +0000 Subject: [PATCH 4/4] chore: sync lombok.config to canonical workspace policy (byte-identical cross-repo) Sync lombok.config to the canonical workspace policy content verbatim The three Lombok repos had IDENTICAL directives but drifted comments (per-repo rationale in the file). Replace each with the canonical block from workspace/policies/lombok-config.md verbatim, so the file is now byte-identical across all three (the rationale lives once in the policy / crossrepo, not in the file). Recorded in the workspace cross-repo checksum drift-check. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01TJzCezSnQ8FxpFdeVxYxQY --- llama/lombok.config | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/llama/lombok.config b/llama/lombok.config index 1e02f1adc..c6fc5b0fd 100644 --- a/llama/lombok.config +++ b/llama/lombok.config @@ -7,25 +7,43 @@ config.stopBubbling = true # Emit @lombok.Generated on every generated member. SpotBugs / JaCoCo / # SonarQube special-case this annotation and skip the synthetic methods -# from coverage requirements and bug detectors. +# from coverage requirements and bug detectors. Without this, SpotBugs at +# effort=Max + threshold=Low surfaces dozens of synthetic-bytecode findings +# (USBR_UNNECESSARY_STORE_BEFORE_RETURN, IMC_IMMATURE_CLASS_NO_TOSTRING, +# NM_FIELD_NAMING_CONVENTION, ...) on every Lombok-generated method. lombok.addLombokGeneratedAnnotation = true # Default to "skip" on @EqualsAndHashCode / @ToString: we inherit from # Object in almost all cases; "skip" is the right default for # Object-extending classes. Classes that extend a non-Object base override # per-annotation with @EqualsAndHashCode(callSuper = true) / -# @ToString(callSuper = true). +# @ToString(callSuper = true). Without this, Lombok emits a WARNING on +# every @EqualsAndHashCode without explicit callSuper, which -Werror +# promotes to a build break. lombok.equalsAndHashCode.callSuper = skip lombok.toString.callSuper = skip # Force Lombok's @EqualsAndHashCode / @ToString to read FIELDS directly -# instead of routing through `this.getX()` (the default). Rationale lives -# in ../workspace/policies/lombok-config.md. Cross-repo invariant: all -# three Lombok-using repos ship the same setting. Without it, -# fb-contrib's OI_OPTIONAL_ISSUES_CHECKING_REFERENCE fires on every -# Lombok-generated `this$x == null` branch when `x` is an Optional, and -# Optional/unmodifiable-wrapper getters allocate fresh wrappers on every -# equals call. +# instead of routing through `this.getX()` (the default). Rationale: +# +# Some classes expose value-add getters that wrap their @Nullable field in +# Optional or wrap a list field in Collections.unmodifiableList + Optional. +# Those wrappers are the public-API contract, not the equality contract: +# +# 1. fb-contrib's OI_OPTIONAL_ISSUES_CHECKING_REFERENCE fires on every +# Lombok-generated `this$x == null` branch when `x` is an Optional — +# Optional is the standard "never null" type, so the null branch is +# dead code. +# 2. unmodifiableList + Optional wrapper getters allocate fresh wrappers +# on every equals call. Field access avoids the allocations. +# 3. The two forms are semantically equivalent: Optional.equals and +# Collections.unmodifiableList(x).equals(...) both delegate to value- +# based comparison of the underlying state. +# +# All value classes in these repos are `final`, so subclass-override of a +# getter cannot change equality. callSuper=true chains are unaffected — +# `super.equals()` is still a method call, and the parent class's own +# field handling is governed by the same setting. lombok.equalsAndHashCode.doNotUseGetters = true lombok.toString.doNotUseGetters = true @@ -33,4 +51,5 @@ lombok.toString.doNotUseGetters = true # needed by this codebase and pulls in the desktop module on some JDKs. lombok.anyConstructor.addConstructorProperties = false +# Allow Lombok-style accessor patterns without warnings. lombok.accessors.flagUsage = ALLOW