ci(release): fail the release when the build never starts

Gitea creates workflow runs from the post-receive hook. When that hook
errors the refs still land, git prints `remote: error: Internal Server
Error` and exits 0 — a post-receive failure does not fail a push. v1.0.3
was cut exactly that way: tag pushed, no build run created, no images
published, and the release step green. It surfaced two steps later as a
404 when the deploy tried to pull 1.0.3.

Capture the push output and warn on `remote: error`, then verify a
build.yml run actually exists for the new commit, dispatching it against
the tag if not. Fail the release if that does not take either, so a
release that publishes nothing is red instead of green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-01 01:58:10 -07:00
co-authored by Claude Opus 5
parent e082113640
commit fdbe9fdb88
+90 -1
View File
@@ -152,7 +152,96 @@ jobs:
# One push for both refs: a commit that lands without its tag builds
# nothing and looks like a successful release.
git push origin "HEAD:master" "refs/tags/v${VERSION}"
#
# The output is captured because a failing *post-receive* hook does not
# fail the push: git prints `remote: error: ...`, updates both refs and
# exits 0. That is how v1.0.3 was cut — the hook 500'd, so Gitea never
# created the build run, and this step went green anyway.
if ! git push origin "HEAD:master" "refs/tags/v${VERSION}" 2>push.log; then
cat push.log
echo "::error::Push failed. Nothing was released."
exit 1
fi
cat push.log
if grep -q '^remote: error' push.log; then
echo "::warning::The remote's post-receive hook errored. Both refs landed,"
echo "::warning::but Gitea most likely created no workflow run for them."
echo "::warning::The next step checks and dispatches build.yml if needed."
fi
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
id: push
# Gitea creates workflow runs from the post-receive hook, so a hook error
# silently costs you the build: the tag exists, no image is ever published,
# and the failure only surfaces later as a 404 when deploy pulls the image.
# Confirm the run exists; dispatch it if it does not; fail loudly if that
# does not work either.
- name: Verify build.yml started
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
VERSION: ${{ steps.ver.outputs.version }}
SHA: ${{ steps.push.outputs.sha }}
run: |
node -e '
const base = `${process.env.GITHUB_SERVER_URL}/api/v1/repos/${process.env.GITHUB_REPOSITORY}`;
const headers = { Authorization: `token ${process.env.RELEASE_TOKEN}` };
const sha = process.env.SHA;
const tag = `v${process.env.VERSION}`;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const started = async () => {
const res = await fetch(`${base}/actions/runs?limit=30`, { headers });
if (!res.ok) throw new Error(`runs query failed: HTTP ${res.status}`);
const body = await res.json();
return (body.workflow_runs || []).some(
(r) => r.head_sha === sha && String(r.path || "").includes("build.yml"),
);
};
// The hook fires synchronously with the push, so a run that is coming
// is usually already there; the retries cover a busy instance.
const poll = async (attempts) => {
for (let i = 0; i < attempts; i++) {
if (await started()) return true;
await sleep(10_000);
}
return started();
};
(async () => {
if (await poll(3)) {
console.log(`build.yml is running for ${sha}.`);
return;
}
console.log(`No build.yml run for ${sha}. Dispatching against ${tag}.`);
const res = await fetch(
`${base}/actions/workflows/build.yml/dispatches`,
{
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
// Must be the tag, not master: metadata-action only emits the
// X.Y.Z and X.Y image tags when the ref is a semver tag.
body: JSON.stringify({ ref: tag }),
},
);
if (!res.ok) console.log(`Dispatch returned HTTP ${res.status}.`);
if (await poll(3)) {
console.log(`build.yml is running for ${sha}.`);
return;
}
console.log(`::error::${tag} is pushed but nothing is building it, and`);
console.log(`::error::the dispatch did not take. Run "Build and Push Images"`);
console.log(`::error::by hand with ref=${tag} (the tag, not master), then`);
console.log(`::error::deploy. Check the Gitea server log for the`);
console.log(`::error::post-receive error while you are at it.`);
process.exit(1);
})();
'
- name: Summary
env: