ci(release): wait for the mirrored tag, never mint it

The Codeberg publish has never once succeeded — 0.2.1, 0.2.2, 0.3.0, 0.3.1
and 0.3.2 all failed, and 0.3.0 was published by hand. Neither previous fix
could have worked, because the cause isn't in this file: ref writes to
jlmakiola/agendula on Codeberg fail. A tag push returns "cannot lock
references" and POST /tags returns an empty-bodied 500, while the identical
calls succeed on jlmakiola/calendula with the same token. Creating a release
mints a tag, so it inherited the same failure.

Attaching a release to a tag that is ALREADY there needs no ref write and
returns 201. So split the responsibility the way it should have been: the
push mirror owns delivering the tag, this step only attaches to it. Poll for
the tag, then POST with no target_commitish so the API attaches rather than
resolves a commit and mints one. If the tag never lands, fail with a pointer
at the mirror instead of trying to create it.

Drop continue-on-error. It reported green across five releases that never
published, which is how 0.3.1's crash fix reached F-Droid while the Codeberg
and Obtainium users who hit the crash got nothing.

Tested against the live API before committing (unlike its predecessors):
tag-present attaches 201, tag-absent exits 1 with the diagnostic. Note this
does not by itself restore publishing — the mirror cannot write the v0.3.2
tag either, so Codeberg must repair the repo's ref store first.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-20 19:50:57 +02:00
parent bc70ed3a9f
commit 36beb2d0ad
4 changed files with 43 additions and 24 deletions

View File

