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
69 changes: 69 additions & 0 deletions .github/sign-fatjars.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/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 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 <asset-dir> (thin jars are left untouched).
#
# 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 <asset-dir>
# <asset-dir> directory holding the fat jars (and possibly thin jars); every
# *-jar-with-dependencies*.jar in it is signed in place (-> <jar>.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 <asset-dir>}"

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'."
2 changes: 1 addition & 1 deletion .github/signing-selftest/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin <bernard.ladenthin@gmail.com>
//
// SPDX-License-Identifier: Apache-2.0
// 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
Expand Down
2 changes: 1 addition & 1 deletion .github/signing-selftest/settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin <bernard.ladenthin@gmail.com>
//
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT OR Apache-2.0

rootProject.name = "signing-selftest"
24 changes: 12 additions & 12 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -486,10 +486,10 @@ jobs:
- name: Build & sign classifier fat jars (GitHub-only assets, never Central)
if: ${{ !cancelled() }}
env:
MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
run: |
set -euo pipefail
if [ -n "${MAVEN_GPG_PASSPHRASE:-}" ]; then echo "::add-mask::${MAVEN_GPG_PASSPHRASE}"; fi
# Source of truth: the <classifier> set in java-llama.cpp llama/pom.xml for the pinned
# ${llama.version} (all published to Maven Central). Keep in sync on a llama bump.
CLASSIFIERS=(
Expand All @@ -509,10 +509,10 @@ jobs:
done
# default CPU build LAST so the unsuffixed fat jar is the all-platform CPU variant
build ""
for f in srcmorph-cli/target/srcmorph-cli-*-jar-with-dependencies*.jar; do
printf '%s' "$MAVEN_GPG_PASSPHRASE" | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 --detach-sign --armor "$f"
cp "$f" "$f".asc signed-snapshot-assets/
done
# Collect every fat jar variant, then GPG-sign each via the cross-repo shared script
# (.github/sign-fatjars.sh, byte-identical with java-llama.cpp) — a .asc per fat jar.
cp srcmorph-cli/target/srcmorph-cli-*-jar-with-dependencies*.jar signed-snapshot-assets/
bash .github/sign-fatjars.sh signed-snapshot-assets
- uses: actions/upload-artifact@v7
if: ${{ !cancelled() }}
with:
Expand Down Expand Up @@ -608,10 +608,10 @@ jobs:
- name: Build & sign classifier fat jars (GitHub-only assets, never Central)
if: ${{ !cancelled() }}
env:
MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
run: |
set -euo pipefail
if [ -n "${MAVEN_GPG_PASSPHRASE:-}" ]; then echo "::add-mask::${MAVEN_GPG_PASSPHRASE}"; fi
# Source of truth: the <classifier> set in java-llama.cpp llama/pom.xml for the pinned
# ${llama.version} (all published to Maven Central). Keep in sync on a llama bump.
CLASSIFIERS=(
Expand All @@ -631,10 +631,10 @@ jobs:
done
# default CPU build LAST so the unsuffixed fat jar is the all-platform CPU variant
build ""
for f in srcmorph-cli/target/srcmorph-cli-*-jar-with-dependencies*.jar; do
printf '%s' "$MAVEN_GPG_PASSPHRASE" | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 --detach-sign --armor "$f"
cp "$f" "$f".asc signed-release-assets/
done
# Collect every fat jar variant, then GPG-sign each via the cross-repo shared script
# (.github/sign-fatjars.sh, byte-identical with java-llama.cpp) — a .asc per fat jar.
cp srcmorph-cli/target/srcmorph-cli-*-jar-with-dependencies*.jar signed-release-assets/
bash .github/sign-fatjars.sh signed-release-assets
- uses: actions/upload-artifact@v7
if: ${{ !cancelled() }}
with:
Expand Down
9 changes: 9 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,15 @@ clear of the JPMS module-mode javadoc trap that bit BAF. **Before raising the Ja
level to ≥ 9 in any module, read**
[`../workspace/policies/jpms-module-descriptor.md`](../workspace/policies/jpms-module-descriptor.md).

## Fat-jar release assets

The `srcmorph-cli` fat jar (`jar-with-dependencies`) is a **GitHub-Release asset only — never
Maven Central** (`srcmorph-cli/pom.xml` sets `<attach>false</attach>`), attached with a detached
GPG `.asc`. CI builds **one fat jar per `net.ladenthin:llama` classifier** (default CPU + every GPU
classifier) and signs them via the cross-repo shared `.github/sign-fatjars.sh` (byte-identical with
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).

## Open TODOs

Open TODOs for this repo live in [`TODO.md`](TODO.md). Cross-repo status
Expand Down
40 changes: 28 additions & 12 deletions lombok.config
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,49 @@ 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<T> 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

# Do NOT generate Spring-style @ConstructorProperties; java.beans is not
# needed by this codebase and pulls in the desktop module on some JDKs.
lombok.anyConstructor.addConstructorProperties = false

# Mojo @Parameter POJOs use direct setter generation (@Setter at field
# level) when needed; flagUsage = ALLOW keeps Lombok quiet on the
# accessor-generation default rules.
# Allow Lombok-style accessor patterns without warnings.
lombok.accessors.flagUsage = ALLOW
Loading