ci: make the glibc-floor PR leg a gating lane

The glibc-floor job builds the production binary in the pinned
ubuntu:22.04 release container and holds it to the release glibc floor
(2.35 / GLIBCXX 3.4.30). It ran report-only; it now gates like every
other lane.

- select-lanes.sh: new lane glibc-floor, part of the full set and
  selected whenever smoke-ubuntu is (a FOLLOWS rule), i.e. whenever the
  production Linux binary can change. Docs-only and tests-only changes
  do not select it.
- pr.yml: the glibc-floor job runs when its own lane is selected, and
  ci-ok needs it.
- require-all-green.sh: maps the glibc-floor job to its lane, so a
  selected glibc-floor that fails, is skipped or cancelled, or never
  runs fails ci-ok; when it is not selected it may skip.
- Contracts: the selector table and the FULL set carry the lane, and
  every selector case asserts glibc-floor is selected exactly when
  smoke-ubuntu is; the lane-gate contract adds green, failed, skipped,
  cancelled, missing and unselected cases; the lane-wiring contract
  requires the job to serve the lane, ci-ok to need it, and no
  continue-on-error on it.
- scripts/ci/README.md notes the rule.

Refs #2486

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-11-02 18:22:27 +01:00
1 parent 0fc2d9923b
commit dd4b5491c6
8 files changed
+90 -34

No files matched your search

