Skip to content

Commit 6c29b53

Browse files
hyperpolymathclaude
andcommitted
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 776e340, 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X3hgXxWm6umMgZkjYyHnnm
1 parent 8a098d2 commit 6c29b53

2 files changed

Lines changed: 75 additions & 42 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -101,21 +101,33 @@ jobs:
101101
echo "commit subject ok: $subject"
102102
103103
test:
104-
# The name is a fixed string, and deliberately carries no version.
104+
# The name is a fixed string, it carries no version, and the job has NO matrix.
105+
# Both are required, and the first without the second is a trap that has already
106+
# been sprung here once.
105107
#
106-
# A branch ruleset matches a required status check by the DISPLAY NAME of the job
107-
# that posts it. When that name was `Julia ${{ matrix.julia-version }} / ${{ matrix.os }}`
108-
# a pin bump renamed the check, and a renamed check does not report at all -- it
109-
# does not fail, it is simply absent, and an absent required check can never be
110-
# satisfied. Every pull request would have deadlocked until an admin bypassed the
111-
# rule, triggered by nothing more alarming than editing a version number.
108+
# A branch ruleset matches a required status check by the DISPLAY NAME GitHub posts
109+
# for the job. A renamed check does not fail -- it is simply absent, and an absent
110+
# required check can never be satisfied, so every pull request deadlocks until an
111+
# admin bypasses the rule, triggered by nothing more alarming than editing a version
112+
# number.
112113
#
113-
# The versions have not been hidden, only moved somewhere that does not double as
114-
# an identifier: `strategy.matrix` below, and the `Set up Julia` step's own log.
115-
# `test/unit/test_install_pins.jl` asserts that no job name in this file
116-
# interpolates anything, so this cannot regress silently.
114+
# MEASURED on PR #39 (2026-09-21): this job, named exactly `Julia tests` but still
115+
# carrying a 1x1 `strategy.matrix`, posted `Julia tests (1.12.5, ubuntu-24.04)`.
116+
# GitHub appends the matrix combination whenever the name does not already reference
117+
# the matrix, so a 1x1 matrix is still a matrix and the pins were still embedded in
118+
# the check name. The matrix was therefore removed rather than merely renamed around:
119+
# it selected exactly one combination and bought nothing.
120+
#
121+
# The two values it held are literals below, each keeping its own reasoning.
122+
# `test/unit/test_install_pins.jl` asserts, for EVERY job in this file, that the name
123+
# interpolates nothing AND that the job has no matrix -- so this cannot regress
124+
# silently. If a matrix is ever genuinely wanted here, the required context must move
125+
# to a matrix-free aggregator job first.
117126
name: Julia tests
118-
runs-on: ${{ matrix.os }}
127+
# Explicit, not ubuntu-latest: the R pin below names an apt package revision built
128+
# for 24.04, and a runner that silently rolled to the next LTS would take that pin
129+
# with it. `runs-on` cannot read `env`, so this is a literal.
130+
runs-on: ubuntu-24.04
119131
# The repository's root .Rprofile sources renv/activate.R, so R started from
120132
# the checkout auto-activates renv and rewrites the library paths. renv is a
121133
# local-development convenience; CI installs into the system library and
@@ -125,27 +137,19 @@ jobs:
125137
# the environment, so those commands pass --no-init-file instead.
126138
env:
127139
RENV_CONFIG_AUTOLOADER_ENABLED: "FALSE"
128-
strategy:
129-
fail-fast: false
130-
matrix:
131-
# Pinned rather than tracking latest stable, because a pinned Manifest.toml
132-
# resolved by an unpinned Julia is not a reproducible build. This must equal
133-
# julia_version in Manifest.toml; it cannot be read from the pin file, since
134-
# nothing can be read before Julia exists. test/unit/test_install_pins.jl
135-
# fails if the three ever disagree.
136-
julia-version: ["1.12.5"]
137-
# Explicit, not ubuntu-latest: the R pin below names an apt package revision
138-
# built for 24.04, and a runner that silently rolled to the next LTS would
139-
# take that pin with it.
140-
os: [ubuntu-24.04]
141140

142141
steps:
143142
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
144143

145144
- name: Set up Julia
146145
uses: julia-actions/setup-julia@4c0cb0fce8556fdb04a90347310e5db8b1f98fb9 # v2
147146
with:
148-
version: ${{ matrix.julia-version }}
147+
# Pinned rather than tracking latest stable, because a pinned Manifest.toml
148+
# resolved by an unpinned Julia is not a reproducible build. This must equal
149+
# julia_version in Manifest.toml; it cannot be read from the pin file, since
150+
# nothing can be read before Julia exists. test/unit/test_install_pins.jl
151+
# fails if the three ever disagree.
152+
version: "1.12.5"
149153

150154
- name: Cache Julia packages
151155
uses: julia-actions/cache@d10a6fd8f31b12404a54613ebad242900567f2b9 # v2

