From fe11e3dea7a67d14624b8feeefbb461356d8f02e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 12:14:16 +0000 Subject: [PATCH] ci: three guards on the snapshot prune, and why it is asset by asset The step that removes the assets a snapshot run did not upload now refuses an empty upload (every asset of the release would count as stale), treats an asset that is already gone as removed (one re-read of the release after a failed delete-asset), and reports a failed deletion while trying the rest, failing only afterwards -- the assets of this upload are on the release either way, so this is "attach first, then red" like the signature check. The guards come from the job's own history, dug up from git and the Actions API: its first form (2026-05-07) deleted and recreated the whole snapshot release, and on 2026-05-10 the runs of several main pushes minutes apart deleted it under each other -- every job of each burst but the last failed in that step with its artifacts present. The job went with the Central migration the next morning and came back as view-or-create plus upload --clobber without any cleanup, which is how the backlog grew; no commit named the failures. The step comment, CLAUDE.md and the CHANGELOG bullet say so now, and the runs are recorded in workspace/policies/fat-jar-release-assets.md. Shared text with the two sibling Maven-only repositories: the github-snapshot job hash in .github/shared-files.sha256 is rewritten. Verified: the script, extracted verbatim from the workflow, against a gh stub in eight cases (empty upload refused with nothing deleted; empty release and equal sets are no-ops; stale assets removed; an asset gone under us tolerated; a real deletion failure reported after the rest were tried; a failed re-read reported; a failed release view fails before anything is deleted), bash -n over every run script, the shared-files and release-gate checks, the buildcheck unit tests. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01AytmJF9faEiQEVt6eetQS2 --- .github/shared-files.sha256 | 2 +- .github/workflows/publish.yml | 39 +++++++++++++++++++++++++++++++++-- CHANGELOG.md | 7 +++++-- CLAUDE.md | 6 ++++-- 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/.github/shared-files.sha256 b/.github/shared-files.sha256 index 9af6918..de628f5 100644 --- a/.github/shared-files.sha256 +++ b/.github/shared-files.sha256 @@ -68,5 +68,5 @@ fcb6cd0e2a71205b918f545f2680d378c790b774b9852129c5f9c7f3f8eb9860 .github/workfl 09230b04c5f14bdda50a0c862152ff197a656cbc3fd0d110899c5f8150f8aa9a .github/workflows/publish.yml#check-snapshot 51e987d59efd9887b43e345edc0e0f3db21686879e3ca6ea9f0fdbc493d37121 .github/workflows/publish.yml#check-tag a8637f88765da759a6eaba243d0d0d695053174df581ccbbcc5e5f3a475fc9e9 .github/workflows/publish.yml#verify-signing-key-gradle -f20f0a991ce68fa1dcc8ef4daffc5e5dea2093d9b578aa41d17f18a56555d79f .github/workflows/publish.yml#github-snapshot +83a1288c55d35e4d3972e013009577fb594a9a5fb8b9b7c96a7f6a19db20d8d1 .github/workflows/publish.yml#github-snapshot ce90d90fcb1f2de35075b91c74411e9eb7d3b43e7965fd592744e914fd04c5db .github/workflows/publish.yml#github-release diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 57c1925..7967479 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -528,23 +528,58 @@ jobs: # upload on purpose: a failed upload must not leave the release emptier than it was. And only # here: a versioned v* release is history, github-release never deletes anything. Every # deletion is printed by name, so the job log is the audit trail. + # + # Asset by asset, never the release. The first form of this job (2026-05-07) deleted the whole + # `snapshot` release with its tag and recreated it; on 2026-05-10 the runs of several main + # pushes minutes apart deleted it under each other, and every job of each burst but the last + # failed in that step with its artifacts present. The job then went with the Central + # migration and came back as `view || create` + `upload --clobber` without any cleanup, which + # is how the backlog grew (workspace/policies/fat-jar-release-assets.md has the runs). Hence + # three guards: an empty upload is refused (every asset would count as stale; the download + # and upload steps above fail first, the guard keeps it so), an asset that is already gone + # counts as removed (two overlapping dispatches may both pick it), and a failed deletion is + # reported and the rest still tried -- the assets of this upload are on the release either + # way, so this is "attach first, then red" like the signature check. env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail keep="$(ls -1 snapshot-assets | sort)" - current="$(gh release view snapshot --repo ${{ github.repository }} --json assets --jq '.assets[].name' | sort)" + if [ -z "$keep" ]; then + echo "::error::refusing to prune: this run uploaded nothing, so every asset of the release would count as stale" + exit 1 + fi + list_assets() { + gh release view snapshot --repo ${{ github.repository }} --json assets --jq '.assets[].name' | sort + } + current="$(list_assets)" stale="$(comm -13 <(printf '%s\n' "$keep") <(printf '%s\n' "$current"))" if [ -z "$stale" ]; then echo "nothing to remove: the release holds exactly the $(printf '%s\n' "$keep" | wc -l) asset(s) of this upload" exit 0 fi echo "removing $(printf '%s\n' "$stale" | wc -l) asset(s) that are not part of this upload:" + failed=0 while IFS= read -r name; do [ -n "$name" ] || continue echo " deleting $name" - gh release delete-asset snapshot "$name" --repo ${{ github.repository }} --yes + if gh release delete-asset snapshot "$name" --repo ${{ github.repository }} --yes; then + continue + fi + if ! remaining="$(list_assets)"; then + echo "::error::could not delete $name, and could not re-read the release to tell whether it is gone" + failed=$((failed + 1)) + elif printf '%s\n' "$remaining" | grep -qxF -- "$name"; then + echo "::error::could not delete $name" + failed=$((failed + 1)) + else + echo " $name was already gone" + fi done <<< "$stale" + if [ "$failed" -ne 0 ]; then + echo "::error::$failed asset(s) could not be removed; the $(printf '%s\n' "$keep" | wc -l) asset(s) of this upload are on the release" + exit 1 + fi echo "kept the $(printf '%s\n' "$keep" | wc -l) asset(s) of this upload" - name: Fail if anything was attached unsigned # After the upload on purpose: the assets must exist even when the signature does not. diff --git a/CHANGELOG.md b/CHANGELOG.md index 4baf127..f4b128e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,8 +79,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 signature check existed) and three SpotBugs plugin jars (`fb-contrib-7.6.4.jar`, `fb-contrib-7.7.4.jar`, `findsecbugs-plugin-1.14.0.jar`) an old collect step uploaded from `target/spotbugs/` in 2026-05, 5 of its 11 assets. `github-snapshot` now deletes every asset - the run did not upload, after the upload and each printed by name; versioned releases are never - pruned, and the next snapshot run clears the backlog by itself. The collect steps copy + the run did not upload, after the upload and each printed by name; it refuses an empty upload, + treats an asset that is already gone as removed and fails only after trying every deletion (asset + by asset on purpose: the job's first form deleted and recreated the release, and failed in every + burst of main pushes in May 2026); versioned releases are never pruned, and the next snapshot run + clears the backlog by itself. The collect steps copy `-.pom` + `.pom.asc` as well (Central had them, the release did not), and the signature check counts every attachable type (`*.jar`, `*.aar`, `*.pom`) instead of jars alone. Shared job text with the two sibling repositories (`shared-files.sha256`), the convention diff --git a/CLAUDE.md b/CLAUDE.md index 4302c48..6bcd510 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -251,8 +251,10 @@ exists here. The attach jobs are shared text with the two sibling Maven-only rep signature check counts every attachable type (`*.jar`, `*.aar`, `*.pom`), and `github-snapshot` ends by deleting every asset of the `snapshot` pre-release that the run did not upload, each printed by name -- `gh release upload --clobber` never removed anything, so the release accumulated every past -version. Versioned releases are never pruned. See the policy's "Attach first, then go red" and "The -`snapshot` pre-release holds exactly the current upload". +version. The prune refuses an empty upload, treats an asset that is already gone as removed and +fails only after trying every deletion; versioned releases are never pruned. See the policy's +"Attach first, then go red" and "The `snapshot` pre-release holds exactly the current upload", which +also records the delete-and-recreate form of this job (May 2026) and why it is gone. ## Shared files and the release gate (`shared-files` job)