From fa0bf4d0dc03da0b4ab3488119d5f106d75cfffa Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 21 Sep 2026 23:55:09 +0100 Subject: [PATCH 1/3] fix(ci): give the required status check a version-stable name Closes #38. The `test` job was named `Julia ${{ matrix.julia-version }} / ${{ matrix.os }}`, and that string is what branch ruleset 23793298 requires as a status check. A ruleset matches a required check by display name. Bumping either pin renamed the check, and a renamed check does not report at all -- it does not fail, it is simply absent, and an absent required check can never be satisfied. Every open pull request would have deadlocked until an admin bypassed the rule. The trigger was an ordinary version bump: the single most routine edit this repository gets. Both halves of the name interpolated, so the OS pin carried the same fault as the Julia pin. Measured, by parsing three hypothetical bumps through both versions of the file: matrix before after 1.12.5 / ubuntu-24.04 "Julia 1.12.5 / ubuntu-24.04" "Julia tests" 1.12.6 / ubuntu-24.04 "Julia 1.12.6 / ubuntu-24.04" "Julia tests" 1.13.0 / ubuntu-26.04 "Julia 1.13.0 / ubuntu-26.04" "Julia tests" The versions are not hidden, only moved off the identifier: they remain in `strategy.matrix` and in the `Set up Julia` step's log. `test/unit/test_install_pins.jl` gains a reflexive testset asserting that NO job name in ci.yml interpolates anything. It is written over every job rather than over the one job we require today, because a rule enforced at each door in turn is a rule the next door escapes. Verified by killing the mutant: against the unedited ci.yml the assertion fails on exactly one job of three (97 pass / 1 fail), and passes 98/98 after the rename. The job ID `test` is unchanged, so `needs: [test]` and the existing `ci["jobs"]["test"]` assertions are untouched. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01X3hgXxWm6umMgZkjYyHnnm (cherry picked from commit 8a098d24a7fc3a0e3f3867de9bb631ae04e482ce) --- .github/workflows/ci.yml | 15 ++++++++++++++- test/unit/test_install_pins.jl | 22 ++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6cecc82..41f64221 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,20 @@ concurrency: jobs: test: - name: Julia ${{ matrix.julia-version }} / ${{ matrix.os }} + # The name is a fixed string, and deliberately carries no version. + # + # A branch ruleset matches a required status check by the DISPLAY NAME of the job + # that posts it. When that name was `Julia ${{ matrix.julia-version }} / ${{ matrix.os }}` + # a pin bump renamed the check, and a renamed check does not report at all -- it + # does not fail, it is simply absent, and an absent required check can never be + # satisfied. Every pull request would have deadlocked until an admin bypassed the + # rule, triggered by nothing more alarming than editing a version number. + # + # The versions have not been hidden, only moved somewhere that does not double as + # an identifier: `strategy.matrix` below, and the `Set up Julia` step's own log. + # `test/unit/test_install_pins.jl` asserts that no job name in this file + # interpolates anything, so this cannot regress silently. + name: Julia tests runs-on: ${{ matrix.os }} # The repository's root .Rprofile sources renv/activate.R, so R started from # the checkout auto-activates renv and rewrites the library paths. renv is a diff --git a/test/unit/test_install_pins.jl b/test/unit/test_install_pins.jl index 2721a260..55894960 100644 --- a/test/unit/test_install_pins.jl +++ b/test/unit/test_install_pins.jl @@ -219,4 +219,26 @@ is_sha256(s) = s isa AbstractString && occursin(r"^[0-9a-f]{64}$", s) @test occursin("fetch_pinned \"$ref\"", runs) end end + + @testset "no job name interpolates a pin" begin + ci = YAML.load_file(CI_PATH) + + # A required status check is matched by the DISPLAY NAME of the job that posts + # it. `name: Julia ${{ matrix.julia-version }} / ${{ matrix.os }}` therefore + # renames the check on every pin bump, and a renamed check does not report at + # all -- it does not fail, it is simply absent, and a required check that never + # reports can never be satisfied. Every pull request then deadlocks until an + # admin bypasses the rule, which is the one outcome branch protection exists to + # prevent. The bump that triggers it is a one-line edit to this very file's + # subject matter, so the deadlock arrives by way of an ordinary maintenance + # change that looks safe. + # + # Asserting this over EVERY job rather than over the one job we happen to + # require today is deliberate: the rule is "a job name is a stable identifier", + # and a rule enforced at each door in turn is a rule that a new door escapes. + for (id, job) in ci["jobs"] + name = get(job, "name", id) + @test !occursin("\${{", name) + end + end end From 3f003920aa24032e4167db3c34bcae4be917fed8 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 22 Sep 2026 00:06:42 +0100 Subject: [PATCH 2/3] fix(ci): drop the 1x1 matrix so the check name is actually stable #39 renamed this job to `Julia tests` and claimed the required status check was therefore version-stable. It was not, and the claim was never measured. MEASURED on PR #39 head 776e340d, after that change had landed: Julia tests (1.12.5, ubuntu-24.04) <- what GitHub actually posted Julia tests <- what the ruleset requires GitHub appends the matrix combination to a job's posted check name whenever the `name:` does not itself reference the matrix. The old interpolating name suppressed the suffix by accident; replacing it with a static string turned the suffix on. A 1x1 matrix is still a matrix, so both pins stayed embedded in the check name and the required context `Julia tests` was never reported at all -- so `main` has been sitting in precisely the deadlock issue #38 describes, with #39 itself merged only by an admin bypass. The matrix selected exactly one combination and bought nothing, so it is removed rather than worked around: `runs-on: ubuntu-24.04` and the setup-julia `version: "1.12.5"` are literals, each keeping the reasoning that used to sit beside the matrix entry. The non-matrix `repo-hygiene` job was the control -- it has always posted its `name:` verbatim. Why the guard did not catch it: `test_install_pins.jl` asserted that no job name interpolates anything -- a property of the YAML `name:` field -- while the consumer, the ruleset, matches the rendered check-run name. The guard asked a different question than its consumer, so it passed on a broken fix. It now asserts BOTH necessary conditions, over every job in the file: the name interpolates nothing AND the job has no `strategy.matrix`. Run against the previous commit that assertion fails on `test` alone (5 pass / 1 fail), so it discriminates rather than blanket-failing. `CI installs what is pinned` made the same claims about the same two values and has been repointed at where they now live, locating the setup step by its `uses:` rather than by index. Mutating either literal fails it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01X3hgXxWm6umMgZkjYyHnnm (cherry picked from commit 6c29b53bfa2f5d99c538ecdff64ff2cd747555db) --- .github/workflows/ci.yml | 56 ++++++++++++++++--------------- test/unit/test_install_pins.jl | 61 +++++++++++++++++++++++++--------- 2 files changed, 75 insertions(+), 42 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 41f64221..c97095cf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,21 +17,33 @@ concurrency: jobs: test: - # The name is a fixed string, and deliberately carries no version. + # The name is a fixed string, it carries no version, and the job has NO matrix. + # Both are required, and the first without the second is a trap that has already + # been sprung here once. # - # A branch ruleset matches a required status check by the DISPLAY NAME of the job - # that posts it. When that name was `Julia ${{ matrix.julia-version }} / ${{ matrix.os }}` - # a pin bump renamed the check, and a renamed check does not report at all -- it - # does not fail, it is simply absent, and an absent required check can never be - # satisfied. Every pull request would have deadlocked until an admin bypassed the - # rule, triggered by nothing more alarming than editing a version number. + # A branch ruleset matches a required status check by the DISPLAY NAME GitHub posts + # for the job. A renamed check does not fail -- it is simply absent, and an absent + # required check can never be satisfied, so every pull request deadlocks until an + # admin bypasses the rule, triggered by nothing more alarming than editing a version + # number. # - # The versions have not been hidden, only moved somewhere that does not double as - # an identifier: `strategy.matrix` below, and the `Set up Julia` step's own log. - # `test/unit/test_install_pins.jl` asserts that no job name in this file - # interpolates anything, so this cannot regress silently. + # MEASURED on PR #39 (2026-09-21): this job, named exactly `Julia tests` but still + # carrying a 1x1 `strategy.matrix`, posted `Julia tests (1.12.5, ubuntu-24.04)`. + # GitHub appends the matrix combination whenever the name does not already reference + # the matrix, so a 1x1 matrix is still a matrix and the pins were still embedded in + # the check name. The matrix was therefore removed rather than merely renamed around: + # it selected exactly one combination and bought nothing. + # + # The two values it held are literals below, each keeping its own reasoning. + # `test/unit/test_install_pins.jl` asserts, for EVERY job in this file, that the name + # interpolates nothing AND that the job has no matrix -- so this cannot regress + # silently. If a matrix is ever genuinely wanted here, the required context must move + # to a matrix-free aggregator job first. name: Julia tests - runs-on: ${{ matrix.os }} + # Explicit, not ubuntu-latest: the R pin below names an apt package revision built + # for 24.04, and a runner that silently rolled to the next LTS would take that pin + # with it. `runs-on` cannot read `env`, so this is a literal. + runs-on: ubuntu-24.04 # The repository's root .Rprofile sources renv/activate.R, so R started from # the checkout auto-activates renv and rewrites the library paths. renv is a # local-development convenience; CI installs into the system library and @@ -41,19 +53,6 @@ jobs: # the environment, so those commands pass --no-init-file instead. env: RENV_CONFIG_AUTOLOADER_ENABLED: "FALSE" - strategy: - fail-fast: false - matrix: - # Pinned rather than tracking latest stable, because a pinned Manifest.toml - # resolved by an unpinned Julia is not a reproducible build. This must equal - # julia_version in Manifest.toml; it cannot be read from the pin file, since - # nothing can be read before Julia exists. test/unit/test_install_pins.jl - # fails if the three ever disagree. - julia-version: ["1.12.5"] - # Explicit, not ubuntu-latest: the R pin below names an apt package revision - # built for 24.04, and a runner that silently rolled to the next LTS would - # take that pin with it. - os: [ubuntu-24.04] steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 @@ -61,7 +60,12 @@ jobs: - name: Set up Julia uses: julia-actions/setup-julia@4c0cb0fce8556fdb04a90347310e5db8b1f98fb9 # v2.7.0 with: - version: ${{ matrix.julia-version }} + # Pinned rather than tracking latest stable, because a pinned Manifest.toml + # resolved by an unpinned Julia is not a reproducible build. This must equal + # julia_version in Manifest.toml; it cannot be read from the pin file, since + # nothing can be read before Julia exists. test/unit/test_install_pins.jl + # fails if the three ever disagree. + version: "1.12.5" - name: Cache Julia packages uses: julia-actions/cache@d10a6fd8f31b12404a54613ebad242900567f2b9 # v2.1.0 diff --git a/test/unit/test_install_pins.jl b/test/unit/test_install_pins.jl index 55894960..a81987fd 100644 --- a/test/unit/test_install_pins.jl +++ b/test/unit/test_install_pins.jl @@ -29,6 +29,15 @@ const EXPECTED_PLATFORMS = ["linux-x86_64", "linux-aarch64", "macos-x86_64", "ma is_sha256(s) = s isa AbstractString && occursin(r"^[0-9a-f]{64}$", s) +"""Return the first step of `job` whose `uses:` names `action`, or nothing.""" +function ci_step_using(job, action) + for step in get(job, "steps", []) + startswith(get(step, "uses", ""), action) && return step + end + return nothing +end + + @testset "install pins" begin @test isfile(PINS_PATH) pins = YAML.load_file(PINS_PATH) @@ -160,16 +169,24 @@ is_sha256(s) = s isa AbstractString && occursin(r"^[0-9a-f]{64}$", s) @testset "CI installs what is pinned" begin ci = YAML.load_file(CI_PATH) - matrix = ci["jobs"]["test"]["strategy"]["matrix"] + job = ci["jobs"]["test"] # Julia is the one version CI cannot read from the pin file, because nothing can # be read before Julia exists. It is therefore duplicated, and this is the test # that makes the duplication safe. - @test matrix["julia-version"] == [pins["runtimes"]["julia"]["version"]] + # + # Read from the `Set up Julia` step rather than from a matrix: the matrix was + # removed because GitHub appends a matrix combination to the posted check name + # (see "a required check name is a stable identifier" below). The step is located + # by its `uses:` rather than by index, so reordering the steps cannot make this + # assertion quietly vanish. + setup = ci_step_using(job, "julia-actions/setup-julia") + @test setup !== nothing + @test setup["with"]["version"] == pins["runtimes"]["julia"]["version"] # A floating runner would carry the R apt pin, which names a 24.04 build, off to # whatever the next LTS ships. - @test matrix["os"] == ["ubuntu-24.04"] + @test job["runs-on"] == "ubuntu-24.04" @test occursin("2404", pins["runtimes"]["r"]["apt_version"]) # Everything else CI installs must be read from the pin file at run time rather @@ -220,25 +237,37 @@ is_sha256(s) = s isa AbstractString && occursin(r"^[0-9a-f]{64}$", s) end end - @testset "no job name interpolates a pin" begin + @testset "a required check name is a stable identifier" begin ci = YAML.load_file(CI_PATH) - # A required status check is matched by the DISPLAY NAME of the job that posts - # it. `name: Julia ${{ matrix.julia-version }} / ${{ matrix.os }}` therefore - # renames the check on every pin bump, and a renamed check does not report at - # all -- it does not fail, it is simply absent, and a required check that never - # reports can never be satisfied. Every pull request then deadlocks until an - # admin bypasses the rule, which is the one outcome branch protection exists to - # prevent. The bump that triggers it is a one-line edit to this very file's - # subject matter, so the deadlock arrives by way of an ordinary maintenance - # change that looks safe. + # A branch ruleset matches a required status check by the DISPLAY NAME GitHub + # posts for the job. A renamed check does not fail -- it is simply absent, and a + # required check that never reports can never be satisfied, so every pull request + # deadlocks until an admin bypasses the rule. The bump that triggers it is a + # one-line edit to this very file's subject matter. + # + # TWO conditions are needed for GitHub to post the `name:` field verbatim, and + # the first without the second is the trap this file exists to close: + # + # 1. the name must not interpolate a pin; and + # 2. the job must not have a `strategy.matrix`. + # + # MEASURED 2026-09-21 on PR #39: a job named exactly `Julia tests` with a 1x1 + # matrix posted `Julia tests (1.12.5, ubuntu-24.04)`. GitHub appends the matrix + # combination whenever the name does not already reference the matrix, so a 1x1 + # matrix is still a matrix and the version was still embedded. A guard asserting + # only (1) PASSED on that broken fix, because it asked about the YAML field while + # the ruleset reads the rendered check name. # - # Asserting this over EVERY job rather than over the one job we happen to - # require today is deliberate: the rule is "a job name is a stable identifier", - # and a rule enforced at each door in turn is a rule that a new door escapes. + # Asserted over EVERY job rather than the one job we happen to require today: + # the rule is "a job name is a stable identifier", and a rule enforced at each + # door in turn is a rule that a new door escapes. If a matrix is ever genuinely + # needed, the required context must move to a matrix-free aggregator job and this + # assertion be re-scoped to that job -- not deleted. for (id, job) in ci["jobs"] name = get(job, "name", id) @test !occursin("\${{", name) + @test !haskey(get(job, "strategy", Dict{String,Any}()), "matrix") end end end From b6ae86c90f04ccccb25393caf0c37ee8978ba955 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 1 Oct 2026 13:23:56 +0100 Subject: [PATCH 3/3] docs(ci): name the fork PR that measured the matrix-suffixed check name Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01F6qhB21oLqSPK261cRy5R8 --- .github/workflows/ci.yml | 2 +- test/unit/test_install_pins.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c97095cf..262159e1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: # admin bypasses the rule, triggered by nothing more alarming than editing a version # number. # - # MEASURED on PR #39 (2026-09-21): this job, named exactly `Julia tests` but still + # MEASURED on hyperpolymath/MetaManifold-WebUI#39 (2026-09-21): this job, named exactly `Julia tests` but still # carrying a 1x1 `strategy.matrix`, posted `Julia tests (1.12.5, ubuntu-24.04)`. # GitHub appends the matrix combination whenever the name does not already reference # the matrix, so a 1x1 matrix is still a matrix and the pins were still embedded in diff --git a/test/unit/test_install_pins.jl b/test/unit/test_install_pins.jl index a81987fd..597c0c4c 100644 --- a/test/unit/test_install_pins.jl +++ b/test/unit/test_install_pins.jl @@ -252,7 +252,7 @@ end # 1. the name must not interpolate a pin; and # 2. the job must not have a `strategy.matrix`. # - # MEASURED 2026-09-21 on PR #39: a job named exactly `Julia tests` with a 1x1 + # MEASURED 2026-09-21 on hyperpolymath/MetaManifold-WebUI#39: a job named exactly `Julia tests` with a 1x1 # matrix posted `Julia tests (1.12.5, ubuntu-24.04)`. GitHub appends the matrix # combination whenever the name does not already reference the matrix, so a 1x1 # matrix is still a matrix and the version was still embedded. A guard asserting