‎test/unit/test_install_pins.jl‎

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@ const EXPECTED_PLATFORMS = ["linux-x86_64", "linux-aarch64", "macos-x86_64", "ma
2929

3030
is_sha256(s) = s isa AbstractString && occursin(r"^[0-9a-f]{64}$", s)
3131

32+
"""Return the first step of `job` whose `uses:` names `action`, or nothing."""
33+
function ci_step_using(job, action)
34+
for step in get(job, "steps", [])
35+
startswith(get(step, "uses", ""), action) && return step
36+
end
37+
return nothing
38+
end
39+
40+
3241
@testset "install pins" begin
3342
@test isfile(PINS_PATH)
3443
pins = YAML.load_file(PINS_PATH)
@@ -158,16 +167,24 @@ is_sha256(s) = s isa AbstractString && occursin(r"^[0-9a-f]{64}$", s)
158167

159168
@testset "CI installs what is pinned" begin
160169
ci = YAML.load_file(CI_PATH)
161-
matrix = ci["jobs"]["test"]["strategy"]["matrix"]
170+
job = ci["jobs"]["test"]
162171

163172
# Julia is the one version CI cannot read from the pin file, because nothing can
164173
# be read before Julia exists. It is therefore duplicated, and this is the test
165174
# that makes the duplication safe.
166-
@test matrix["julia-version"] == [pins["runtimes"]["julia"]["version"]]
175+
#
176+
# Read from the `Set up Julia` step rather than from a matrix: the matrix was
177+
# removed because GitHub appends a matrix combination to the posted check name
178+
# (see "a required check name is a stable identifier" below). The step is located
179+
# by its `uses:` rather than by index, so reordering the steps cannot make this
180+
# assertion quietly vanish.
181+
setup = ci_step_using(job, "julia-actions/setup-julia")
182+
@test setup !== nothing
183+
@test setup["with"]["version"] == pins["runtimes"]["julia"]["version"]
167184

168185
# A floating runner would carry the R apt pin, which names a 24.04 build, off to
169186
# whatever the next LTS ships.
170-
@test matrix["os"] == ["ubuntu-24.04"]
187+
@test job["runs-on"] == "ubuntu-24.04"
171188
@test occursin("2404", pins["runtimes"]["r"]["apt_version"])
172189

173190
# Everything else CI installs must be read from the pin file at run time rather
@@ -182,25 +199,37 @@ is_sha256(s) = s isa AbstractString && occursin(r"^[0-9a-f]{64}$", s)
182199
end
183200
end
184201

185-
@testset "no job name interpolates a pin" begin
202+
@testset "a required check name is a stable identifier" begin
186203
ci = YAML.load_file(CI_PATH)
187204

188-
# A required status check is matched by the DISPLAY NAME of the job that posts
189-
# it. `name: Julia ${{ matrix.julia-version }} / ${{ matrix.os }}` therefore
190-
# renames the check on every pin bump, and a renamed check does not report at
191-
# all -- it does not fail, it is simply absent, and a required check that never
192-
# reports can never be satisfied. Every pull request then deadlocks until an
193-
# admin bypasses the rule, which is the one outcome branch protection exists to
194-
# prevent. The bump that triggers it is a one-line edit to this very file's
195-
# subject matter, so the deadlock arrives by way of an ordinary maintenance
196-
# change that looks safe.
205+
# A branch ruleset matches a required status check by the DISPLAY NAME GitHub
206+
# posts for the job. A renamed check does not fail -- it is simply absent, and a
207+
# required check that never reports can never be satisfied, so every pull request
208+
# deadlocks until an admin bypasses the rule. The bump that triggers it is a
209+
# one-line edit to this very file's subject matter.
210+
#
211+
# TWO conditions are needed for GitHub to post the `name:` field verbatim, and
212+
# the first without the second is the trap this file exists to close:
213+
#
214+
# 1. the name must not interpolate a pin; and
215+
# 2. the job must not have a `strategy.matrix`.
216+
#
217+
# MEASURED 2026-09-21 on PR #39: a job named exactly `Julia tests` with a 1x1
218+
# matrix posted `Julia tests (1.12.5, ubuntu-24.04)`. GitHub appends the matrix
219+
# combination whenever the name does not already reference the matrix, so a 1x1
220+
# matrix is still a matrix and the version was still embedded. A guard asserting
221+
# only (1) PASSED on that broken fix, because it asked about the YAML field while
222+
# the ruleset reads the rendered check name.
197223
#
198-
# Asserting this over EVERY job rather than over the one job we happen to
199-
# require today is deliberate: the rule is "a job name is a stable identifier",
200-
# and a rule enforced at each door in turn is a rule that a new door escapes.
224+
# Asserted over EVERY job rather than the one job we happen to require today:
225+
# the rule is "a job name is a stable identifier", and a rule enforced at each
226+
# door in turn is a rule that a new door escapes. If a matrix is ever genuinely
227+
# needed, the required context must move to a matrix-free aggregator job and this
228+
# assertion be re-scoped to that job -- not deleted.
201229
for (id, job) in ci["jobs"]
202230
name = get(job, "name", id)
203231
@test !occursin("\${{", name)
232+
@test !haskey(get(job, "strategy", Dict{String,Any}()), "matrix")
204233
end
205234
end
206235
end

0 commit comments

Comments
 (0)