From ec656dd21ec9fa00baed5f09c706c6430761fd6b Mon Sep 17 00:00:00 2001 From: Elmehdi Aitbrahim Date: Tue, 1 Sep 2026 08:39:12 -0400 Subject: [PATCH] chore(actions): name every run, and stop re-scanning the merge commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TWO PROBLEMS, ONE OF THEM COSMETIC AND ONE NOT. The Actions list titles every run with the COMMIT MESSAGE. One push to `main` starts four workflows, so all four appear under the same `Merge pull request #NNN from ...` heading -- which reads as one job running four times rather than four different checks running once. That is how this change came to exist: the quartet at 12:22:08Z on 6ca893a looked like duplicated work and was not. `run-name` on all SEVEN workflows, not the four that collide today. Two of the others (`install-smoke`, `installer-smoke`) already run on pull requests, and a release run sitting in the same list under a commit message is the same confusion, just rarer. DELIBERATELY NOT `github.event.head_commit.message`. That was the obvious interpolation and it is the wrong one here: this repository writes long, multi-paragraph commit messages, GitHub already shows the commit beside every run, and re-titling with it restores exactly the clutter this removes. What the default title DROPS is the workflow's own identity, so that is what run-name carries -- name, event, and ref. THE REAL COST WAS ELSEWHERE. `security.yml` answered both `pull_request` and `push: branches: [main]`, so both its jobs ran a second time on the merge commit to re-answer a question the pull-request run had answered four minutes earlier: `dependencies` audits `uv.lock`, which the merge does not change, and neither job reads anything a merge could combine differently. Measured over the last forty runs: 11.4 minutes, ~2.5 per merge, 30s of it `dependencies` and ~2min `codeql`. The `push` trigger goes. `ci.yml` KEEPS its one -- when two pull requests merge in quick succession the post-merge run is the only thing that ever tests the tree that actually landed, and that argument does not transfer to a dependency audit. ⚠️ The tradeoff is recorded in the file rather than left to be discovered. Code scanning reports alerts against the DEFAULT BRANCH, and that baseline is refreshed by analyses OF the default branch -- which now means the weekly cron alone. A finding introduced by a merge, or an alert a merge fixes, can take up to seven days to appear or clear in the Security tab. Pull-request analysis is unaffected and still runs on every change, so nothing reaches `main` unscanned; what is slower is the SUMMARY of what is on `main`, not the scanning itself. Pinned, because the clutter is caused by the workflows nobody thinks about: the new test discovers every `*.yml` from disk rather than listing them, so a workflow added tomorrow is covered on the day it lands. 4 mutants, 4 killed. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NzuKAe2RVrPt9acVAWjRyL --- .github/workflows/ci.yml | 8 ++++++ .github/workflows/code-quality.yml | 1 + .github/workflows/install-smoke.yml | 1 + .github/workflows/installer-smoke.yml | 1 + .github/workflows/migrate.yml | 1 + .github/workflows/release.yml | 1 + .github/workflows/security.yml | 31 ++++++++++++++-------- tests/test_workflow_triggers.py | 38 ++++++++++++++++++++++++++- 8 files changed, 70 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33eda65b..e07bae80 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,12 @@ name: CI +# The Actions list titles every run with the COMMIT MESSAGE, so four different workflows +# triggered by one push read as four identical entries -- which is how a merge came to look +# like a job running four times. `run-name` replaces that title with what the run IS. +# +# Deliberately NOT `github.event.head_commit.message`: this repository writes long, +# multi-paragraph commit messages, the title would swallow the list, and GitHub already +# shows the commit beside every run. The workflow's identity is the part that was missing. +run-name: "CI · ${{ github.event_name }} · ${{ github.head_ref || github.ref_name }}" # ON EVERY MERGE, and on demand. A push to `main` is what a merge looks like from Actions' side, # so every merged PR now re-runs lint, the suite, and the build-identity check against the tree diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 0e399b36..08701153 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -1,4 +1,5 @@ name: Code Quality +run-name: "Sonar/Snyk · ${{ github.event_name }} · ${{ github.head_ref || github.ref_name }}" # SonarQube (static analysis + coverage) and Snyk (dependency vulnerabilities). # diff --git a/.github/workflows/install-smoke.yml b/.github/workflows/install-smoke.yml index 28c578e3..f1e14fa1 100644 --- a/.github/workflows/install-smoke.yml +++ b/.github/workflows/install-smoke.yml @@ -1,4 +1,5 @@ name: Installer smoke +run-name: "Install smoke · ${{ github.event_name }} · ${{ github.head_ref || github.ref_name }}" # The terminal installer's acceptance evidence (#479): "works on macOS and Linux". # diff --git a/.github/workflows/installer-smoke.yml b/.github/workflows/installer-smoke.yml index b7670742..46ed5f7f 100644 --- a/.github/workflows/installer-smoke.yml +++ b/.github/workflows/installer-smoke.yml @@ -1,4 +1,5 @@ name: Installer smoke +run-name: "Installer smoke · ${{ github.event_name }} · ${{ github.head_ref || github.ref_name }}" # Compiles packaging/keel.iss on a real Windows runner against a PLACEHOLDER bundle. # diff --git a/.github/workflows/migrate.yml b/.github/workflows/migrate.yml index 8d3adf8d..f23a6fbf 100644 --- a/.github/workflows/migrate.yml +++ b/.github/workflows/migrate.yml @@ -1,4 +1,5 @@ name: Migrate database +run-name: "DB migration smoke test · ${{ github.event_name }} · ${{ github.head_ref || github.ref_name }}" # Migrating a REAL database is manual only. Merging to `main` runs the migration-chain SMOKE TEST, # and structurally cannot do more than that -- see the safety property below. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a0381140..23bc5b93 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,5 @@ name: Release +run-name: "Release · ${{ github.event_name }} · ${{ github.head_ref || github.ref_name }}" # MANUAL ONLY. Nothing about a money-moving tool should ship on a merge. # diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 838d16dc..da5e8a7b 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -3,7 +3,7 @@ # Why this workflow exists: the pre-launch gate names a gap that has been true since the # repo opened -- `SONAR_TOKEN` and `SNYK_TOKEN` were names for secrets nobody created, so # keel had no static analysis or dependency scanning that actually runs. This workflow is -# the tokenless BASELINE: it runs on GITHUB_TOKEN alone, always, on every push and weekly. +# the tokenless BASELINE: it runs on GITHUB_TOKEN alone, on every pull request and weekly. # `code-quality.yml` remains the optional enhanced tier (SonarQube + Snyk) for IF those # tokens are ever created -- its preflight skips cleanly while they are not, so the two # workflows disagree about nothing: baseline here, optional depth there. @@ -12,10 +12,11 @@ # context `test`, which ci.yml produces. A vulnerable dependency or a CodeQL finding is # information about the state of the world, not a verdict on a proposed change -- a new # CVE published against an already-pinned version must surface without waiting for a -# merge to blame. So these jobs run alongside (on push, on PRs, and weekly on the -# calendar, because CVEs are published against pinned versions whether or not anyone -# pushes) without gating merges. +# merge to blame. So these jobs run alongside (on PRs, and weekly on the calendar, because +# CVEs are published against pinned versions whether or not anyone pushes) without gating +# merges. name: security +run-name: "Security audit · ${{ github.event_name }} · ${{ github.head_ref || github.ref_name }}" # ── WHY THIS SKIPS DOCUMENTATION, AND WHY `ci.yml` DELIBERATELY DOES NOT (#644) ────────────── # This workflow reads no documentation, so a docs-only change can produce no signal from it and @@ -37,21 +38,29 @@ name: security # # SAFE HERE for two independent reasons: `main` carries no branch protection, so no check is # "required" and a skipped run cannot leave a pull request permanently unmergeable; and -# `paths-ignore` applies only to `push`/`pull_request` -- `schedule` fires regardless, so the +# `paths-ignore` applies only to `pull_request` here -- `schedule` fires regardless, so the # weekly cron below re-scans everything within seven days even on a pathological miss. # # `docs/**` sweeps up the experiment drivers and ledgers under `docs/experiments/`. Correct # here, since this workflow reads none of it; NOT correct for `ci.yml`, where # `docs/experiments/trials-ledger.jsonl` is a hash-chained artifact the research tests reason # about. +# ── WHY THERE IS NO `push` TRIGGER ─────────────────────────────────────────────────────────── +# Both jobs ran a second time on the merge commit, and the second run cost ~2.5 minutes to +# re-answer a question the pull-request run had answered four minutes earlier: `dependencies` +# audits `uv.lock`, which the merge does not change, and neither job reads anything a merge +# could combine differently. Five merges on 2026-09-01 spent ~12 minutes that way. +# +# ⚠️ THE COST, STATED RATHER THAN DISCOVERED. Code scanning reports alerts against the DEFAULT +# BRANCH, and that baseline is refreshed by analyses OF the default branch -- which now means the +# weekly cron alone. A finding introduced by a merge, or an alert a merge fixes, can therefore +# take up to seven days to appear or clear in the Security tab. Pull-request analysis is +# unaffected and still runs on every change, so nothing reaches `main` unscanned; what is slower +# is the SUMMARY of what is on `main`, not the scanning itself. If that lag ever matters, put +# `push` back rather than shortening the cron -- a daily scan pays most of the cost for a +# fraction of the freshness. on: workflow_dispatch: - push: - branches: [main] - paths-ignore: - - "docs/**" - - "**/*.md" - - "LICENSE" pull_request: paths-ignore: - "docs/**" diff --git a/tests/test_workflow_triggers.py b/tests/test_workflow_triggers.py index 00ec3921..e0fd7d25 100644 --- a/tests/test_workflow_triggers.py +++ b/tests/test_workflow_triggers.py @@ -1,4 +1,4 @@ -"""Which workflows may skip a documentation-only change, and which may never (#644). +"""Workflow-YAML invariants: which runs may skip documentation, and what a run is called. Three workflows read no documentation, so a docs-only change produces no signal from them and the run is pure cost. `ci.yml` is the opposite: keel's documentation is UNDER TEST, and @@ -28,6 +28,11 @@ _EXPECTED = {"docs/**", "**/*.md", "LICENSE"} +#: Every workflow, because the clutter this guards against is caused by the ones nobody thinks +#: about. Discovered from disk rather than listed, so a new workflow is covered on the day it +#: lands instead of the day someone remembers to add it here. +_ALL = sorted(p.name for p in _WORKFLOWS.glob("*.yml")) + def _triggers(name: str) -> dict: """The `on:` block. PyYAML resolves the bare key `on` to the boolean True (YAML 1.1), @@ -73,3 +78,34 @@ def test_ci_never_skips_a_documentation_change() -> None: f"ci.yml's `{event}` trigger has grown a paths allowlist, which skips everything " "NOT listed -- the same defect as paths-ignore, inverted (#644)." ) + + +# -- what a run is CALLED ---------------------------------------------------------------------- + + +def test_every_workflow_names_its_own_runs() -> None: + """Without `run-name`, the Actions list titles every run with the COMMIT MESSAGE. + + One push to `main` starts four workflows, and all four then appear under the same + `Merge pull request #NNN from ...` heading -- which reads as one job running four times + rather than four different checks running once. That is not a hypothetical: it is how this + pin came to exist. + + The workflow's own identity is the part the default title drops, so that is what + `run-name` has to carry. Asserted for EVERY workflow, including the ones that only run on + dispatch: a release run sitting in the same list under a commit message is the same + confusion, just rarer. + """ + assert _ALL, "no workflows found -- has .github/workflows moved?" + for name in _ALL: + doc = strict_load((_WORKFLOWS / name).read_text(encoding="utf-8"), source=name) + run_name = doc.get("run-name") + assert run_name, ( + f"{name} has no `run-name`, so its runs are titled with the commit message and are " + "indistinguishable from every other workflow the same push started" + ) + assert "github.event.head_commit.message" not in run_name, ( + f"{name}'s `run-name` interpolates the commit message. This repository writes long, " + "multi-paragraph commit messages and GitHub already shows the commit beside every " + "run -- putting it in the title restores exactly the clutter run-name removes" + )