From 0172f7e083f3709add66b564076f5644856a5c72 Mon Sep 17 00:00:00 2001 From: Lukas Wuttke Date: Wed, 19 Aug 2026 12:04:25 +0200 Subject: [PATCH] ci(2183): the R8 manifest check lived in a job nobody has to wait for MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Static analysis` (installer-tests.yaml's `static` job) is a required status check on no branch — measured against develop, staging, main and both rulesets. Four guards lived there: the R8 installer-manifest check, cross-OS facts single-sourcing, style/terminology, and PSScriptAnalyzer. A PR could change an installer sub-script, leave scripts/manifest.sha256 stale, and merge on its required checks — shipping a manifest that rejects the scripts it covers. They could also be silenced. On five runs of #752 the job's ShellCheck step ate the whole `timeout-minutes: 10` on `apt-get install shellcheck`; all four guards reported `skipped` while every required check went green. The apt-get was never needed — shellcheck is on the ubuntu-latest image. No branch-protection change and no new required context: `Source-of-truth drift` is already required on develop and main, and its header already declares itself the home for guards that must gate. The Makefile's `drift` target already ran R8 + check-facts + check-style while that job ran check-drift + env-vocabulary, so each side gated what the other did not. One list now, in the Makefile. - drift-checks.yaml runs `make drift` and holds no guard list of its own - make drift gains check-drift.sh (five guards), and runs ALL of them before failing, so one red guard no longer hides the rest — it still blocks - Lint (required) absorbs the deleted job's bash -n + shellcheck, which restated Lint's own 19-file SHELLCHECK_FILES sweep at the same severity, plus its PSScriptAnalyzer; `make lint-warnings` keeps the advisory SC2034 pass - apt-get in Lint is now conditional on shellcheck being absent - the k3s-cuda paths move to drift-checks.yaml with the check-facts guard (#616) - docs/SUPPLY_CHAIN.md and .cursor/BUGBOT.md both asserted the gate existed; corrected to name the required job Mutation-proved: changing cluster.sh without regenerating the manifest turns `make drift` red (anchor asserted: blob 87080bdb -> 0b74919c); a stale manifest plus a style violation reports both and exits non-zero. bats 1125/1125 green. Refs tracebloc/backend#2183 Co-Authored-By: Claude Opus 5 --- .cursor/BUGBOT.md | 7 +- .github/workflows/drift-checks.yaml | 60 +++++++++++--- .github/workflows/installer-tests.yaml | 105 +++++-------------------- .github/workflows/standard-checks.yml | 84 ++++++++++++++++++-- Makefile | 81 ++++++++++++++----- docs/SUPPLY_CHAIN.md | 17 +++- scripts/check-style.sh | 5 +- 7 files changed, 228 insertions(+), 131 deletions(-) diff --git a/.cursor/BUGBOT.md b/.cursor/BUGBOT.md index 3783dac0..36e22d6e 100644 --- a/.cursor/BUGBOT.md +++ b/.cursor/BUGBOT.md @@ -102,8 +102,8 @@ for *what the operator sees and can act on*, not code elegance. files should be mode 0600. - **A changed bootstrap-fetched script without a regenerated `scripts/manifest.sha256`** - (`scripts/gen-manifest.sh`). The "Installer manifest is current (supply-chain, R8)" step - in `installer-tests.yaml` fails, and a stale manifest breaks `install.sh`'s verified fetch. + (`scripts/gen-manifest.sh`). `make drift` fails in the required `Source-of-truth drift` + check, and a stale manifest breaks `install.sh`'s verified fetch. - **A `Chart.yaml` `version` bump without the matching `appVersion`** — the `app.kubernetes.io/version` label depends on it. @@ -117,7 +117,8 @@ for *what the operator sees and can act on*, not code elegance. so they can inspect a failing check's exit code instead of aborting. - **SC2034 "unused variable" in `scripts/lib/*.sh` is a known false positive** — those vars are consumed cross-file once the libs are sourced together. CI blocks at - `--severity=error` and runs `--severity=warning` advisory-only (`installer-tests.yaml:63-67`). + `--severity=error` via `make lint` in the required `Lint` check, and prints the + `--severity=warning` sweep advisory-only via `make lint-warnings`. - `scripts/manifest.sha256` and `scripts/testdata/golden/*.golden` are **generated**. The golden copy catalog is regenerated with `TB_UPDATE_GOLDEN=1 bats scripts/tests/copy-catalog.bats`, never hand-edited — review the diff --git a/.github/workflows/drift-checks.yaml b/.github/workflows/drift-checks.yaml index b8bcd839..9b804574 100644 --- a/.github/workflows/drift-checks.yaml +++ b/.github/workflows/drift-checks.yaml @@ -13,6 +13,12 @@ name: Drift checks # printing red on a workflow nobody has to wait for. A guard that cannot block # is advice (backend#1729). # +# It is therefore also the home for the R8 installer-manifest check and the +# cross-OS facts check, which moved here from installer-tests.yaml's `static` +# job (`Static analysis`, required on NO branch) on 2026-08-19. If you add a +# guard, add it to the Makefile's `drift` target -- this job runs that target +# and holds no list of its own. +# # NO `paths:` ON pull_request, deliberately. `Source-of-truth drift` is a # REQUIRED status check on develop and on main, and a required check that is # path-filtered can never report on a PR outside those paths -- GitHub leaves it @@ -31,6 +37,13 @@ on: - 'scripts/**' - 'client/**' - '.github/workflows/drift-checks.yaml' + # #616: check-facts pins the GPU node image's k3s tag against + # scripts/spec/facts.env, so a K3S_TAG edit under docker/k3s-cuda must run + # this job too. These moved here with the guard itself, out of + # installer-tests.yaml's `paths`. + - 'docker/k3s-cuda/**' + - '.github/workflows/build-k3s-cuda.yaml' + - 'Makefile' pull_request: branches: [main, develop, openshift] workflow_dispatch: @@ -48,14 +61,39 @@ jobs: uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1 with: version: v3.15.4 - - name: check-drift - run: bash scripts/tests/check-drift.sh - # One CLIENT_ENV vocabulary, four declarations (Go template, JSON Schema, - # bash, PowerShell), backend#1729 sweep 5. Lives here rather than in - # helm-ci because helm-ci's `Helm lint` is NOT a required check, so the - # guard could only advise: a PR that made the four disagree was mergeable - # with it red. It needs bash + python3 only (~2s), and drift-checks runs - # unfiltered on every PR, so it reports on the installer-only PRs that - # helm-ci's `paths:` filter would have skipped entirely. - - name: CLIENT_ENV vocabulary agreement (backend#1729) - run: bash scripts/tests/env-vocabulary-agreement.sh + # ONE step, and it does not spell the guard list out: `make drift` is the + # single declaration of "the duplicated-declaration guards", and the + # pre-push hook (`make check` -> `drift`) runs the identical set. Listing + # them here again would be a second copy to drift from -- which is exactly + # what this job exists to catch, and exactly what had happened: the + # Makefile's `drift` target carried gen-manifest/check-facts/check-style + # while this job carried check-drift, so each side gated what the other + # did not. Add a guard in the Makefile and it gates here automatically. + # + # The guards this covers, and why each is a merge gate rather than advice: + # gen-manifest.sh --check R8 supply chain. The bootstrap verifies every + # sub-script against scripts/manifest.sha256 + # before the privileged steps; a script changed + # without regenerating the manifest publishes a + # manifest that rejects the scripts it ships. + # check-facts.sh --check Cross-OS tool pins single-sourced from + # scripts/spec/facts.env (#435, #616). + # check-style.sh Terminal style + terminology (STYLE.md). + # check-drift.sh Chart <-> script name/host coupling. + # env-vocabulary-agreement One CLIENT_ENV vocabulary, four declarations + # (backend#1729 sweep 5). + # + # The first three moved here from installer-tests.yaml's `static` job on + # 2026-08-19. `Static analysis` is required on no branch, so all three were + # advice: a PR could change an installer script, leave manifest.sha256 + # stale, and merge on its required checks. Worse, they shared that job's + # `timeout-minutes: 10` with an apt-get, and on 2026-08-19 that apt-get ate + # the whole budget five times on client#752 -- all four guards reported + # `skipped` while the required checks went green. A guard that shares a + # budget with an unrelated download can be silenced by it. + # + # No `timeout-minutes` here, deliberately. A required job with no timeout + # that hangs blocks the merge; a required job that is killed mid-way can + # report guards as `skipped`. Fail closed: prefer the slow red. + - name: Duplicated-declaration guards (make drift) + run: make drift diff --git a/.github/workflows/installer-tests.yaml b/.github/workflows/installer-tests.yaml index f3904f71..7978f5f4 100644 --- a/.github/workflows/installer-tests.yaml +++ b/.github/workflows/installer-tests.yaml @@ -2,7 +2,6 @@ name: Installer tests # Validates the curl/PowerShell installer (scripts/) across the breadth of # environments a customer might actually have: -# • static — shellcheck + bash -n + PSScriptAnalyzer # • unit-bash — bats (mocked) for the bash installer # • unit-pester— Pester for the PowerShell installer, on Linux AND real Windows # • distro-prereqs — runs the REAL Linux prerequisite-install path (package @@ -16,11 +15,6 @@ on: paths: - 'scripts/**' - '.github/workflows/installer-tests.yaml' - # #616: the check-facts drift gate enforces the GPU node image's k3s pin against - # facts.env — run it when those files change too, else a K3S_TAG edit could drift - # and merge green (Bugbot). - - 'docker/k3s-cuda/**' - - '.github/workflows/build-k3s-cuda.yaml' pull_request: # `labeled` is required so adding the `e2e` label to an open PR starts the # e2e-journey job immediately — with the default types it would only fire on @@ -30,11 +24,6 @@ on: paths: - 'scripts/**' - '.github/workflows/installer-tests.yaml' - # #616: the check-facts drift gate enforces the GPU node image's k3s pin against - # facts.env — run it when those files change too, else a K3S_TAG edit could drift - # and merge green (Bugbot). - - 'docker/k3s-cuda/**' - - '.github/workflows/build-k3s-cuda.yaml' schedule: - cron: '0 3 * * 1' # Mondays 03:00 UTC — catch drift as distro base images move workflow_dispatch: @@ -50,82 +39,24 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: - static: - timeout-minutes: 10 - name: Static analysis - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: bash -n (syntax) on every shell script - run: | - # .bats files are bats DSL (@test "name" { … }), not valid bash — they are - # syntax-checked by actually running them in the unit-bash job. Parse the - # real shell scripts here. - find scripts -type f -name '*.sh' -print0 \ - | while IFS= read -r -d '' f; do bash -n "$f" || exit 1; done - echo "all shell scripts parse" - - - name: ShellCheck (libs + entrypoints) - run: | - sudo apt-get update -qq && sudo apt-get install -y -qq shellcheck - shellcheck --version | grep version - # Gate at error severity. The libs are sourced together as one program, - # so single-file shellcheck reports SC2034 "unused" false positives for - # shared vars (CURL_SECURE, ARCH_DL, colours…) that are defined in - # common.sh and consumed in other sourced files. Warnings are printed - # below for visibility but don't fail the gate. - shellcheck --severity=error --shell=bash \ - scripts/install.sh scripts/install-k8s.sh scripts/gen-manifest.sh scripts/check-facts.sh scripts/check-style.sh scripts/resolve-ingestor-digest.sh scripts/lib/*.sh \ - scripts/tests/check-drift.sh scripts/tests/distro-prereqs.sh scripts/tests/e2e-auto-upgrade.sh scripts/tests/e2e-seal-check.sh scripts/tests/e2e-full-seal.sh scripts/tests/e2e-cluster.sh scripts/tests/e2e-journey.sh scripts/tests/e2e-proxy.sh scripts/tests/lib/e2e-common.sh scripts/tests/path-persist.sh scripts/tests/chart-env-vocabulary.sh scripts/tests/env-vocabulary-agreement.sh - echo "── shellcheck warnings (advisory, non-blocking) ──" - shellcheck --severity=warning --shell=bash \ - scripts/install.sh scripts/install-k8s.sh scripts/gen-manifest.sh scripts/check-facts.sh scripts/check-style.sh scripts/resolve-ingestor-digest.sh scripts/lib/*.sh \ - scripts/tests/check-drift.sh scripts/tests/distro-prereqs.sh scripts/tests/e2e-auto-upgrade.sh scripts/tests/e2e-seal-check.sh scripts/tests/e2e-full-seal.sh scripts/tests/e2e-cluster.sh scripts/tests/e2e-journey.sh scripts/tests/e2e-proxy.sh scripts/tests/lib/e2e-common.sh scripts/tests/path-persist.sh scripts/tests/chart-env-vocabulary.sh scripts/tests/env-vocabulary-agreement.sh || true - - - name: Installer manifest is current (supply-chain, R8) - # The bootstrap verifies each sub-script against scripts/manifest.sha256 - # before running the privileged steps. If a sub-script changed but the - # manifest didn't, the release would publish a manifest that rejects the - # very scripts it ships — so fail the PR until the manifest is regenerated. - # Also cross-checks the bootstrap's FILES list against gen-manifest.sh. - run: | - chmod +x scripts/gen-manifest.sh - scripts/gen-manifest.sh --check - - - name: Installer facts are single-sourced (#435, RFC D3/D4) - # Cross-OS facts (tool version pins) live once in scripts/spec/facts.env and are - # stamped into every consumer (bash common.sh, PowerShell install-k8s.ps1). Fail - # the PR if a consumer drifted from the spec — so a pin bumped in one OS path but - # not the other (the #410 incident) can no longer ship. Fix: check-facts.sh --write. - run: | - chmod +x scripts/check-facts.sh - scripts/check-facts.sh --check - - - name: Style + terminology guard - # Enforces the terminal style system (STYLE.md): no hardcoded brand colour - # outside the tone engine (common.sh), no status/traffic-light emoji, and - # "secure environment" not "workspace" in user-facing text. Mechanical - # checks only — role/wording judgement stays with CODEOWNERS review. - run: bash scripts/check-style.sh - - - name: PSScriptAnalyzer (PowerShell installer) - shell: pwsh - run: | - Set-PSRepository PSGallery -InstallationPolicy Trusted - Install-Module PSScriptAnalyzer -Force -SkipPublisherCheck -Scope CurrentUser - # Both PowerShell entrypoints (the bootstrap install.ps1, R8 trust root, and the - # main installer install-k8s.ps1) plus the Windows e2e driver (#436) — the driver - # only RUNS on the self-hosted nested-virt runner, but lint it here on every push - # so a syntax/verb regression fails fast instead of at the nightly run. - $issues = @() - foreach ($p in 'scripts/install.ps1', 'scripts/install-k8s.ps1', 'scripts/tests/e2e-windows.ps1') { - $issues += Invoke-ScriptAnalyzer -Path $p -Severity Error,Warning - } - if ($issues) { $issues | Format-Table -AutoSize } - $errs = @($issues | Where-Object { $_.Severity -eq 'Error' }) - if ($errs.Count -gt 0) { Write-Error "PSScriptAnalyzer: $($errs.Count) error(s)"; exit 1 } - Write-Host "no PSScriptAnalyzer errors" + # The `static` job lived here until 2026-08-19. It is gone, not moved wholesale: + # its shellcheck sweep and bash -n parse loop were a verbatim restatement of + # standard-checks.yml's `Lint` (same 19 files from the Makefile's + # SHELLCHECK_FILES, same error severity), and its four guards -- R8 installer + # manifest, cross-OS facts, style/terminology, PSScriptAnalyzer -- gated + # nothing, because `Static analysis` is a required status check on no branch. + # + # Where each part went: + # bash -n + shellcheck -> standard-checks.yml `Lint` (required) via make lint + # PSScriptAnalyzer -> standard-checks.yml `Lint` (required) + # gen-manifest --check -> drift-checks.yaml `Source-of-truth drift` (required) + # check-facts --check -> drift-checks.yaml `Source-of-truth drift` (required) + # check-style.sh -> drift-checks.yaml `Source-of-truth drift` (required) + # + # The three guard moves are via the Makefile's `drift` target, so the gate and + # the pre-push hook now read one list instead of two that had already diverged. + # This job keeps only what nothing else can do: the real bats suite, Pester on + # real Windows, the 9-distro prereq matrix, PATH persistence and the e2e runs. unit-bash: timeout-minutes: 10 diff --git a/.github/workflows/standard-checks.yml b/.github/workflows/standard-checks.yml index 5e3caced..d50c21fb 100644 --- a/.github/workflows/standard-checks.yml +++ b/.github/workflows/standard-checks.yml @@ -10,10 +10,22 @@ name: Standard checks # forever at "Expected — waiting for status to be reported", blocking the merge. # # These jobs reuse the same proven steps as `installer-tests.yaml`'s deeper -# suite (which keeps the broader coverage: PSScriptAnalyzer, Pester on Windows, -# distro-prereqs, e2e). If the two ever feel redundant, fold the deep suite's -# names into these and drop the duplicates — but keep SOMETHING reporting -# `Lint` + `Unit tests` unconditionally. +# suite (which keeps the broader coverage: Pester on Windows, distro-prereqs, +# e2e). Keep SOMETHING reporting `Lint` + `Unit tests` unconditionally. +# +# That "if the two ever feel redundant, fold the deep suite's names into these +# and drop the duplicates" invitation was taken on 2026-08-19: installer-tests' +# `static` job restated this job's parse loop and its exact 19-file shellcheck +# sweep, and carried four guards that gated nothing because `Static analysis` is +# a required check on no branch. The job is gone. Its shellcheck and bash -n are +# `make lint` here, its PSScriptAnalyzer is a step below, and its three +# duplicated-declaration guards are in drift-checks.yaml's required job. +# +# STILL NOT GATED, and deliberately out of scope of that change: Pester. The +# PowerShell installer's unit suite reports as `Pester (windows-latest)` / +# `Pester (ubuntu-latest)`, neither of which is a required context, so `Unit +# tests` below gates bats only. Adding it means a required check on a +# windows-latest runner -- a cost and flake question, not a cleanup. on: push: @@ -32,11 +44,24 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - # shellcheck itself is not on the runner image, so it is installed here. - # That is environment bootstrapping, not a duplicated command. - - name: Install shellcheck + # shellcheck IS on the ubuntu-latest runner image -- the comment that used + # to sit here said it wasn't, and the unconditional `apt-get update && + # apt-get install` it justified is not harmless bootstrapping. On + # 2026-08-19 that same apt-get pair, in installer-tests.yaml's `static` + # job, hung for the job's full `timeout-minutes: 10` on five consecutive + # runs of client#752 and took four guards down with it (they reported + # `skipped`). Measured: the org's `quality / shellcheck` job runs + # shellcheck 3 s after checkout having installed nothing. + # + # So: use what the image ships, and install only if a future image drops + # it. Fail closed -- if neither path yields shellcheck, `make lint` below + # fails and this REQUIRED check goes red rather than skipping the sweep. + - name: Ensure shellcheck run: | - sudo apt-get update -qq && sudo apt-get install -y -qq shellcheck + if ! command -v shellcheck >/dev/null 2>&1; then + echo "shellcheck not on the runner image — installing" + sudo apt-get update -qq && sudo apt-get install -y -qq shellcheck + fi shellcheck --version | grep version - name: make lint @@ -60,6 +85,49 @@ jobs: # importing a backlog. run: make lint + # Advisory only, and it carries no file list of its own -- `make + # lint-warnings` reuses SHELLCHECK_FILES. Inherited from the deleted + # `static` job so the SC2034 visibility survives the move. `|| true` lives + # in the target; this step cannot fail the check. + - name: shellcheck warnings (advisory, non-blocking) + if: always() + run: make lint-warnings + + # PSScriptAnalyzer moved here from installer-tests.yaml's `static` job on + # 2026-08-19, for the same reason R8 moved to the drift job: `Static + # analysis` is a required check on no branch, so the PowerShell lint could + # only advise. `Lint` IS required on develop and on main, and a lint of + # scripts/install.ps1 -- the R8 trust root on Windows -- belongs behind a + # gate rather than beside one. + # + # NOT in `make lint`: it needs pwsh and a PSGallery fetch, which most dev + # machines have neither of, and a `make check` that fails on a missing pwsh + # is a `make check` people stop running. Skipping it locally is fine + # precisely because this job cannot skip it -- `shell: pwsh` fails the + # REQUIRED check outright if pwsh is ever absent. + # + # `if: always()` so a red `make lint` above still reports the PowerShell + # verdict in the same run. Both are mechanical and independent; making the + # author fix one to discover the other is two round trips for no reason. + - name: PSScriptAnalyzer (PowerShell installer) + if: always() + shell: pwsh + run: | + Set-PSRepository PSGallery -InstallationPolicy Trusted + Install-Module PSScriptAnalyzer -Force -SkipPublisherCheck -Scope CurrentUser + # Both PowerShell entrypoints (the bootstrap install.ps1, R8 trust root, and the + # main installer install-k8s.ps1) plus the Windows e2e driver (#436) — the driver + # only RUNS on the self-hosted nested-virt runner, but lint it here on every push + # so a syntax/verb regression fails fast instead of at the nightly run. + $issues = @() + foreach ($p in 'scripts/install.ps1', 'scripts/install-k8s.ps1', 'scripts/tests/e2e-windows.ps1') { + $issues += Invoke-ScriptAnalyzer -Path $p -Severity Error,Warning + } + if ($issues) { $issues | Format-Table -AutoSize } + $errs = @($issues | Where-Object { $_.Severity -eq 'Error' }) + if ($errs.Count -gt 0) { Write-Error "PSScriptAnalyzer: $($errs.Count) error(s)"; exit 1 } + Write-Host "no PSScriptAnalyzer errors" + unit-tests: name: Unit tests runs-on: ubuntu-latest diff --git a/Makefile b/Makefile index 3116ffb3..9953a3cc 100644 --- a/Makefile +++ b/Makefile @@ -67,8 +67,8 @@ help: # ---- check: the pre-push tier ------------------------------------ # -# Measured at ~4 s (macOS): bash -n 0.1 s, shellcheck 3.2 s, the three -# drift guards 0.2 s, helm lint 0.7 s. +# Measured at ~5 s (macOS): bash -n 0.1 s, shellcheck 3.2 s, the five +# drift guards 1.0 s, helm lint 0.7 s. # # The bats suite is deliberately NOT here. It is the repo's real unit # suite and it takes ~2 min serially on macOS — three times over the @@ -164,24 +164,69 @@ lint: @echo "all shell scripts parse" shellcheck --severity=error --shell=bash $(SHELLCHECK_FILES) -# drift: the repo's duplicated-declaration guards. The first three come from -# installer-tests.yaml's `static` job; all three are pure local file -# comparisons (~0.2 s) and all three have a --write / regenerate mode named in -# their own output. -# -# The fourth is the CLIENT_ENV vocabulary-agreement guard (backend#1729 -# sweep 5). It lives here, not in `helm-vocab`, because #715 moved it out of -# helm-ci's `Helm lint` into drift-checks.yaml's `Source-of-truth drift` job -- -# the one that is a REQUIRED check, so the guard can block rather than advise. -# helm-ci's lint job calls `make helm-lint helm-vocab`, so keeping the guard in -# `helm-vocab` would silently put it back where #715 took it from. ~2 s, bash -# and python3 only. +# lint-warnings: the advisory `--severity=warning` sweep, over the SAME file set +# (hence no second list). NOT part of `lint` and NOT a gate: the libs are sourced +# together as one program, so single-file shellcheck reports SC2034 "unused" for +# shared vars (CURL_SECURE, ARCH_DL, the colours) that common.sh defines and +# other libs consume. It is printed for visibility only. +# +# It exists as a target because installer-tests.yaml's `static` job used to run +# this sweep inline, and that job is gone; standard-checks.yml's `Lint` calls +# this so the visibility survives the move rather than being quietly dropped. +.PHONY: lint-warnings +lint-warnings: + @shellcheck --severity=warning --shell=bash $(SHELLCHECK_FILES) || true + +# drift: the repo's duplicated-declaration guards, and the ONLY declaration of +# that set. drift-checks.yaml's `Source-of-truth drift` job -- the one that is a +# REQUIRED check on develop and on main -- runs `make drift` and lists nothing +# itself, so a guard added here gates automatically and the pre-push hook and +# the merge gate cannot disagree about what "the drift guards" are. +# +# They could, and did. Until 2026-08-19 this target held gen-manifest / +# check-facts / check-style while the required job held check-drift, so each +# side gated exactly what the other did not: a stale manifest.sha256 was caught +# by the pre-push hook and NOT at the merge gate, because the job that ran R8 +# (`Static analysis`) is required on no branch. Hence one list, here. +# +# All five are pure local file comparisons and all have a --write / regenerate +# mode named in their own output. ~2 s total; check-drift adds `helm template` +# (~1 s) and is the only one that wants a tool beyond bash + python3. +# +# env-vocabulary-agreement lives here, not in `helm-vocab`, because #715 moved +# it out of helm-ci's `Helm lint` into the required drift job; helm-ci's lint job +# calls `make helm-lint helm-vocab`, so keeping it there would silently put it +# back where #715 took it from. +# `|`-separated because each guard is a multi-word command; the recipe splits on +# it. One entry per guard, and this is the only place they are written down. +DRIFT_GUARDS := scripts/gen-manifest.sh --check|scripts/check-facts.sh --check|bash scripts/check-style.sh|bash scripts/tests/check-drift.sh|bash scripts/tests/env-vocabulary-agreement.sh + +# Every guard RUNS even when an earlier one fails, and the target fails at the +# end if any did. Sequential `cmd1cmd2` recipe lines stop at the first +# failure, which means a stale manifest hides a terminology violation until you +# fix the manifest, push, and wait for CI again -- three round trips to learn two +# independent mechanical facts that took one second each to compute. +# +# This is NOT the fail-open shape. A failure here still fails the target and so +# reddens the REQUIRED `Source-of-truth drift` check; the guards are independent +# in their REPORTING, not in whether they block. (Contrast the bug this replaced: +# guards sharing one job's `timeout-minutes` reported `skipped` and blocked +# nothing.) `if [ "$$fail" -ne 0 ]` at the end is what keeps it closed -- a guard +# that cannot be reached still counts as a failure, because sh -c returns +# non-zero for a missing or non-executable script. .PHONY: drift drift: - scripts/gen-manifest.sh --check - scripts/check-facts.sh --check - bash scripts/check-style.sh - bash scripts/tests/env-vocabulary-agreement.sh + @guards='$(DRIFT_GUARDS)'; \ + fail=0; IFS='|'; for g in $$guards; do \ + printf '\n==> %s\n' "$$g"; \ + sh -c "$$g" || { fail=1; printf '!! FAILED: %s\n' "$$g"; }; \ + done; \ + unset IFS; \ + if [ "$$fail" -ne 0 ]; then \ + printf '\ndrift: one or more guards FAILED (all were run — see !! lines above)\n'; \ + exit 1; \ + fi; \ + printf '\ndrift: all guards green\n' # digest-drift: the watcher on every mutable label that points at a pinned # digest (backend#1853). NOT in `check`: it needs the network and a docker diff --git a/docs/SUPPLY_CHAIN.md b/docs/SUPPLY_CHAIN.md index 92aecbda..7397b2c5 100644 --- a/docs/SUPPLY_CHAIN.md +++ b/docs/SUPPLY_CHAIN.md @@ -96,9 +96,20 @@ job (runs on `release: published`, same trigger as the chart publish) that: 6. Attaches `install.sh`, `manifest.sha256`, `manifest.sha256.sig`, `manifest.sha256.cert` to the release. -A CI gate in `installer-tests.yaml` (`gen-manifest.sh --check`) fails any PR that -changes a sub-script without regenerating the committed manifest, so the -in-repo manifest never drifts from the scripts it covers. +`gen-manifest.sh --check` runs in the **`Source-of-truth drift`** job +(`drift-checks.yaml`, via `make drift`) and fails any PR that changes a +sub-script without regenerating the committed manifest, so the in-repo manifest +never drifts from the scripts it covers. That job is a **required status check** +on `develop` and on `main` — which is the whole reason the check lives there. + +> Until 2026-08-19 this paragraph named `installer-tests.yaml`, and the claim it +> made was false. That workflow's `Static analysis` job is a required check on no +> branch, so the R8 step could only annotate a PR, never block it — and on five +> runs of client#752 it did not even annotate: it shared a `timeout-minutes: 10` +> budget with an `apt-get`, which consumed the lot, and R8 reported `skipped` +> while every required check went green. If you move this check again, move it to +> a job whose context is in `branches/{develop,main}/protection`, and re-word +> this paragraph to name that job. ## 5. Human follow-ups required to make this fully real diff --git a/scripts/check-style.sh b/scripts/check-style.sh index 241dc765..c7c297fc 100755 --- a/scripts/check-style.sh +++ b/scripts/check-style.sh @@ -3,7 +3,10 @@ # check-style.sh — enforce the tracebloc terminal style system + terminology # on the installer scripts. See STYLE.md. # -# Runs in CI (the "Static analysis" job) and locally: bash scripts/check-style.sh +# Runs in CI as part of the REQUIRED "Source-of-truth drift" check (via +# `make drift`), and locally: bash scripts/check-style.sh +# It used to run in the "Static analysis" job, which is required on no branch — +# so until 2026-08-19 a violation here printed red and merged anyway. # Exit 0 = clean, 1 = violations found, 2 = the guard itself errored (fail-closed). # # Three mechanical checks (semantic calls — role misuse, judgement-y wording —