ci(build): skip the redundant master build when a release is cut
Build and Push Images / Build jorgecuadros-api (push) Canceled after 1m10s
Build and Push Images / Build jorgecuadros-web (push) Canceled after 1m8s

release.yml pushes the release commit and its tag in a single `git push`,
so Gitea created two build.yml runs for the same commit. Only the tag run
matters: it emits the X.Y.Z and X.Y image tags, and since it is the same
commit it publishes `latest` and `sha-<short>` as well. The master run was
pure duplicate work that had to be waited out or cancelled by hand.

Guard the build job with an `if` that skips a branch push whose head commit
message starts with `chore(release):`. Ordinary pushes to master are
unaffected, and tag pushes and manual dispatches always build.

The skipped master run keeps the release commit's sha, which would have let
release.yml's "Verify build.yml started" check go green on it alone even if
the tag run were never created — the exact failure that check exists to
catch. It now also requires the run's ref to be the tag, falling back to the
sha match only when the API reports no ref.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-01 02:28:09 -07:00
co-authored by Claude Opus 5
parent 98f7aa8a2d
commit e589bda28b
2 changed files with 25 additions and 1 deletions
+10
View File
@@ -37,6 +37,16 @@ env:
jobs: jobs:
build: build:
name: Build ${{ matrix.image }} name: Build ${{ matrix.image }}
# release.yml pushes the release commit and its tag in a single `git push`,
# so Gitea creates two runs for the same commit: one for master, one for the
# tag. Only the tag run matters — it is the one that emits the X.Y.Z / X.Y
# image tags, and it publishes `latest` and `sha-<short>` too, since it is
# the same commit. Skip the branch run rather than racing or cancelling it.
# Ordinary pushes to master (any message but `chore(release):`) still build.
if: >-
github.event_name != 'push' ||
startsWith(github.ref, 'refs/tags/') ||
!startsWith(github.event.head_commit.message, 'chore(release):')
runs-on: docker runs-on: docker
container: container:
image: docker:27-dind image: docker:27-dind
+15 -1
View File
@@ -191,12 +191,26 @@ jobs:
const tag = `v${process.env.VERSION}`; const tag = `v${process.env.VERSION}`;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// The master push and the tag push carry the SAME commit, so a sha
// match alone is not enough: build.yml skips the master run by
// design, and that skipped run would satisfy a sha-only check even
// if the tag run were never created. When the API reports a ref for
// the run, require it to be the tag; when it reports none, fall back
// to the sha match rather than failing a release over a field name.
const isTagRun = (r) => {
const ref = r.head_branch || r.ref || "";
return !ref || ref === tag || ref === `refs/tags/${tag}`;
};
const started = async () => { const started = async () => {
const res = await fetch(`${base}/actions/runs?limit=30`, { headers }); const res = await fetch(`${base}/actions/runs?limit=30`, { headers });
if (!res.ok) throw new Error(`runs query failed: HTTP ${res.status}`); if (!res.ok) throw new Error(`runs query failed: HTTP ${res.status}`);
const body = await res.json(); const body = await res.json();
return (body.workflow_runs || []).some( return (body.workflow_runs || []).some(
(r) => r.head_sha === sha && String(r.path || "").includes("build.yml"), (r) =>
r.head_sha === sha &&
String(r.path || "").includes("build.yml") &&
isTagRun(r),
); );
}; };