From 90dd21a5750081aeb6a5ac451209bddab767f6ab Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 22:26:13 +0000 Subject: [PATCH] ci: fail the release when an asset is attached unsigned The attach jobs collect `target/*.jar.asc` with `|| true`, so a signing step that produced nothing yields an attach that looks complete and is not: the jars land on the release without a signature and nothing says so. The obvious fix -- verify, refuse to attach -- would defeat the reason both attach jobs run on a failed publish job in the first place: when Central is unreachable, the GitHub assets are the only way to get the build output at all, so withholding them over a missing signature is the worst outcome available. So the check is split around the upload: report before it (never exits non-zero, one annotation per unsigned jar, count written to the step output), upload unconditionally, assert after it. Assets always land; an unsigned release is loudly red instead of quietly wrong. -1 distinguishes "nothing was collected at all" from a signing failure. Byte-identical with the copies in the sibling repos; see workspace/policies/fat-jar-release-assets.md. --- .github/workflows/publish.yml | 68 +++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index dfdc0cbe..9d8e0472 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -3574,6 +3574,34 @@ jobs: GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} run: bash .github/sign-fatjars.sh snapshot-assets + - name: Report unsigned assets (does not block the upload) + # Deliberately NON-blocking, and deliberately BEFORE the upload. Both attach jobs run even + # when their publish job failed, because a Central publish-poll timeout must not cost the + # GitHub assets: if Central is unreachable these are the ONLY way to get the artifacts at + # all. Refusing to attach on a signing failure would defeat exactly that. So annotate here, + # upload regardless, and fail the job afterwards. Assets always land; an unsigned release is + # still loudly red rather than quietly wrong. + # See workspace/policies/fat-jar-release-assets.md, "Attach first, then go red". + id: signatures + run: | + set -uo pipefail + dir="snapshot-assets" + jars=$(find "$dir" -maxdepth 1 -name '*.jar' | sort) + if [ -z "$jars" ]; then + echo "::error::no jars in $dir -- the collection step produced nothing" + echo "missing=-1" >> "$GITHUB_OUTPUT" + exit 0 + fi + missing=0 + for jar in $jars; do + if [ ! -e "$jar.asc" ]; then + echo "::error::unsigned: $(basename "$jar") has no detached .asc" + missing=$((missing + 1)) + fi + done + echo "missing=$missing" >> "$GITHUB_OUTPUT" + [ "$missing" -eq 0 ] && echo "all $(echo "$jars" | wc -l) jar(s) signed" + exit 0 - name: Update snapshot pre-release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -3587,6 +3615,12 @@ jobs: gh release upload snapshot snapshot-assets/* \ --repo ${{ github.repository }} \ --clobber + - name: Fail if anything was attached unsigned + # After the upload on purpose: the assets must exist even when the signature does not. + if: ${{ always() && steps.signatures.outputs.missing != '0' }} + run: | + echo "::error::${{ steps.signatures.outputs.missing }} asset(s) attached without a signature (-1 means none were collected at all)" + exit 1 publish-release: name: Publish Release to Central @@ -3807,7 +3841,41 @@ jobs: GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} run: bash .github/sign-fatjars.sh release-assets + - name: Report unsigned assets (does not block the upload) + # Deliberately NON-blocking, and deliberately BEFORE the upload. Both attach jobs run even + # when their publish job failed, because a Central publish-poll timeout must not cost the + # GitHub assets: if Central is unreachable these are the ONLY way to get the artifacts at + # all. Refusing to attach on a signing failure would defeat exactly that. So annotate here, + # upload regardless, and fail the job afterwards. Assets always land; an unsigned release is + # still loudly red rather than quietly wrong. + # See workspace/policies/fat-jar-release-assets.md, "Attach first, then go red". + id: signatures + run: | + set -uo pipefail + dir="release-assets" + jars=$(find "$dir" -maxdepth 1 -name '*.jar' | sort) + if [ -z "$jars" ]; then + echo "::error::no jars in $dir -- the collection step produced nothing" + echo "missing=-1" >> "$GITHUB_OUTPUT" + exit 0 + fi + missing=0 + for jar in $jars; do + if [ ! -e "$jar.asc" ]; then + echo "::error::unsigned: $(basename "$jar") has no detached .asc" + missing=$((missing + 1)) + fi + done + echo "missing=$missing" >> "$GITHUB_OUTPUT" + [ "$missing" -eq 0 ] && echo "all $(echo "$jars" | wc -l) jar(s) signed" + exit 0 - name: Upload release assets uses: softprops/action-gh-release@v3 with: files: release-assets/* + - name: Fail if anything was attached unsigned + # After the upload on purpose: the assets must exist even when the signature does not. + if: ${{ always() && steps.signatures.outputs.missing != '0' }} + run: | + echo "::error::${{ steps.signatures.outputs.missing }} asset(s) attached without a signature (-1 means none were collected at all)" + exit 1