servuo-plugins hit both of these on its first real release run; this repo
runs the same engine, so it has the same two defects latent.
REGISTRY_USER / REGISTRY_TOKEN were empty there, yet the tag push
SUCCEEDED: 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. Net result was the worst
available outcome — the repo tagged, no release, and a failed job.
Two fixes:
A credential preflight, before anything is built or pushed, gated on the
run actually intending to publish so a docs:/chore:-only merge (or this
repo's pre-crate no-op) still passes on a repo with no secrets. It names
the missing secrets and the scope they need instead of failing wherever
they happen to be used first.
Orphan-tag recovery. A tag with no release behind it means an earlier run
died after tagging, and the old code treated any existing tag as "nothing
to release" — so that state could never clear itself: every later run
would see the tag and stand down, forever. The plan step now asks the API
whether a release exists for the tag, and on 404 reuses the tag and
publishes the release it is missing. This deliberately overrides the
RELEASE=false the bump logic just decided, which is the whole point —
with the tag in place there are no releasable commits after it.
Anything other than 200/404 (network failure, bad token) is refused
rather than guessed, since assuming "no release" would republish over a
good one.
The tag step now reuses an existing tag instead of failing on `git tag`,
and the changelog for a recovery run summarizes what the tag contains
(previous-tag..this-tag) rather than the empty range after it.
sync-project-tree gets the same preflight: its first run on main failed
with an opaque `git clone` error against `https://:@host/...` that said
nothing about a missing secret.
Verified by extracting every run block and exercising the paths: empty
secrets fail the preflight with a legible message and populated ones pass;
the no-Cargo.toml guard still short-circuits to release=false; a crate with
no tag still takes the seed path; and against real repo state, a tag with a
release stands down while an orphan tag recovers.
Co-Authored-By: Claude <noreply@anthropic.com>