The first release run tagged the repo and then failed, leaving v0.1.0 with
no release behind it and no way to ever get one.
REGISTRY_USER and REGISTRY_TOKEN are not configured on this repo, but the
tag push SUCCEEDED anyway: actions/checkout leaves an
`http.<host>.extraheader` credential in the local git config, so
`git remote set-url` to a URL with empty credentials still authenticated
through that leftover header. The release API call had no such fallback and
returned 401 (visible in the run log as `REGISTRY_USER:` / `REGISTRY_TOKEN:`
with empty values). So the run got exactly far enough to do the one thing
that is hard to undo.
Worse, that state was self-perpetuating. The plan step treated any existing
tag as "nothing to release", so every subsequent push to main would see
v0.1.0, set RELEASE=false, and stand down — the release would never appear,
and no amount of re-running would fix it.
Two fixes:
A credential preflight, before anything is built or pushed, gated on the
run intending to publish so a docs:/chore:-only merge still passes on a
repo without secrets. It names the missing secrets and the scope they
need, rather than failing at whichever step happens to use them first.
Orphan-tag recovery. The plan step now asks the API whether a release
exists for the tag: 200 means stand down, 404 means an earlier run died
after tagging, so reuse the tag and publish the release it is missing.
This deliberately overrides the RELEASE=false the bump logic just decided
— with the tag already in place there are no releasable commits after it,
which is precisely why the stuck state could not clear itself.
Anything other than 200/404 (network failure, bad token) is refused
rather than guessed, because assuming "no release" would republish over a
good one.
The tag step reuses an existing tag instead of failing on `git tag`, and a
recovery run's changelog summarizes what the tag contains
(previous-tag..this-tag) instead of the empty range after it.
Once REGISTRY_USER / REGISTRY_TOKEN are set, the next push to main will
finish the release that the first run started — v0.1.0, from the same
commit it already points at.
Verified against the live repo state: the plan step now reports
release=true reuse_tag=true for the orphaned v0.1.0 and renders the correct
changelog; a tag that does have a release (checked against link's v0.3.0)
still stands down; a fresh repo still takes the seed path; and the
preflight fails loudly on empty secrets and passes on populated ones.
Co-Authored-By: Claude <noreply@anthropic.com>