+13 -13
View File
@@ -289,20 +289,20 @@ jobs:
with:
mode: pr
# ── glibc floor, REPORT-ONLY: builds the production binary in the SAME
# pinned ubuntu:22.04 container the standard Linux release binaries build
# in (_build.yml build-linux) and runs the composition gate with the same
# declared floor, so a change that needs a newer glibc or libstdc++ symbol
# version -- or stops compiling with that GCC 11 toolchain -- shows up at
# PR time instead of in a release run. Light on purpose: a headless build
# (the shipped composition only swaps src/ui/embedded_stub.c for the
# generated asset data) and no smoke. Deliberately NOT in ci-ok's needs
# and no lane of its own: it never blocks or passes a merge. It runs
# whenever the selection holds smoke-ubuntu, i.e. whenever the production
# binary can change. ──
# ── glibc floor (lane glibc-floor, GATING): builds the production binary in
# the SAME pinned ubuntu:22.04 container the standard Linux release
# binaries build in (_build.yml build-linux) and runs the composition gate
# with the same declared floor, so a change that needs a newer glibc or
# libstdc++ symbol version -- or stops compiling with that GCC 11
# toolchain -- fails the PR instead of a release run. Light on purpose: a
# headless build (the shipped composition only swaps
# src/ui/embedded_stub.c for the generated asset data) and no smoke. The
# selector picks the lane whenever it picks smoke-ubuntu, i.e. whenever
# the production binary can change; ci-ok needs it, and a selected
# glibc-floor that fails, skips or never runs fails ci-ok. ──
glibc-floor:
needs: [changes]
if: ${{ !cancelled() && needs.changes.result == 'success' && contains(fromJSON(needs.changes.outputs.lanes), 'smoke-ubuntu') }}
if: ${{ !cancelled() && needs.changes.result == 'success' && contains(fromJSON(needs.changes.outputs.lanes), 'glibc-floor') }}
runs-on: ubuntu-latest
container:
image: ubuntu:22.04@sha256:076e7bf82319619f28dd0aa9c02ab34b055ad37100395b4f49a2ffba04fb5e11
@@ -429,7 +429,7 @@ jobs:
# every lane the selector chose ran green: `skipped` is OK only for a lane
# nobody selected (a selected lane that skipped is a silent gate loss).
# memwaste and the test-impact shadow are report-only and not needed here.
needs: [changes, security, lint, test, pr-smoke, contracts, codeql]
needs: [changes, security, lint, test, pr-smoke, contracts, codeql, glibc-floor]
if: ${{ always() }}
runs-on: ubuntu-latest
timeout-minutes: 5
+1 -1
View File
@@ -14,7 +14,7 @@ CI and the local infrastructure — both of which the venue-parity contract
| `check-glibc-compat.sh` | Run a linux binary in debian:bullseye (glibc 2.31) — the portable binary must start on old glibc. | `_smoke.yml` portable legs |
| `generate-sbom.py` | The release SPDX SBOM (vendored versions reviewable here, diffable by vendoring PRs — was inline YAML). | `release.yml` |
| `require-all-green.sh` | The aggregate gate: fail unless every needed job succeeded or legitimately skipped (was inline YAML) — and, under lane selection, every SELECTED lane ran and succeeded (skipped/cancelled/missing = red; unselected lanes may skip). | `pr.yml ci-ok` |
| `select-lanes.sh` | Decide which PR lanes a change set runs (tier, lanes, full, reasons) from its file list — the R1 rule set, fail-safe to every lane. Pinned by `tests/test_select_lanes.sh` and the September 2026 history replay (`scripts/test-impact/replay-selector.sh`). | `pr.yml changes` |
| `select-lanes.sh` | Decide which PR lanes a change set runs (tier, lanes, full, reasons) from its file list — the R1 rule set, fail-safe to every lane; `glibc-floor` rides with `smoke-ubuntu`. Pinned by `tests/test_select_lanes.sh` and the September 2026 history replay (`scripts/test-impact/replay-selector.sh`). | `pr.yml changes` |
| `test-impact-shadow.sh` | The report-only test-impact shadow: fetch the latest released binary (checksum-verified) or take a given one, index this checkout into a throwaway cache, run `detect_changes` against the base and map it to suites (`scripts/test-impact/predict.py`). Never gates. | `pr.yml test-impact-shadow` |
| `verify-shard-union.sh` | Prove sharded test legs lost nothing: shard count agreement, indices 1..n, identical suite lists, union of slices == full list (was inline YAML). No manifests is green only when the lane selection holds no test leg. | `_test.yml` shard-completeness |
| `prepare-release-candidates.sh` | Copy one linker output into stripped/unstripped candidates, finalize signatures, composition-check them without execution, and record their hashes. | `_build.yml`, local artifact smoke |
+1
View File
@@ -70,6 +70,7 @@ if 'LANES' in os.environ:
'test / test-diag': 'diag', 'test / test-msan': 'msan',
'test / test-lsan-macos': 'lsan-macos', 'test / test-windows-guards': 'windows-guards',
'test / shard-completeness': 'shard-completeness', 'contracts': 'contracts',
'glibc-floor': 'glibc-floor',
}
matrix = ('test / test-unix (', 'test / test-windows (', 'test / test-tsan (', 'pr-smoke (',
'codeql / analyze (')
+9 -1
View File
@@ -320,7 +320,7 @@ def tier_of(cats):
# lane, by any full run: an undeterminable diff scans every language.
CODEQL_LANES = {"c-cpp": "codeql-cpp", "javascript-typescript": "codeql-js",
"python": "codeql-py", "actions": "codeql-actions"}
FULL = {"diag", "license-gate", "lint", "lint-mem", "lsan-macos",
FULL = {"diag", "glibc-floor", "license-gate", "lint", "lint-mem", "lsan-macos",
"memwaste", "msan", "pkg-wrappers", "security-static", "shard-completeness",
"smoke-mac", "smoke-ubuntu", "smoke-win", "tsan-arm", "tsan-mac", "tsan-x86",
"unix-arm64", "unix-macos-intel", "unix-macos14", "unix-x86", "windows",
@@ -330,6 +330,11 @@ LINT = {"lint", "lint-mem"}
CORE_LEGS = {"unix-x86", "unix-macos14", "windows", "shard-completeness"}
TEST_LEGS = {"unix-x86", "unix-arm64", "unix-macos14", "unix-macos-intel", "windows"}
ALWAYS = {"security-static"}
# Lanes that ride with another lane, whatever selected it: glibc-floor (pr.yml:
# the production binary built in the pinned ubuntu:22.04 release container and
# held to the release glibc floor) runs whenever smoke-ubuntu does, i.e.
# whenever the production Linux binary can change.
FOLLOWS = {"smoke-ubuntu": "glibc-floor"}
R1 = {
"T0a-docs": {"contracts"},
# The contract steps police test-infrastructure/ and lint config too.
@@ -474,6 +479,9 @@ def select(paths, expect=None):
lanes = lanes | extra
if lanes & TEST_LEGS:
lanes = lanes | {"shard-completeness"}
for leader, follower in sorted(FOLLOWS.items()):
if leader in lanes:
lanes = lanes | {follower}
for language, lane in sorted(CODEQL_LANES.items()):
hits = [p for p in paths if codeql_scope(language, p)]
if hits and lane not in lanes:
+10 -10
View File
@@ -10,7 +10,7 @@ npm-readme-runs-the-test-legs pkg/npm/README.md T1-tests false lint lint-mem sec
issue-template-is-docs .github/ISSUE_TEMPLATE/bug.yml T0a-docs false contracts security-static
local-infra test-infrastructure/run.sh T0b-nonproduct false contracts lint lint-mem security-static
lint-config scripts/memory-core-baseline.txt T0b-nonproduct false contracts lint lint-mem security-static
graph-ui-adds-guards graph-ui/src/App.tsx T0b-nonproduct false contracts lint lint-mem security-static smoke-ubuntu windows-guards codeql-js
graph-ui-adds-guards graph-ui/src/App.tsx T0b-nonproduct false contracts glibc-floor lint lint-mem security-static smoke-ubuntu windows-guards codeql-js
memwaste-tools-add-memwaste scripts/memwaste.sh T0b-nonproduct false contracts lint lint-mem memwaste security-static
non-gating-script-runs-contracts-leg scripts/benchmark-index.sh T0b-nonproduct false contracts lint lint-mem security-static shard-completeness unix-x86
test-impact-tooling scripts/test-impact/replay-selector.sh T0b-nonproduct false contracts lint lint-mem security-static shard-completeness unix-x86
@@ -19,13 +19,13 @@ test-fixture tests/fixtures/cpp_include/a.h T1-tests false lint lint-mem securit
contract-test tests/test_version_metadata_contract.sh T1-tests false lint lint-mem security-static shard-completeness unix-macos14 unix-x86 windows
fuzz-harness tests/fuzz/fuzz_cypher.c T1-tests false lint lint-mem security-static shard-completeness unix-macos14 unix-x86 windows
docs-plus-tests README.md tests/test_store.c T1-tests false lint lint-mem security-static shard-completeness unix-macos14 unix-x86 windows
lsp-language internal/cbm/lsp/go_lsp.c T2-extract false lint lint-mem security-static shard-completeness smoke-ubuntu tsan-x86 unix-macos14 unix-x86 windows codeql-cpp
lsp-core internal/cbm/lsp/type_registry.c T2-extract false lint lint-mem security-static shard-completeness smoke-ubuntu tsan-x86 unix-macos14 unix-x86 windows codeql-cpp
grammar-is-extract-not-full internal/cbm/vendored/grammars/go/parser.c T2-extract false lint lint-mem security-static shard-completeness smoke-ubuntu tsan-x86 unix-macos14 unix-x86 windows codeql-cpp
extraction internal/cbm/extract_calls.c T2-extract false lint lint-mem security-static shard-completeness smoke-ubuntu tsan-x86 unix-macos14 unix-x86 windows codeql-cpp
pipeline src/pipeline/pass_calls.c T3-mid false lint lint-mem security-static shard-completeness smoke-ubuntu tsan-x86 unix-macos14 unix-x86 windows codeql-cpp
mcp-plus-tests src/mcp/mcp.c tests/test_mcp.c T3-mid false lint lint-mem security-static shard-completeness smoke-ubuntu tsan-x86 unix-macos14 unix-x86 windows codeql-cpp
mid-plus-memwaste-tools src/store/store.c scripts/memwaste-report.py T3-mid false lint lint-mem memwaste security-static shard-completeness smoke-ubuntu tsan-x86 unix-macos14 unix-x86 windows codeql-cpp codeql-py
lsp-language internal/cbm/lsp/go_lsp.c T2-extract false glibc-floor lint lint-mem security-static shard-completeness smoke-ubuntu tsan-x86 unix-macos14 unix-x86 windows codeql-cpp
lsp-core internal/cbm/lsp/type_registry.c T2-extract false glibc-floor lint lint-mem security-static shard-completeness smoke-ubuntu tsan-x86 unix-macos14 unix-x86 windows codeql-cpp
grammar-is-extract-not-full internal/cbm/vendored/grammars/go/parser.c T2-extract false glibc-floor lint lint-mem security-static shard-completeness smoke-ubuntu tsan-x86 unix-macos14 unix-x86 windows codeql-cpp
extraction internal/cbm/extract_calls.c T2-extract false glibc-floor lint lint-mem security-static shard-completeness smoke-ubuntu tsan-x86 unix-macos14 unix-x86 windows codeql-cpp
pipeline src/pipeline/pass_calls.c T3-mid false glibc-floor lint lint-mem security-static shard-completeness smoke-ubuntu tsan-x86 unix-macos14 unix-x86 windows codeql-cpp
mcp-plus-tests src/mcp/mcp.c tests/test_mcp.c T3-mid false glibc-floor lint lint-mem security-static shard-completeness smoke-ubuntu tsan-x86 unix-macos14 unix-x86 windows codeql-cpp
mid-plus-memwaste-tools src/store/store.c scripts/memwaste-report.py T3-mid false glibc-floor lint lint-mem memwaste security-static shard-completeness smoke-ubuntu tsan-x86 unix-macos14 unix-x86 windows codeql-cpp codeql-py
daemon src/daemon/application.c T4-platform false FULL -msan -diag -memwaste -codeql-js -codeql-py -codeql-actions
main-c-is-cli src/main.c T4-platform false FULL -msan -diag -memwaste -codeql-js -codeql-py -codeql-actions
foundation-non-memory src/foundation/log.c T4-platform false FULL -msan -diag -memwaste -codeql-js -codeql-py -codeql-actions
@@ -59,7 +59,7 @@ harness-driver-forces-full scripts/test.sh T5-core true FULL
harness-scheduler-forces-full scripts/run-test-wave.py T5-core true FULL
lower-tier-plus-trigger-is-full docs/x.md src/pipeline/a.c tests/test_main.c T3-mid true FULL
empty-list-fails-safe - T0a-docs true FULL
graph-ui-python-agent graph-ui/agents/agent.py T0b-nonproduct false codeql-py contracts lint lint-mem security-static smoke-ubuntu windows-guards
vendored-js-is-not-scanned graph-ui/vendor/lib.js T0b-nonproduct false contracts lint lint-mem security-static smoke-ubuntu windows-guards
graph-ui-python-agent graph-ui/agents/agent.py T0b-nonproduct false codeql-py contracts glibc-floor lint lint-mem security-static smoke-ubuntu windows-guards
vendored-js-is-not-scanned graph-ui/vendor/lib.js T0b-nonproduct false contracts glibc-floor lint lint-mem security-static smoke-ubuntu windows-guards
vendored-only-c-scans-c vendored/sqlite3/sqlite3.c T5-core true FULL
codeql-config-is-full .github/codeql/codeql-config.yml T5-core true FULL
Can't render this file because it contains an unexpected character in line 3 and column 58.
+35 -2
View File
@@ -33,9 +33,14 @@ trap 'rm -rf "$WORK"' EXIT
failures=0
# ── ci-ok ────────────────────────────────────────────────────────────────
NEEDS_OK='{"changes":{"result":"success"},"security":{"result":"success"},"lint":{"result":"success"},"test":{"result":"success"},"pr-smoke":{"result":"skipped"},"contracts":{"result":"skipped"}}'
NEEDS_RED='{"changes":{"result":"success"},"security":{"result":"success"},"lint":{"result":"failure"},"test":{"result":"skipped"},"pr-smoke":{"result":"skipped"},"contracts":{"result":"skipped"}}'
NEEDS_OK='{"changes":{"result":"success"},"security":{"result":"success"},"lint":{"result":"success"},"test":{"result":"success"},"pr-smoke":{"result":"skipped"},"contracts":{"result":"skipped"},"glibc-floor":{"result":"skipped"}}'
NEEDS_RED='{"changes":{"result":"success"},"security":{"result":"success"},"lint":{"result":"failure"},"test":{"result":"skipped"},"pr-smoke":{"result":"skipped"},"contracts":{"result":"skipped"},"glibc-floor":{"result":"skipped"}}'
NEEDS_T3='{"changes":{"result":"success"},"security":{"result":"success"},"lint":{"result":"success"},"test":{"result":"success"},"pr-smoke":{"result":"success"},"contracts":{"result":"skipped"},"glibc-floor":{"result":"success"}}'
NEEDS_FLOOR_RED='{"changes":{"result":"success"},"security":{"result":"success"},"lint":{"result":"success"},"test":{"result":"success"},"pr-smoke":{"result":"success"},"contracts":{"result":"skipped"},"glibc-floor":{"result":"failure"}}'
T1='["lint","lint-mem","security-static","shard-completeness","unix-macos14","unix-x86","windows"]'
# A product change: the Ubuntu smoke leg and, riding with it, the gating
# release-container build (glibc-floor).
T3='["glibc-floor","lint","lint-mem","security-static","shard-completeness","smoke-ubuntu","unix-macos14","unix-x86","windows"]'
job() { # name conclusion [status]
printf '{"name":"%s","status":"%s","conclusion":%s}\n' "$1" "${3:-completed}" \
@@ -60,10 +65,16 @@ t1_jobs() { # the jobs of a green T1 run; $1 = conclusion of the x86 leg
job "test / shard-completeness" success
job "pr-smoke" skipped
job "contracts" skipped
job "glibc-floor" skipped
job "memwaste" skipped
job "test-impact-shadow" failure
job "ci-ok" null in_progress
}
t3_jobs() { # a green T3 run's jobs; $1 = glibc-floor's conclusion, or "absent"
t1_jobs success | grep -v -e '"name":"pr-smoke"' -e '"name":"glibc-floor"'
job "pr-smoke (ubuntu-latest, smoke-ubuntu)" success
[ "$1" = absent ] || job "glibc-floor" "$1"
}
gate() { # description want needs lanes jobs-file|-
local description="$1" want="$2" needs="$3" lanes="$4" jobs="$5" got
@@ -109,6 +120,28 @@ gate "an unknown lane is red" 1 "$NEEDS_OK" '["lint","security-static","no-such-
gate "a malformed selection is red" 1 "$NEEDS_OK" 'lint,unix-x86' "$WORK/t1-green"
gate "a selection without the job list is red" 1 "$NEEDS_OK" "$T1" "$WORK/does-not-exist"
# glibc-floor GATES (user decision R103, #2887): selected with smoke-ubuntu, it
# must run and succeed like any lane; unselected (docs, tests) it may skip.
t3_jobs success >"$WORK/t3-green"
t3_jobs failure >"$WORK/t3-floor-failed"
t3_jobs skipped >"$WORK/t3-floor-skipped"
t3_jobs cancelled >"$WORK/t3-floor-cancelled"
t3_jobs absent >"$WORK/t3-floor-absent"
{
job "changes" success
job "security / security-static" success
job "contracts" success
job "glibc-floor" skipped
job "ci-ok" null in_progress
} >"$WORK/docs-floor-skipped"
gate "glibc-floor selected and green" 0 "$NEEDS_T3" "$T3" "$WORK/t3-green"
gate "a selected glibc-floor that FAILED is red" 1 "$NEEDS_T3" "$T3" "$WORK/t3-floor-failed"
gate "a selected glibc-floor that was skipped is red" 1 "$NEEDS_T3" "$T3" "$WORK/t3-floor-skipped"
gate "a selected glibc-floor that was cancelled is red" 1 "$NEEDS_T3" "$T3" "$WORK/t3-floor-cancelled"
gate "a selected glibc-floor with no job at all is red" 1 "$NEEDS_T3" "$T3" "$WORK/t3-floor-absent"
gate "an unselected glibc-floor (docs-only) may skip" 0 "$NEEDS_OK" '["contracts","security-static"]' "$WORK/docs-floor-skipped"
gate "a failed glibc-floor fails the needs-level gate" 1 "$NEEDS_FLOOR_RED" - -
# Matrix jobs carry their lane in the name; the neighbouring single job
# (test-windows-guards) must never be read as the windows leg.
{
+11 -5
View File
@@ -8,8 +8,9 @@
# before lane selection. Only pr.yml asks for the PR shard profile.
# 2. pr.yml feeds the selector's lanes to every reusable workflow, runs the
# T0a/T0b contracts job, and hands ci-ok both the selection and the
# run's job list (actions: read) -- ci-ok needs `contracts` but never the
# report-only memwaste / shadow jobs, and the shadow is continue-on-error.
# run's job list (actions: read) -- ci-ok needs `contracts` and the
# gating `glibc-floor` but never the report-only memwaste / shadow jobs,
# and the shadow is continue-on-error.
# 3. every lane of scripts/ci/select-lanes.sh --list-lanes is served by a
# job: matrix lanes as a "lane":"<name>" tag in the leg data (the tag
# ci-ok reads back from the job name), single lanes by an `if:` that
@@ -102,10 +103,14 @@ require("scripts/test.sh --contracts-only" in pr.get("contracts", ""),
ci_ok = pr.get("ci-ok", "")
needs = re.search(r"needs: \[([^\]]*)\]", ci_ok)
needs = {n.strip() for n in needs.group(1).split(",")} if needs else set()
require({"changes", "security", "lint", "test", "pr-smoke", "contracts"} <= needs,
f"ci-ok must need every gating stage incl. contracts, has {sorted(needs)}")
require({"changes", "security", "lint", "test", "pr-smoke", "contracts", "glibc-floor"} <= needs,
f"ci-ok must need every gating stage incl. contracts and glibc-floor, has {sorted(needs)}")
require(not needs & {"memwaste", "test-impact-shadow"},
"ci-ok must not need the report-only memwaste / shadow jobs")
# glibc-floor gates (user decision R103, #2887): no continue-on-error may turn
# the release-container build back into a report.
require("continue-on-error" not in pr.get("glibc-floor", "continue-on-error"),
"pr.yml glibc-floor must exist and gate: no continue-on-error")
for needle, why in (("LANES: ${{ needs.changes.outputs.lanes }}", "the selection"),
("JOBS: ", "the run's job list"),
("actions: read", "actions: read to list jobs"),
@@ -164,7 +169,8 @@ SINGLE = {"lint": ("_lint.yml", "lint"), "lint-mem": ("_lint.yml", "lint-mem"),
"diag": ("_test.yml", "test-diag"), "msan": ("_test.yml", "test-msan"),
"lsan-macos": ("_test.yml", "test-lsan-macos"),
"windows-guards": ("_test.yml", "test-windows-guards"),
"memwaste": ("pr.yml", "memwaste"), "contracts": ("pr.yml", "contracts")}
"memwaste": ("pr.yml", "memwaste"), "contracts": ("pr.yml", "contracts"),
"glibc-floor": ("pr.yml", "glibc-floor")}
ALWAYS = {"security-static": ("_security.yml", "security-static"),
"shard-completeness": ("_test.yml", "shard-completeness")}
for lane in lanes:
+10 -2
View File
@@ -10,7 +10,9 @@
# - determinism: order and duplicates never change a byte of the output;
# - one decision per input channel: stdin, a file, --batch and the
# github-output form all agree;
# - a truncated file list (the pulls/files API stops at 3000) selects FULL.
# - a truncated file list (the pulls/files API stops at 3000) selects FULL;
# - glibc-floor (the gating release-container build, pr.yml) is selected
# exactly when smoke-ubuntu is, in every case.
# The history replay (every September PR push + the real failures) is
# scripts/test-impact/replay-selector.sh, a separate step.
#
@@ -48,7 +50,7 @@ failures = []
# Every lane of the full PR pipeline, spelled out here rather than read from
# the selector, so a lane silently dropped from the selector's universe fails.
FULL = {
"diag", "license-gate", "lint", "lint-mem", "lsan-macos",
"diag", "glibc-floor", "license-gate", "lint", "lint-mem", "lsan-macos",
"memwaste", "msan", "pkg-wrappers", "security-static",
"shard-completeness", "smoke-mac", "smoke-ubuntu", "smoke-win",
"tsan-arm", "tsan-mac", "tsan-x86", "unix-arm64", "unix-macos-intel",
@@ -106,6 +108,12 @@ for name, files, tier, full, lanes in cases:
failures.append(f"{name}: lanes/reasons are not sorted")
if full and not any(r.startswith("full:") for r in got["reasons"]):
failures.append(f"{name}: full=true without a 'full:' reason")
# The release-container build gates whenever the production Linux binary
# can change -- the same condition that selects the Ubuntu smoke leg.
if ("glibc-floor" in got["lanes"]) != ("smoke-ubuntu" in got["lanes"]):
failures.append(f"{name}: glibc-floor must be selected exactly when smoke-ubuntu is, "
f"got glibc-floor={'glibc-floor' in got['lanes']} "
f"smoke-ubuntu={'smoke-ubuntu' in got['lanes']}")
# 2. Determinism: shuffled, duplicated, CRLF-terminated input with blank lines
# is byte-identical to the canonical run.