diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d747f1f..00c7885 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -100,18 +100,54 @@ jobs: # The mirror's registry, from the image just built: no second build, # and Cloudflare rate limiting git.tben.me (429/1015 on blob uploads) - # can never fail a release the installer depends on. + # can never fail a release the installer depends on. It must still be + # loud: `continue-on-error` alone leaves the check green when the + # mirror does nothing at all, which is how the 413 below went unseen + # for four releases. - name: Mirror to Gitea + id: mirror continue-on-error: true env: GITEA_TAGS: ${{ steps.tags.outputs.gitea }} GHCR_IMAGES: ${{ steps.tags.outputs.ghcr }} run: | - set -euo pipefail + set -uo pipefail first_ghcr="$(printf '%s\n' "$GHCR_IMAGES" | head -n1)" args=() while IFS= read -r tag; do [ -n "$tag" ] || continue args+=(--tag "${tag}") done <<< "$GITEA_TAGS" - docker buildx imagetools create "${args[@]}" "${first_ghcr}" + + output="$(mktemp)" + status=0 + docker buildx imagetools create "${args[@]}" "${first_ghcr}" 2>&1 | tee "$output" || status=$? + + if [ "$status" -eq 0 ]; then + { + echo "### Gitea mirror" + echo + echo "Mirrored \`${first_ghcr}\` to:" + echo + printf -- '- \`%s\`\n' $GITEA_TAGS + } >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + + # The usual cause: git.tben.me sits behind Cloudflare, whose + # request-body cap is 100 MB, and this image has a 243 MiB + # `bun install` layer that no route through it can carry. + reason="$(grep -oE '[0-9]{3} [A-Za-z ]{0,40}' "$output" | tail -n1)" + { + echo "### Gitea mirror failed" + echo + echo "\`${first_ghcr}\` did **not** reach the Gitea registry. The release itself is fine: the installer defaults to GHCR." + echo + echo '```' + tail -n 12 "$output" + echo '```' + echo + echo "See the [registry push limits](https://developers.cloudflare.com/workers/platform/limits/): git.tben.me is behind Cloudflare, which refuses any request body over 100 MB, and this image's \`bun install\` layer alone is about 243 MiB." + } >> "$GITHUB_STEP_SUMMARY" + echo "::warning title=Gitea mirror failed::${reason:-docker buildx imagetools create failed}; the image did not reach git.tben.me. See the job summary." + exit "$status" diff --git a/scripts/lib/publish-release.sh b/scripts/lib/publish-release.sh index 445d3bb..670d8f8 100755 --- a/scripts/lib/publish-release.sh +++ b/scripts/lib/publish-release.sh @@ -134,9 +134,24 @@ prepare_tag() { # END:tag # MARK:notes-file -# Writes release_notes into NOTES_FILE, which the caller cleans up. +# Writes release_notes into NOTES_FILE, which the caller cleans up. The +# compare base is the host's /compare/ path; the tag range is added here, +# since only this side knows which tag came before (a bare /compare/ opens +# the host's default comparison rather than this release's). +build_compare_url() { + local compare_base="$1" + local from="$2" + + if [ -z "$from" ]; then + printf '%s' "$compare_base" + return 0 + fi + printf '%s%s...%s' "$compare_base" "$from" "$TAG" +} + write_notes_file() { - local compare_url="$1" + local compare_base="$1" + local compare_url="" local from="" # A dry run makes no tag, so the notes are read up to HEAD instead. local to="$TAG" @@ -153,6 +168,8 @@ write_notes_file() { detail "No earlier tag, so every commit is listed." fi + compare_url="$(build_compare_url "$compare_base" "$from")" + NOTES_FILE="$(mktemp -t cobble-release-notes.XXXXXX)" release_notes --from "$from" --to "$to" --compare-url "$compare_url" >"$NOTES_FILE" ok "Wrote the release notes to $NOTES_FILE." diff --git a/scripts/publish-gitea.sh b/scripts/publish-gitea.sh index 99152f4..01d32a3 100755 --- a/scripts/publish-gitea.sh +++ b/scripts/publish-gitea.sh @@ -208,6 +208,9 @@ else if [ -n "${response_file:-}" ]; then rm -f "$response_file" fi + if [ "$api_status" = "403" ]; then + fail "Gitea answered HTTP 403: the token's user cannot write to $REPO_SLUG. The tags and $BRANCH still push over SSH, but creating a release needs the token's user to be a writer (write or admin) on the repository. Check it with: curl -s -H \"Authorization: token \$GITEA_TOKEN\" $API_URL/repos/$REPO_SLUG | grep -o '\"permissions\":{[^}]*}'" + fi fail "Gitea answered HTTP $api_status for the release." ;; esac diff --git a/scripts/publish-tools.test.ts b/scripts/publish-tools.test.ts index 9e5b9d9..3283acb 100644 --- a/scripts/publish-tools.test.ts +++ b/scripts/publish-tools.test.ts @@ -292,6 +292,38 @@ describe("publish lib release notes", () => { }); }); +describe("publish lib compare link", () => { + // The range is what makes the link open this release's diff; a bare + // /compare/ opened the host's default comparison instead. + test("adds the tag range to the compare path", async () => { + const block = await blockCaller({ file: RELEASE, name: "notes-file" }); + + const out = await runBash({ + script: [ + block, + `TAG=v0.1.4`, + `build_compare_url https://example.test/compare/ v0.1.3`, + ].join("\n"), + }); + + expect(out).toBe("https://example.test/compare/v0.1.3...v0.1.4"); + }); + + test("leaves the path alone when there is no earlier tag", async () => { + const block = await blockCaller({ file: RELEASE, name: "notes-file" }); + + const out = await runBash({ + script: [ + block, + `TAG=v0.1.0`, + `build_compare_url https://example.test/compare/ ""`, + ].join("\n"), + }); + + expect(out).toBe("https://example.test/compare/"); + }); +}); + describe("publish lib json escaping", () => { const sample = 'a\nb\t"c" \\ é';