diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index f97cd0b5..dbebe30b 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -37,7 +37,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, ubuntu-24.04-arm] + os: [ubuntu-24.04, ubuntu-24.04-arm] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1 diff --git a/.github/workflows/gate.yml b/.github/workflows/gate.yml index 2fbddf32..c3003674 100644 --- a/.github/workflows/gate.yml +++ b/.github/workflows/gate.yml @@ -24,7 +24,10 @@ on: # quotes it. `fail-fast: false` so one leg's red does not hide another's measurement. # # THE BOX LEG: the pair a fielded edge box runs (OTP 28 at a patched release, Elixir 1.20), on -# the architecture it runs on (aarch64, GitHub's arm64 runner, free for public repositories). +# the architecture it runs on (aarch64, GitHub's arm64 runner, free for public repositories), +# over the OpenSSL line the box's image pins (3.5 or later): Ubuntu 26.04's runner ships 3.5.5, +# and builds.hex.pm's OTP for it links that, where 24.04's links 3.0.13. Every runner is named +# by its image, never `-latest`, so an image moves only in a commit (governance_test.exs). # Its versions are exact (`version-type: strict`), not "the newest 28": the box pins its # runtime, so the leg does too. Every leg's gate runs the OTP floor step (tools/otp_floor.sh), # so a resolution below a line's patched floor is a red leg, not a green one on a vulnerable @@ -49,10 +52,10 @@ jobs: fail-fast: false matrix: include: - - { leg: floor, otp: "27", elixir: "1.17", os: ubuntu-latest, version-type: loose } - - { leg: pinned, otp: "28", elixir: "1.18", os: ubuntu-latest, version-type: loose } - - { leg: head, otp: "29", elixir: "1.20", os: ubuntu-latest, version-type: loose } - - { leg: box, otp: "28.5.0.8", elixir: "1.20.4", os: ubuntu-24.04-arm, version-type: strict } + - { leg: floor, otp: "27", elixir: "1.17", os: ubuntu-24.04, version-type: loose } + - { leg: pinned, otp: "28", elixir: "1.18", os: ubuntu-24.04, version-type: loose } + - { leg: head, otp: "29", elixir: "1.20", os: ubuntu-24.04, version-type: loose } + - { leg: box, otp: "28.5.0.8", elixir: "1.20.4", os: ubuntu-26.04-arm, version-type: strict } steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -93,7 +96,7 @@ jobs: # script, so its contents cannot be run as a command. pr-body: name: Gate - pull-request body - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 if: github.event_name == 'pull_request' steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -114,7 +117,7 @@ jobs: summary: name: Gate - quality gate - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 needs: [gate, pr-body] if: always() # The edited path may wait up to 20 minutes for running legs (tools/ci_legs_verdict.sh's @@ -139,7 +142,7 @@ jobs: # the REQUIRED context, and a fresh green under that name on a commit whose legs # this run never read would let a title edit stand a green beside a red (a review # lane's finding). So the legs' verdicts for this head SHA are read from the checks - # API -- the latest run of each leg name -- and all three must be success. The + # API -- the latest run of each leg name -- and all four must be success. The # read WAITS for legs still running (G-079: an edit during the synchronize run read # null conclusions, failed, and the ruleset blocked on that red beside the later # green); tools/ci_legs_verdict.sh polls to a bound, and its probe runs offline. @@ -152,7 +155,7 @@ jobs: dco: name: Gate - DCO sign-off - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: diff --git a/.github/workflows/provenance.yml b/.github/workflows/provenance.yml index 46604ba8..1de53c03 100644 --- a/.github/workflows/provenance.yml +++ b/.github/workflows/provenance.yml @@ -50,7 +50,7 @@ permissions: jobs: attest: name: Provenance - build, attest, verify - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: contents: read id-token: write diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 3883e6f7..f262b49d 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -26,7 +26,7 @@ permissions: read-all jobs: analysis: name: Scorecard - the measurement - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: # The action's documented needs: SARIF upload and the OIDC token that publish_results # signs the result with. Nothing writes to the tree. diff --git a/docs/assurance-case.md b/docs/assurance-case.md index df1e2f10..99f6f797 100644 --- a/docs/assurance-case.md +++ b/docs/assurance-case.md @@ -104,7 +104,7 @@ partial scheme. ## How the case is kept true -- The gate runs on every push on four OTP/Elixir legs (three pairs on x86_64, and the box pair, OTP 28.5.0.8 with Elixir 1.20.4, on aarch64), each held to its line's patched OTP floor; the censuses above fail when the code +- The gate runs on every push on four OTP/Elixir legs (three pairs on x86_64, and the box pair, OTP 28.5.0.8 with Elixir 1.20.4, on aarch64 over the OpenSSL 3.5 line the box's image pins; every runner named by its image, so none moves without a commit), each held to its line's patched OTP floor; the censuses above fail when the code they describe changes. - The threat model's citations and the will-not-implement page's entries are each held to the test suite by a census of their own. diff --git a/test/beam_mcp/governance_test.exs b/test/beam_mcp/governance_test.exs index ccb67f91..96c34a13 100644 --- a/test/beam_mcp/governance_test.exs +++ b/test/beam_mcp/governance_test.exs @@ -70,6 +70,27 @@ defmodule BeamMCP.GovernanceTest do end end + # A `-latest` label is moved by GitHub, not by a commit: `ubuntu-latest` becomes 26.04 in + # November 2026 (actions/runner-images#14748), which changes the OpenSSL every such leg links + # (3.0.13 to 3.5.5) with nothing in this tree's history to say so. Named images move only in + # a commit, as the pins on actions do. + test "every job names its runner image by version: no -latest label, and a matrix supplies only named images" do + for wf <- @workflows, + {line, n} <- Enum.with_index(String.split(File.read!(wf), "\n"), 1) do + refute line =~ ~r/\b[a-z]+-latest\b/, + "#{wf}:#{n} names a moving runner label: #{String.trim(line)}" + + if line =~ ~r/^\s*runs-on:/ do + assert line =~ ~r/runs-on:\s+(ubuntu-\d\d\.\d\d(-arm)?|\$\{\{ matrix\.os \}\})\s*$/, + "#{wf}:#{n} runs on something other than a named image: #{String.trim(line)}" + end + + for [image] <- Regex.scan(~r/\bubuntu-[a-z0-9.]+(?:-arm)?\b/, line) do + assert image =~ ~r/^ubuntu-\d\d\.\d\d(-arm)?$/, "#{wf}:#{n} names #{image}" + end + end + end + test "every workflow declares top-level permissions, and write permissions live on the job that needs them" do for wf <- @workflows do text = File.read!(wf)