From 7bb9dc59095d851c5f4a2d0deb9748db24f77855 Mon Sep 17 00:00:00 2001 From: Rod Vince Date: Tue, 6 Oct 2026 22:45:27 -0600 Subject: [PATCH 1/3] ci: a commit speaks for its author alone, and CI reads it CI read the code and never the words that travel with it: a commit message, and the title and body of a pull request, which become the commit when it is squashed. The message gate now reads them, as a step of the job that branch protection already requires, so a refused text blocks the merge the way a failing test does. The gate is the family's one copy and lives in getmilpa/devtools. It refuses a co-author trailer for an identity the house does not list, a generator signature and a session reference, and it ends red when it cannot read. Three things change around it. CI also runs when a pull request is edited, because its text can change after the last push. The token may read pull requests. And there is one run per pull request, so a newer event replaces the run it makes stale. --- .github/workflows/ci.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5264fdc..91b72fc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,9 +4,19 @@ on: push: branches: [ main ] pull_request: + # `edited` too: a pull request's title and body become its squashed commit, so the message gate + # reads them again whenever they change. + types: [ opened, synchronize, reopened, edited ] permissions: contents: read + pull-requests: read + +# One run per pull request. A newer event (a push, an edit) replaces the run it makes stale, so an +# older run can never finish after it and leave a verdict about a text that is no longer there. +concurrency: + group: ci-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true jobs: test: @@ -26,6 +36,11 @@ jobs: php-version: ${{ matrix.php }} coverage: xdebug + # The family's one copy of this gate lives in getmilpa/devtools. It reads this pull request's + # title, body and commits (or the commits a push added) and needs nothing from the checkout. + - name: Message gate (a commit speaks for its author alone) + uses: getmilpa/devtools/.github/actions/message-gate@main + - name: Validate composer.json run: composer validate --strict From b61f26b94341f0b012cc43e37eae72012c99a89d Mon Sep 17 00:00:00 2001 From: Rod Vince Date: Wed, 7 Oct 2026 00:19:17 -0600 Subject: [PATCH 2/3] ci: one run per commit of a pull request, and the comment says what a squash carries Grouping runs by pull request let a run for an older commit, started later by a re-run or an approval, cancel the run of the newest commit and leave its checks cancelled. The group is now the pull request and its head commit: an edit of the title or the body still replaces the run it makes stale, and a run for another commit is left alone. The comment on `edited` said the body becomes the squashed commit. It no longer does: the title does, and the body stays public beside it, which is why the gate still reads both. --- .github/workflows/ci.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 91b72fc..4df5454 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,18 +4,19 @@ on: push: branches: [ main ] pull_request: - # `edited` too: a pull request's title and body become its squashed commit, so the message gate - # reads them again whenever they change. + # `edited` too: the title of a pull request becomes its squashed commit and the body stays public + # beside it, so the message gate reads both again whenever they change. types: [ opened, synchronize, reopened, edited ] permissions: contents: read pull-requests: read -# One run per pull request. A newer event (a push, an edit) replaces the run it makes stale, so an -# older run can never finish after it and leave a verdict about a text that is no longer there. +# One run per commit of a pull request. A newer event on the same commit (an edit of the title or +# the body) replaces the run it makes stale. A run for another commit is left alone, so starting an +# older one, by a re-run or an approval, can never cancel the run of the newest. concurrency: - group: ci-${{ github.event.pull_request.number || github.run_id }} + group: ci-${{ github.event.pull_request.number || github.run_id }}-${{ github.event.pull_request.head.sha || github.sha }} cancel-in-progress: true jobs: From b835c73295a0672be077182c0e8859341fd96ded Mon Sep 17 00:00:00 2001 From: Rod Vince Date: Wed, 7 Oct 2026 10:34:45 -0600 Subject: [PATCH 3/3] ci: runs are not grouped or cancelled, so the latest event's run always finishes Branch protection takes, for each check, the run of the latest event on a commit. Grouping runs to cancel the stale one could cancel that very run: when two events reached the same commit within a second, the run of the earlier one sometimes survived, and the pull request stayed blocked with a green run on its head. It happened in 2 of the 37 pull requests of this change. Without the group every run finishes and the latest event decides, which is what was wanted in the first place. --- .github/workflows/ci.yml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4df5454..a91a626 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,20 +5,14 @@ on: branches: [ main ] pull_request: # `edited` too: the title of a pull request becomes its squashed commit and the body stays public - # beside it, so the message gate reads both again whenever they change. + # beside it, so the message gate reads both again whenever they change. No `concurrency` here: + # branch protection takes the run of the latest event, and cancelling one can leave it blocked. types: [ opened, synchronize, reopened, edited ] permissions: contents: read pull-requests: read -# One run per commit of a pull request. A newer event on the same commit (an edit of the title or -# the body) replaces the run it makes stale. A run for another commit is left alone, so starting an -# older one, by a re-run or an approval, can never cancel the run of the newest. -concurrency: - group: ci-${{ github.event.pull_request.number || github.run_id }}-${{ github.event.pull_request.head.sha || github.sha }} - cancel-in-progress: true - jobs: test: name: PHP ${{ matrix.php }}