From 4dbe76def1da56f84d43adf0943a1084a7aa335b Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Tue, 8 Sep 2026 00:37:04 -0300 Subject: [PATCH] CI runs once, and never cancels a merge to main MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two problems, both visible as a column of `cancelled` in the run list — 14 of the last 40 runs here. **Every push ran the gate twice.** `push:` with no filter fires alongside `pull_request:` for any branch with a pull request open, and the concurrency group cleaned that up after the fact: same key for both events, cancel-in-progress. It worked and it cost a run each time, because both started and took a runner before one was killed. On a self-hosted runner it costs more than the runner: two runs of this gate install the same release and restart the same containerd, so the loser does not merely stop, it fails during setup — and the failure is about a machine two runs were fighting over rather than about the change. A branch is tested through its pull request now, and main when something lands on it. **A merge to main could be cancelled.** cancel-in-progress applied there too, so two merges landing close together left the first with no verdict. That already happened, to the merge of #3. Two merges are two things to verify, not one, and a gate whose own rule is that a skipped lane counts as a failure should not quietly drop a whole run. What is given up: a branch with no pull request open is not tested. That is what opening one is for. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_019GHRRFvbnZi5q3KSTvyW5a --- .github/workflows/ci.yml | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aec0b6e..09517de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,17 +1,35 @@ name: CI +# A branch is tested through its pull request, and main is tested when something lands on +# it. Not both: `push:` with no filter fires alongside `pull_request:` for every push to a +# branch that has one open, so every push started two identical runs. +# +# The concurrency group used to clean that up afterwards — same key for both events, +# cancel-in-progress — which worked and cost a run each time: both started, both took a +# runner, and one was killed. What is left in the run list is a column of `cancelled` that +# reads like something is wrong, and 14 of the last 40 runs here were exactly that. +# +# The cost is not only the runner. On a self-hosted one, two runs of this gate install the +# same release and restart the same containerd, so the loser does not merely stop — it +# fails during setup, and the failure is about a machine two runs were fighting over +# rather than about the change. +# +# What is given up: a branch with no pull request open is not tested. That is what +# opening one is for. on: push: + branches: [main] pull_request: -# One run per branch, not one per event. A push to a branch with an open pull request fires -# both triggers; keying on `head_ref || ref_name` gives both the same string, while -# `github.ref` — the obvious choice — does not (`refs/pull/N/merge` vs -# `refs/heads/my-branch`, two groups, nothing de-duplicated). `cancel-in-progress` then -# makes a new push supersede its predecessor, on main as well. +# A new push to a branch supersedes the run its predecessor started, because only the tip +# is worth a verdict. +# +# Never on main. A cancelled run there is a merge commit nobody proved, which is the +# thing this gate exists to make impossible — it already happened once, to the merge of +# #3. Two merges landing close together are two things to verify, not one. concurrency: group: ci-${{ github.head_ref || github.ref_name }} - cancel-in-progress: true + cancel-in-progress: ${{ github.ref_name != 'main' }} # Least privilege by default: the gate reads the tree and nothing else. The jobs that # touch GitHub Packages raise it to `packages: read` themselves, at the job, so the