Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 30 additions & 26 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,21 +101,33 @@ jobs:
echo "commit subject ok: $subject"

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
Expand All @@ -125,27 +137,19 @@ 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

- name: Set up Julia
uses: julia-actions/setup-julia@4c0cb0fce8556fdb04a90347310e5db8b1f98fb9 # v2
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
Expand Down
61 changes: 45 additions & 16 deletions test/unit/test_install_pins.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -158,16 +167,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
Expand All @@ -182,25 +199,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
Loading