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
7 changes: 4 additions & 3 deletions .cursor/BUGBOT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
66 changes: 44 additions & 22 deletions .github/workflows/drift-checks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -42,31 +55,40 @@ jobs:
drift:
name: Source-of-truth drift
runs-on: ubuntu-latest
# 20 minutes against guards that measure ~2 s. The previous revision of this
# file argued for NO timeout, on the grounds that a killed job reports its
# guards as `skipped`. That reasoning does not survive this PR's own change
# (Arturo, #755 review): `skipped` guards were harmless only while `Static
# analysis` was required nowhere. Now that `Source-of-truth drift` IS a
# required check, a timeout-kill sets it `cancelled`, which branch protection
# treats as not-success -- red and blocked, which is exactly what we want.
# So a bound is now free, and it is worth having: the hang risk here is not
# `make drift`, it is `azure/setup-helm` below fetching a binary before any
# guard runs.
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Helm
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
# The installer's telemetry vocabularies (backend#1907): four closed sets
# whose values are produced elsewhere -- the phases by install-k8s.sh's
# step_header calls, the client states by summary.sh, the script names by
# gen-manifest.sh's FILES array, the error classes by telemetry.sh's own
# classifier. A closed set that has drifted from its producer does not fail
# loudly; it reports `unknown` forever, on exactly the runs somebody added
# the new value for. It lives HERE for the same reason the CLIENT_ENV guard
# does: `Source-of-truth drift` is required, so this can block rather than
# advise. bash only, ~1s.
- name: Telemetry vocabulary agreement (backend#1907)
run: bash scripts/tests/telemetry-vocabulary-agreement.sh
# ONE step, and it names no guard. `make drift` is the single declaration of
# "the duplicated-declaration guards", and the pre-push hook (`make check`
# -> `drift`) runs the identical set. Add a guard in the Makefile and it
# gates here automatically.
#
# There is deliberately NO enumeration of the guards in this file. An
# earlier version of this comment listed five of them "for orientation",
# and within two hours it was wrong -- `telemetry-vocabulary-agreement.sh`
# arrived on develop and no prose copy followed it, while the header eight
# lines above claimed this job "holds no list of its own" (Asad + Arturo,
# #755 review). A second copy of a list is the defect this job exists to
# catch; it does not get an exemption for being a comment. `make drift`
# prints the live set and its count on every run.
#
# These guards moved here from installer-tests.yaml's `static` job on
# 2026-08-19, because `Static analysis` is required on no branch: a PR could
# change an installer script, leave manifest.sha256 stale, and merge on its
# required checks.
- name: Duplicated-declaration guards (make drift)
run: make drift
122 changes: 24 additions & 98 deletions .github/workflows/installer-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ name: Installer tests

# Validates the curl/PowerShell installer (scripts/) across the breadth of
# environments a customer might actually have:
# • static — bash -n + PSScriptAnalyzer + the repo's drift guards
# (shellcheck is NOT here: it is the `quality / shellcheck`
# required check — see the `static` job for why)
# • 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
Expand All @@ -18,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
Expand All @@ -32,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:
Expand All @@ -52,91 +39,30 @@ 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"

# There is no shellcheck step here any more, and adding one back is a
# regression. shellcheck ran in THREE places in this repo (#753): here,
# `Standard checks / Lint`, and `quality / shellcheck` from
# tracebloc/.github's code-quality.yml. This copy is the one with no
# reason to exist:
#
# • It is not a REQUIRED check on develop (this whole job isn't), so it
# could never block anything — it was advice, not a gate. Making the
# R8 step below a gate is backend#2182 / #2183, a separate concern.
# • It ENUMERATED its file set. Measured on develop 2026-08-19: the list
# that used to sit here expanded to 34 files where deriving from the
# tree yields 44 — it had drifted past eight real scripts, including
# scripts/chart-version-guard.sh, scripts/check-digest-drift.sh and
# scripts/index-invariants.sh, and #747 then had to hand-edit the list
# in three places to add one new script. A parse error planted in
# index-invariants.sh was invisible to this step.
# • shellcheck is preinstalled on ubuntu-latest, so it needs no
# `apt-get install` — the very step that hung this job to its
# 10-minute timeout twice on 2026-08-19 and reported as `cancelled`.
#
# The two survivors are complementary rather than duplicated, and both
# derive their file set: `Standard checks / Lint` runs `make lint` over
# the WHOLE TREE, and `quality / shellcheck` scans the PR DIFF. Same
# severity (error) and same SC1091 exclusion as the step deleted here, so
# no verdict changes.

- 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, and every part of it
# now sits behind a check that can actually block.
#
# #753/#754 had already deleted its shellcheck step (three copies of shellcheck,
# only this one non-gating and enumerated) and said the R8-step-as-a-gate
# question was backend#2182/#2183's to answer. This is that answer: `Static
# analysis` is a required status check on NO branch — verified against develop,
# staging, main and both rulesets — so R8, the cross-OS facts check, the style
# guard and PSScriptAnalyzer could annotate a PR and never stop one. Worse, they
# shared this job's `timeout-minutes: 10` with an apt-get that ate the whole
# budget five times on client#752; all four reported `skipped` while every
# required check went green.
#
# Where each part went:
# bash -n -> standard-checks.yml `Lint` (required), via `make parse`
# 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 go through the Makefile's `drift` target, so the gate and
# the pre-push hook 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:
# 10 -> 15. At 10 the bounded install (390 s worst case) plus ~155 s of bats
Expand Down
Loading
Loading