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:
@@ -152,7 +152,96 @@ jobs:
|
|||||||
|
|
||||||
# One push for both refs: a commit that lands without its tag builds
|
# One push for both refs: a commit that lands without its tag builds
|
||||||
# nothing and looks like a successful release.
|
# 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
|
- name: Summary
|
||||||
env:
|
env:
|
||||||
|
|||||||
Reference in New Issue
Block a user