@@ -370,11 +370,13 @@ jobs:
# release. Needs the CODEBERG_RELEASE_TOKEN secret; skips cleanly if unset.
- name: Publish release to Codeberg
if: env.IS_RELEASE == 'true'
continue-on-error: true
# NOT continue-on-error: this step reported green through 0.2.1, 0.2.2,
# 0.3.0, 0.3.1 and 0.3.2 while never once publishing, which is how a
# crash-fix release reached F-Droid but not the Codeberg/Obtainium
# users who needed it. A broken mirror must fail the release loudly.
env:
TOKEN: ${{ secrets.CODEBERG_RELEASE_TOKEN }}
API: https://codeberg.org/api/v1/repos/jlmakiola/agendula
SHA: ${{ github.sha }}
run: |
set -e
if [ -z "${TOKEN:-}" ]; then
@@ -399,29 +401,39 @@ jobs:
sed -i -e '/./,$!d' release-notes.md
fi
[ -s release-notes.md ] || echo "_See CHANGELOG.md for ${VERSION}._" > release-notes.md
# Forgejo 500s on POST /releases when the tag ALREADY exists, and a
# bare tag is not a release — so GET /releases/tags then 404s and the
# upsert has no id to fall back on ("Could not resolve Codeberg
# release id", the 0.3.1 failure). Pushing the tag first therefore
# guarantees the 500 rather than avoiding it; 0.3.0 published because
# it POSTed while the tag was still absent. So: POST with the tag
# ABSENT and let the API mint tag + release together from
# target_commitish. Branches mirror reliably, so the commit is already
# on Codeberg — only a tag the mirror raced in ahead of us is in the
# way, and clearing it is safe precisely because no release owns it.
ID=$(curl -s -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG" | jq -r '.id // empty')
if [ -z "$ID" ]; then
curl -s -o /dev/null -w "codeberg tag DELETE HTTP %{http_code}\n" -X DELETE \
-H "Authorization: token $TOKEN" "$API/tags/$TAG"
# Never mint the tag here. Gitea's push mirror owns getting it to
# Codeberg; this step's only job is to attach a release to a tag that
# has already landed. That split matters because every way of creating
# a tag from here — git push, or a release POST carrying
# target_commitish for a tag Codeberg lacks — is a ref WRITE, and ref
# writes are what fail on this repo ("cannot lock references" on push,
# an empty-bodied 500 on the API). Attaching to a tag that is already
# present needs no ref write and succeeds.
#
# So: wait for the mirror, verify, then attach. If the tag never shows
# up, fail — do NOT fall back to creating it, which is what produced
# the silent breakage across 0.2.1 through 0.3.2.
TAG_OK=""
for i in $(seq 1 30); do
if [ "$(curl -s -o /dev/null -w '%{http_code}' \
-H "Authorization: token $TOKEN" "$API/tags/$TAG")" = "200" ]; then
TAG_OK=1; echo "Codeberg has $TAG (after ~$((i*10))s)"; break
fi
sleep 10
done
if [ -z "$TAG_OK" ]; then
echo "Codeberg never received $TAG from the push mirror (waited 300s)." >&2
echo "Not creating it here: ref writes to this repo fail, so that" >&2
echo "would 500. Check the mirror, then re-run once the tag is there." >&2
exit 1
fi
python3 - "$TAG" "$SHA" "$PRERELEASE" <<'PY' > cb-payload.json
# No target_commitish: the tag exists, so the API must attach to it
# rather than resolve a commit and mint one.
python3 - "$TAG" "$PRERELEASE" <<'PY' > cb-payload.json
import json, sys
tag, sha, pre = sys.argv[1:4]
tag, pre = sys.argv[1:3]
print(json.dumps({
"tag_name": tag,
# Recreate the tag as part of the release; the commit is on
# Codeberg already via the branch mirror.
"target_commitish": sha,
"name": tag,
"body": open("release-notes.md").read(),
"draft": False,
@@ -429,9 +441,9 @@ jobs:
"prerelease": pre == "true",
}))
PY
# Upsert (re-run safe): a release that already exists is PATCHed in
# place — the delete above is skipped in that case, so re-running
# never disturbs a published release.
# Upsert (re-run safe): a release already attached to this tag is
# PATCHed in place, so re-running never disturbs a published release.
ID=$(curl -s -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG" | jq -r '.id // empty')
if [ -n "$ID" ]; then
curl -s -o /dev/null -w "release PATCH HTTP %{http_code}\n" -X PATCH \
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \

1
cb-payload.json Normal file
View File

@@ -0,0 +1 @@
{"tag_name": "v0.3.1", "name": "v0.3.1", "body": "### Fixed\n- Agendula no longer crashes on launch. Every 0.3.0 install was affected: the\n release build stripped a constructor that the background-work scheduler needs\n to open its database, and that happens before the app draws anything.\n\n", "draft": true, "prerelease": true}

1
cb-response.json Normal file
View File

@@ -0,0 +1 @@
{"id":10985210,"tag_name":"v0.3.1","target_commitish":"","name":"v0.3.1","body":"### Fixed\n- Agendula no longer crashes on launch. Every 0.3.0 install was affected: the\n release build stripped a constructor that the background-work scheduler needs\n to open its database, and that happens before the app draws anything.\n\n","url":"https://codeberg.org/api/v1/repos/jlmakiola/agendula/releases/10985210","html_url":"https://codeberg.org/jlmakiola/agendula/releases/tag/v0.3.1","tarball_url":"https://codeberg.org/jlmakiola/agendula/archive/v0.3.1.tar.gz","zipball_url":"https://codeberg.org/jlmakiola/agendula/archive/v0.3.1.zip","hide_archive_links":false,"upload_url":"https://codeberg.org/api/v1/repos/jlmakiola/agendula/releases/10985210/assets","draft":true,"prerelease":true,"created_at":"2026-07-20T19:50:15+02:00","published_at":"2026-07-20T19:50:15+02:00","author":{"id":1256480,"login":"jlmakiola","login_name":"","source_id":0,"full_name":"Jean-Luc Makiola","email":"jlmakiola@noreply.codeberg.org","avatar_url":"https://codeberg.org/avatars/0b2d6a937427776f023efc504039607c8b24c7ad0331e6d7171d86cca11c8e78","html_url":"https://codeberg.org/jlmakiola","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2026-06-22T10:25:16+02:00","restricted":false,"active":false,"prohibit_login":false,"location":"Germany","pronouns":"he/him","website":"https://jeanlucmakiola.de","description":"I build privacy-respecting open-source apps — modern, well-designed, and made to last.","visibility":"public","followers_count":2,"following_count":0,"starred_repos_count":0,"username":"jlmakiola"},"assets":[],"archive_download_count":{"zip":0,"tar_gz":0}}

5
release-notes.md Normal file
View File

@@ -0,0 +1,5 @@
### Fixed
- Agendula no longer crashes on launch. Every 0.3.0 install was affected: the
release build stripped a constructor that the background-work scheduler needs
to open its database, and that happens before the app draws anything.