Repository navigation
chore(deps): update helm release hcloud-cloud-controller-manager to v1.39.0 #10250
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| pull_request: | |
| branches: [main] | |
| merge_group: | |
| permissions: {} | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| changes: | |
| name: 🔍 Detect Changes | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read # checkout repository | |
| pull-requests: read # detect changed files | |
| outputs: | |
| k8s: ${{ steps.filter.outputs.k8s }} | |
| bridge_validation: ${{ steps.filter.outputs.bridge_validation }} | |
| talos: ${{ steps.filter.outputs.talos }} | |
| crossview: ${{ steps.filter.outputs.crossview }} | |
| steps: | |
| - name: 📑 Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| fetch-depth: 0 | |
| - name: ⚙️ Setup Go | |
| uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version-file: go.mod | |
| cache: false | |
| - name: 🧪 Validate the complete Go helper regression suite | |
| run: go test ./... | |
| - name: 💾 Validate storage audit regressions | |
| run: | | |
| go test ./scripts/audit-storage-retention | |
| shellcheck scripts/audit-storage-retention/capture.sh | |
| - name: 💾 Require bounded OpenBao expansion transport controls | |
| env: | |
| AUDIT_REQUIRE_NATIVE_CONTROL: '1' | |
| run: go test -race -count=1 ./scripts/expand-openbao-audit | |
| - name: 🧯 Validate private recovery field extraction | |
| run: go test -race ./scripts/validate-break-glass | |
| - name: 💾 Validate Flux replacement safety | |
| run: | | |
| go test ./scripts/validate-flux-force-safety | |
| shellcheck scripts/tests/test-flux-force-safety.sh | |
| bash scripts/tests/test-flux-force-safety.sh | |
| - name: 🧭 Validate GitHub bootstrap isolation | |
| run: | | |
| shellcheck scripts/guard-github-bootstrap-isolation.sh scripts/tests/test-github-bootstrap-isolation.sh | |
| bash scripts/tests/test-github-bootstrap-isolation.sh | |
| bash scripts/tests/test-crossplane-sync-alerter.sh | |
| - name: 🚩 Validate runtime flag definitions | |
| run: | | |
| go test ./scripts/validate-feature-flags | |
| go run ./scripts/validate-feature-flags | |
| - name: 🛡️ Validate preserved policy failure actions | |
| run: | | |
| go test ./scripts/validate-policy-actions | |
| go run ./scripts/validate-policy-actions | |
| policy_render="$(mktemp -d)" | |
| trap 'rm -rf "$policy_render"' EXIT | |
| kubectl kustomize k8s/providers/hetzner/infrastructure > "$policy_render/production.yaml" | |
| go run ./scripts/validate-policy-actions "$policy_render" | |
| - name: 🩹 Validate merge-group heal contract | |
| run: | | |
| go test ./scripts/validate-merge-group-heal | |
| go run ./scripts/validate-merge-group-heal .github/workflows/ci.yaml | |
| go run ./scripts/validate-merge-group-heal --deploy-only .github/workflows/cd.yaml | |
| shellcheck scripts/verify-prod-recovery-source.sh scripts/tests/test-prod-recovery-source.sh scripts/tests/test-prod-recovery-source-wiring.sh | |
| bash scripts/tests/test-prod-recovery-source.sh | |
| bash scripts/tests/test-prod-recovery-source-wiring.sh | |
| shellcheck scripts/merge-group-evicted.sh scripts/tests/test-merge-group-evicted.sh | |
| bash scripts/tests/test-merge-group-evicted.sh | |
| shellcheck scripts/check-flux-orphaned-objects.sh scripts/tests/test-check-flux-orphaned-objects.sh | |
| bash scripts/tests/test-check-flux-orphaned-objects.sh | |
| # A merge-group validator that ci-required-checks enforces but deploy-prod does not | |
| # need runs in parallel with the deploy, so prod can receive a revision it rejects (#4148). | |
| - name: 🚦 Validate the deploy waits for every merge-group validator | |
| run: | | |
| shellcheck scripts/guard-deploy-waits-for-merge-group-validators.sh scripts/tests/test-guard-deploy-waits-for-merge-group-validators.sh | |
| bash scripts/guard-deploy-waits-for-merge-group-validators.sh .github/workflows/ci.yaml | |
| bash scripts/tests/test-guard-deploy-waits-for-merge-group-validators.sh | |
| # Reports whether prod's published artifact is on main, from its signed | |
| # provenance (#3846). Not wired into any deploy yet; its tests also read | |
| # this workflow's k8s filter, so a filter it can no longer parse fails here. | |
| - name: 🧭 Validate prod convergence resolver | |
| run: go test ./scripts/resolve-prod-convergence | |
| - name: 🧭 Validate live required-workflow resolution | |
| run: | | |
| shellcheck scripts/required-merge-workflows.sh scripts/tests/test-required-merge-workflows.sh | |
| bash scripts/tests/test-required-merge-workflows.sh | |
| - name: 🐚 Validate Bash local initialization | |
| run: | | |
| go test -v ./scripts/guard-bash-local-initialization | |
| go run ./scripts/guard-bash-local-initialization scripts .github/scripts | |
| - name: 🛟 Validate the DR Longhorn disk preflight | |
| run: | | |
| shellcheck scripts/tests/test-dr-longhorn-disk-preflight.sh | |
| bash scripts/tests/test-dr-longhorn-disk-preflight.sh | |
| # Run on merge groups too: the manifest job below is PR-only. | |
| - name: 🔐 Validate Cilium SNI hostname coverage | |
| run: | | |
| go test ./scripts/guard-cilium-sni | |
| go run ./scripts/guard-cilium-sni k8s | |
| # validate-main.yaml runs the report after each push to main (#3848); test it on the | |
| # pull request too, so a broken report is caught before it can turn main red. | |
| - name: 🧭 Validate prod convergence report | |
| run: | | |
| shellcheck scripts/report-prod-convergence.sh | |
| shellcheck scripts/tests/test-report-prod-convergence.sh | |
| bash scripts/tests/test-report-prod-convergence.sh | |
| shellcheck scripts/redeploy-main-when-behind.sh scripts/tests/test-redeploy-main-when-behind.sh | |
| bash scripts/tests/test-redeploy-main-when-behind.sh | |
| shellcheck scripts/prod-stranded-gate.sh scripts/tests/test-prod-stranded-gate.sh | |
| bash scripts/tests/test-prod-stranded-gate.sh | |
| shellcheck scripts/tests/test-restore-stranded-prod-workflow.sh | |
| bash scripts/tests/test-restore-stranded-prod-workflow.sh | |
| - name: 🧱 Validate the reviewed KSail kernel-argument fold | |
| run: | | |
| go test ./scripts/reconcile-talos-kernel-args | |
| go run ./scripts/reconcile-talos-kernel-args --check-pins | |
| # The read-only shared-catalogue coverage proof still uses this evaluator. | |
| - name: 💒 Validate Wedding backup catalogue evaluator | |
| run: go test ./scripts/mirror-wedding-backup-catalogue | |
| - name: 💒 Validate isolated Wedding bootstrap proof | |
| run: go test ./scripts/verify-wedding-backup-bootstrap | |
| - name: 💾 Validate OpenBao snapshot fetch | |
| run: go test ./scripts/tests/vault-snapshot-fetch | |
| - name: 🔎 Validate finite runtime-log evidence | |
| run: go test ./scripts/analyze-cri-logs | |
| - name: 🛑 Validate inactive ARC staging boundaries | |
| run: go test ./scripts/tests/arc-staging | |
| - name: 🔐 Validate ARC transport proof boundaries | |
| run: | | |
| set -euo pipefail | |
| native_hubble="$(mktemp -d)" | |
| trap 'rm -rf "$native_hubble"' EXIT | |
| curl --fail --silent --show-error --location --retry 2 --max-time 60 \ | |
| https://github.com/cilium/hubble/releases/download/v1.20.2/hubble-linux-amd64.tar.gz \ | |
| --output "$native_hubble/hubble.tar.gz" | |
| printf '%s %s\n' 03f248a50cad11e728bf9383382392c57148fd6a19853ad359fb7598dae29312 "$native_hubble/hubble.tar.gz" \ | |
| | sha256sum --check | |
| tar --extract --gzip --file "$native_hubble/hubble.tar.gz" --directory "$native_hubble" | |
| export HUBBLE_TEST_BIN="$native_hubble/hubble" | |
| go test -race ./scripts/verify-arc-transport | |
| shellcheck scripts/verify-arc-transport.sh scripts/tests/test-verify-arc-transport.sh | |
| bash scripts/tests/test-verify-arc-transport.sh | |
| - name: 🛑 Validate ARC recovery metadata proof | |
| run: | | |
| go test -race ./scripts/guard-arc-recovery ./scripts/retire-arc-pool | |
| shellcheck scripts/retire-arc-pool.sh scripts/tests/test-retire-arc-pool.sh | |
| bash scripts/tests/test-retire-arc-pool.sh | |
| - name: 💾 Validate protected Longhorn client maintenance | |
| run: go test -race ./scripts/maintain-longhorn-clients | |
| - name: 🔎 Validate read-only portal retirement assessment | |
| run: go test -race ./scripts/assess-backstage-retirement | |
| # Shared Wedding backup access is retired on this proof's word, so it must | |
| # pass only on an AccessDenied for every access, after a positive control | |
| # and against an object the shared credential showed exists. These cases | |
| # keep an accepted access, a wrong bucket or a network failure from passing | |
| # as isolation. The runtime test also runs them in the pinned images and | |
| # feeds the pinned mc's own output for a refused request through the proof. | |
| - name: 💒 Validate Wedding backup denial proof | |
| run: | | |
| shellcheck scripts/verify-wedding-backup-denial.sh scripts/verify-wedding-backup-denial-pod.sh scripts/tests/test-verify-wedding-backup-denial.sh | |
| bash scripts/tests/test-verify-wedding-backup-denial.sh | |
| shellcheck scripts/tests/test-wedding-backup-denial-runtime.sh | |
| bash scripts/tests/test-wedding-backup-denial-runtime.sh | |
| # The stale shared copy of the Wedding catalogue is removed on this proof's | |
| # word, and that cannot be undone. These cases keep a missing object, | |
| # different bytes, a live writer or an unreviewed bucket from passing as | |
| # covered, and pin the pod script to commands that only read. | |
| - name: 💒 Validate Wedding shared backup coverage proof | |
| run: | | |
| shellcheck scripts/verify-wedding-shared-backup-coverage.sh scripts/verify-wedding-shared-backup-coverage-pod.sh scripts/tests/test-verify-wedding-shared-backup-coverage.sh | |
| bash scripts/tests/test-verify-wedding-shared-backup-coverage.sh | |
| - name: 💾 Validate two-stage PVC retirement | |
| # A merge-group artifact is speculative, but a PVC deletion is not: its | |
| # deletionTimestamp survives queue eviction and main cannot undo it. | |
| # Run in the shared changes job so both PR and merge-group refs compare | |
| # against their exact base even when the full manifest job is skipped. | |
| env: | |
| PVC_PRUNE_BASE_SHA: >- | |
| ${{ github.event.pull_request.base.sha || | |
| github.event.merge_group.base_sha }} | |
| run: | | |
| shellcheck scripts/tests/test-pvc-prune-safety.sh | |
| bash scripts/tests/test-pvc-prune-safety.sh | |
| - name: ✂️ Validate every Flux Kustomization prunes | |
| # Flux defaults spec.prune to false, so an omitted field silently opts a | |
| # whole tree out of deletion (#3373). The unifi tree prunes too and keeps | |
| # its live network objects at the resource layer instead; the test renders | |
| # that patch so a target that matches nothing cannot pass. | |
| run: | | |
| shellcheck scripts/tests/test-flux-kustomization-prune.sh | |
| bash scripts/tests/test-flux-kustomization-prune.sh | |
| - name: 🔐 Validate directly-invoked scripts are tracked executable | |
| # A script a workflow runs as ./scripts/<name> must be tracked 100755, because | |
| # CI clones from git and the tracked mode is what the runner gets. Caught live on | |
| # #3530, where a promotion gate was committed 100644 and died "permission denied" | |
| # BEFORE verifying anything — present, reviewed green, and inert. Nothing that runs | |
| # could see it: shellcheck was clean, bash <file> passed, and the script own suite | |
| # passed, because none of them exec the file. The class has been introduced three | |
| # times, twice via an awk-into-tmp-and-mv idiom that drops the bit while leaving the | |
| # content byte-correct. The guard derives the requirement from the invocation, so a | |
| # script that moves behind an interpreter stops needing the bit, and finding NO | |
| # direct invocation is cannot-check rather than a clean pass. | |
| # | |
| # Run in the shared changes job, unconditionally, for the same reason the PVC | |
| # check above does: the manifest job is pull_request-only, so hosting the guard | |
| # there left the merge group — the event that actually gates main — running none | |
| # of it. Unconditional also removes the path filter as a way to skip the check, | |
| # which is the failure mode the guard exists to catch, one level up. | |
| run: | | |
| shellcheck scripts/guard-tracked-exec-bit.sh | |
| shellcheck scripts/tests/test-guard-tracked-exec-bit.sh | |
| bash scripts/guard-tracked-exec-bit.sh . | |
| bash scripts/tests/test-guard-tracked-exec-bit.sh | |
| shellcheck scripts/guard-test-script-exec-bit.sh scripts/tests/test-guard-test-script-exec-bit.sh | |
| bash scripts/guard-test-script-exec-bit.sh . | |
| bash scripts/tests/test-guard-test-script-exec-bit.sh | |
| # The DR contract runs in its own dedicated job (below). This checks that | |
| # the verify block is in effect at all: it ran at spec.cluster.verify for | |
| # ~3 weeks, where KSail discards it, and every text-based check passed | |
| # throughout (#2920). It also refuses any FluxInstance kustomize patch whose | |
| # target selects no operator-generated resource, which flux-operator | |
| # would otherwise drop silently (#2996). | |
| - name: 🔐 Validate Flux verification is in effect | |
| run: | | |
| go test ./scripts/validate-flux-verify | |
| go run ./scripts/validate-flux-verify \ | |
| ksail.prod.yaml \ | |
| k8s/providers/hetzner/infrastructure/controllers/flux-instance/flux-instance.yaml | |
| # The step above covers the ROOT source. These are the other eight cosign | |
| # subjects — app OCIRepositories, the tenant RGD, the Kyverno policy and the | |
| # Talos image config — which trust the SHARED actions publish workflows and | |
| # must therefore pin a fixed revision. #2816 set that by hand in eight places; | |
| # this is what keeps it set (#2818). | |
| - name: 🔐 Validate shared publish workflows are pinned | |
| run: ./scripts/guard-shared-publish-workflow-pin.sh | |
| # The rules in talos/cluster/verify-first-party-images.yaml decide which images a node | |
| # will REFUSE once verification enforces, so a change to their order or their identities | |
| # silently changes the blast radius. This is the inventory tool's own hermetic suite — | |
| # no cluster, no registry, no credential — and it lives here rather than in the dormant | |
| # liveness workflow because a checker whose job is noticing a silent condition must not | |
| # itself regress silently (#3334, under #3101). | |
| - name: 🔐 Validate the first-party image signature inventory | |
| run: | | |
| # -x: the inventory script now sources scripts/registry-auth-lib.sh, and without it | |
| # SC1091 is reported and the run exits 1 on a script that is otherwise clean. | |
| shellcheck -x scripts/inventory-first-party-image-signatures.sh | |
| shellcheck scripts/tests/test-inventory-first-party-image-signatures.sh | |
| bash scripts/tests/test-inventory-first-party-image-signatures.sh | |
| # The credential lookup the probe uses to tell "unreadable" from "unsigned" apart. Pure | |
| # file parsing against fixtures — no registry and no credential of ours is involved. | |
| shellcheck -x scripts/registry-auth-lib.sh | |
| shellcheck -x scripts/tests/test-registry-auth-lib.sh | |
| bash scripts/tests/test-registry-auth-lib.sh | |
| # The enforcement probe's negative control only proves anything while it stays unsigned. | |
| # One added signing step would silently turn a refusal test into a no-op, so this runs | |
| # unconditionally, on every PR and merge group, like the checks above (#3336). The | |
| # verification script both the publisher and the probe call is tested here too: a | |
| # fake curl stands in for the registry, so no credential or network is involved. | |
| - name: 🔐 Validate the unsigned probe image stays unsigned | |
| run: | | |
| shellcheck scripts/guard-unsigned-probe-image-publisher.sh | |
| shellcheck scripts/tests/test-guard-unsigned-probe-image-publisher.sh | |
| shellcheck scripts/verify-image-unsigned.sh | |
| shellcheck scripts/tests/test-verify-image-unsigned.sh | |
| bash scripts/guard-unsigned-probe-image-publisher.sh . | |
| bash scripts/tests/test-guard-unsigned-probe-image-publisher.sh | |
| bash scripts/tests/test-verify-image-unsigned.sh | |
| - name: 🔐 Validate proposed Crossplane package signature checks | |
| run: | | |
| go test ./scripts/collect-first-party-packages | |
| shellcheck scripts/check-first-party-package-signatures.sh | |
| shellcheck scripts/tests/test-check-first-party-package-signatures.sh | |
| bash scripts/tests/test-check-first-party-package-signatures.sh | |
| shellcheck scripts/check-upbound-package-signatures.sh | |
| shellcheck scripts/tests/test-check-upbound-package-signatures.sh | |
| bash scripts/tests/test-check-upbound-package-signatures.sh | |
| - name: 🔐 Validate the scoped package-access experiment | |
| # Always run, including workflow-only changes. The real registry proof | |
| # is manual and uses credentials only after this code reaches main. | |
| run: go test ./scripts/prove-scoped-package-access | |
| # The guard above proves every matcher pins A revision. It cannot say WHICH, and | |
| # narrowing them to approved revisions (#2818) needs that fact for each consumer. | |
| # This runs the hermetic half of the report that computes it: the network resolver | |
| # is stubbed, so nothing here depends on another repository's release cadence. | |
| - name: 🔐 Validate the publish-revision report | |
| run: | | |
| shellcheck scripts/report-publish-workflow-signing-revisions.sh | |
| shellcheck scripts/tests/test-publish-workflow-signing-revisions.sh | |
| bash scripts/tests/test-publish-workflow-signing-revisions.sh | |
| shellcheck -x scripts/tests/test-publish-workflow-signing-revision-resolvers.sh | |
| bash scripts/tests/test-publish-workflow-signing-revision-resolvers.sh | |
| # The report above discovers its consumers with a raw FILE SCAN, while production is | |
| # what Flux RENDERS. This renders every production root (read from k8s/clusters/prod) | |
| # and fails when the two consumer sets differ — an excluded base, a patched ref or URL, | |
| # a consumer in a file the scan never opens — or when the render cannot be judged | |
| # statically (#3332). Renders only manifests. Unconditional, like the report: one half | |
| # of what it compares lives under scripts/, outside the k8s paths. | |
| - name: 🔐 Validate consumer discovery against the production render | |
| run: | | |
| shellcheck -x scripts/guard-consumer-discovery-conservation.sh | |
| shellcheck scripts/tests/test-guard-consumer-discovery-conservation.sh | |
| ./scripts/guard-consumer-discovery-conservation.sh | |
| # The report names the two revisions per consumer; the generator (#3550) turns them | |
| # into the committed approved set the matcher narrowing (#3308) is derived from. Its | |
| # cluster read and its pin read are both stubbed here, so this proves the fail-closed | |
| # shape — an unresolved consumer writes nothing — not any particular revision. | |
| - name: 🔐 Validate the approved-revisions generator | |
| run: | | |
| # -x: the generator SOURCES the report, and without it SC1091 is reported and the run exits 1. | |
| shellcheck -x scripts/generate-publish-workflow-approved-revisions.sh | |
| shellcheck -x scripts/tests/test-publish-workflow-approved-revisions.sh | |
| bash scripts/tests/test-publish-workflow-approved-revisions.sh | |
| shellcheck scripts/tests/test-canonical-publisher-regeneration.sh | |
| bash scripts/tests/test-canonical-publisher-regeneration.sh | |
| # The pin guard above proves each matcher pins a FIXED revision; this one proves each | |
| # per-consumer matcher pins the revision PAIR the committed approved set names for that | |
| # consumer, and refuses any revision outside it (#3551, AC4 of #3308). Unconditional for | |
| # the same reason as the pin guard: the subjects live in four different trees. | |
| # Enforcement rejects broad SHA patterns and revisions outside each approved pair. | |
| - name: 🔐 Validate the approved-revisions guard | |
| env: | |
| APPROVED_REVISIONS_ENFORCE: '1' | |
| run: | | |
| shellcheck -x scripts/publish-workflow-approved-revisions.lib.sh | |
| shellcheck -x scripts/guard-publish-workflow-approved-revisions.sh | |
| shellcheck -x scripts/write-publish-workflow-matchers.sh | |
| shellcheck -x scripts/tests/test-publish-workflow-approved-revisions-guard.sh | |
| shellcheck scripts/tests/test-write-publish-workflow-matchers.sh | |
| shellcheck scripts/tests/test-publish-workflow-canonical-approvals.sh | |
| bash scripts/tests/test-publish-workflow-approved-revisions-guard.sh | |
| bash scripts/tests/test-write-publish-workflow-matchers.sh | |
| bash scripts/tests/test-publish-workflow-canonical-approvals.sh | |
| - name: 🔐 Enforce approved publication signers | |
| env: | |
| APPROVED_REVISIONS_ENFORCE: '1' | |
| run: ./scripts/guard-publish-workflow-approved-revisions.sh | |
| # The scheduled regeneration (#3552, third child of #3308) runs the generator and | |
| # writer and guard above against prod and turns a moved set into ONE draft pull request. | |
| # Its safety is the WORKFLOW's shape — a fixed branch, a draft, an App-signed | |
| # commit, least-privilege token grants, and generator → writer → guard → pull request with | |
| # no continue-on-error — none of which any script it calls can enforce, so the | |
| # test below pins each with an ablation. Unconditional for the same reason as | |
| # the two steps above: the workflow lives outside the k8s paths. | |
| - name: 🔐 Validate the approved-revisions regeneration workflow | |
| run: | | |
| shellcheck scripts/tests/test-regenerate-publish-workflow-approved-revisions.sh | |
| bash scripts/tests/test-regenerate-publish-workflow-approved-revisions.sh | |
| # The release watcher (#3960) dispatches that regeneration when devantler-tech/actions | |
| # publishes a release the committed candidate does not yet name. Its test stubs `gh` and | |
| # pins that every failed read is UNKNOWN and dispatches nothing. | |
| - name: 🔐 Validate the actions release watcher | |
| run: | | |
| shellcheck scripts/watch-actions-release-candidate.sh scripts/tests/test-watch-actions-release-candidate.sh | |
| bash scripts/tests/test-watch-actions-release-candidate.sh | |
| # The authorization gate's verdict and its moved-entry diagnostics are two | |
| # commands in one step, and a real aggregate mismatch fails BOTH — so a step | |
| # that stopped at the first never ran the diagnostics on the one failure they | |
| # exist to explain (#3879). That is a property of the STEP's shape, which no | |
| # script it calls can enforce, so the test executes the step against stub | |
| # binaries and ablates each assertion. It also refuses an `if:` or a tolerated | |
| # failure on that step, which would let the gate run without deciding | |
| # anything — the Go coverage guards cannot read this compound step (#4361). | |
| # Unconditional for the same reason as the steps above: its subjects are | |
| # ci.yaml and cd.yaml themselves, which sit outside the k8s paths. | |
| - name: 🔐 Validate the authorization gate reports its diagnostics | |
| run: | | |
| shellcheck scripts/tests/test-authorization-gate-diagnostics.sh | |
| bash scripts/tests/test-authorization-gate-diagnostics.sh | |
| - name: 🔐 Validate OCI chart source readiness contracts | |
| run: go test ./scripts/tests/oci-chart-sources | |
| # Both subject guards above examine a file only when it already carries a | |
| # shared-publish-workflow subject, so an OCIRepository pointed at a devantler-tech | |
| # artifact with NO spec.verify at all is invisible to them — and refused by nothing | |
| # else. This one discovers OCIRepositories by KIND and URL, at any nesting depth, | |
| # and requires cosign with exactly one identity on every devantler-tech GHCR | |
| # artifact (#3558). Unconditional, like the guards above. | |
| - name: 🔐 Validate every devantler-tech OCIRepository verifies | |
| run: | | |
| shellcheck scripts/guard-oci-repository-verify.sh | |
| shellcheck scripts/tests/test-guard-oci-repository-verify.sh | |
| bash scripts/tests/test-guard-oci-repository-verify.sh | |
| ./scripts/guard-oci-repository-verify.sh | |
| # Dropping a framework from the Kubescape gate REMOVES findings, so the | |
| # compliance score RISES and every check stays green — a coverage | |
| # regression that looks like an improvement. That is what un-gated 59 | |
| # excepted controls for weeks (#2823). Unconditional, like the guard | |
| # above: the property it protects is not confined to the k8s paths. | |
| - name: 🔐 Validate the Kubescape gate's framework coverage | |
| run: | | |
| go test ./scripts/validate-kubescape-frameworks | |
| go run ./scripts/validate-kubescape-frameworks | |
| # .github/actionlint.yaml silences actionlint's "unexpected key" diagnostic | |
| # for concurrency.queue, which also removes its enum check. This restores it. | |
| - name: 🚦 Validate concurrency queue values | |
| run: | | |
| # BOTH extensions: the actionlint ignore covers .github/workflows/**, so | |
| # a .yml workflow left out here would have the "unexpected key" | |
| # diagnostic suppressed with nothing restoring the enum check. | |
| shopt -s nullglob | |
| workflows=(.github/workflows/*.yaml .github/workflows/*.yml) | |
| # Fail closed rather than pass vacuously if the glob ever matches nothing. | |
| if [ ${#workflows[@]} -eq 0 ]; then | |
| echo "::error::no workflow files matched; nothing was validated" | |
| exit 1 | |
| fi | |
| go test ./scripts/validate-concurrency-queue | |
| go run ./scripts/validate-concurrency-queue "${workflows[@]}" | |
| # This guard spans the HelmRelease and Renovate policy, so it must run | |
| # without routing policy-only changes into the production k8s deploy path. | |
| - name: 🔐 Validate Headlamp OIDC version gate | |
| run: | | |
| shellcheck scripts/tests/test-headlamp-oidc-version-gate.sh | |
| bash scripts/tests/test-headlamp-oidc-version-gate.sh | |
| # The renderer's Helm SDK follows the deployed controller, not Helm's own | |
| # releases. This spans the build guard and the Renovate policy, so it runs | |
| # here for the same reason as the Headlamp gate above (#4738). | |
| - name: 📌 Validate the controller Helm SDK Renovate pin | |
| run: | | |
| shellcheck scripts/tests/test-controller-helm-sdk-renovate-pin.sh | |
| bash scripts/tests/test-controller-helm-sdk-renovate-pin.sh | |
| # Renovate moves only the source commit of the vendored origin-ca-issuer CRDs, and | |
| # the bytes do not change with it, so nothing but the updater's own record can tell | |
| # a bump from a refresh (#4136). The test runs a copy of the updater against stub | |
| # binaries: no network, no Checkov. Its first case validates the tree as committed, | |
| # so this step is also what fails on a pull request that moves only the pin, and it | |
| # fails in the merge group too, where the pull-request-only manifest job never runs. | |
| # Unconditional, like the gate above: one half of its subject is the Renovate | |
| # policy, which sits outside the k8s paths. | |
| - name: 🔐 Validate the origin-ca-issuer CRD source pin | |
| run: | | |
| shellcheck scripts/tests/test-origin-ca-issuer-crd-source-pin.sh | |
| bash scripts/tests/test-origin-ca-issuer-crd-source-pin.sh | |
| # Independent of the conditional validate job whose nested-RGD steps this protects. If those | |
| # steps are removed, this unconditional job must still fail the PR and merge-group revision. | |
| - name: 🔬 Validate nested RGD workflow wiring | |
| run: | | |
| shellcheck scripts/tests/test-rgd-template-static-scan-wiring.sh | |
| bash scripts/tests/test-rgd-template-static-scan-wiring.sh | |
| - name: Validate dedicated wedding backup staging | |
| run: | | |
| node --version | |
| yq --version | |
| node --test tests/wedding-backup-staging/*.test.mjs | |
| shellcheck scripts/verify-wedding-backup-cutover.sh scripts/tests/test-verify-wedding-backup-cutover.sh | |
| bash scripts/tests/test-verify-wedding-backup-cutover.sh | |
| go test ./scripts/verify-wedding-dedicated-restore | |
| - name: 🔍 Filter paths | |
| uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3 | |
| id: filter | |
| with: | |
| filters: | | |
| k8s: | |
| - 'k8s/**' | |
| - 'ksail.yaml' | |
| - 'ksail.prod.yaml' | |
| - '.sops.yaml' | |
| - 'talos*/**' | |
| - '.github/manifest-naming.yaml' | |
| - 'scripts/validate-embedded-json/**' | |
| - 'scripts/validate-embedded-json.sh' | |
| - 'scripts/validate-policy-actions/**' | |
| - 'tests/policy-failure-actions.json' | |
| - '.github/scripts/setup-ksail.sh' | |
| - '.github/scripts/kyverno-version.sh' | |
| - 'scripts/guard-cilium-sni/**' | |
| - 'scripts/tests/test-setup-ksail.sh' | |
| - '.github/scripts/setup-talosctl.sh' | |
| - 'scripts/tests/test-setup-talosctl.sh' | |
| - '.github/scripts/setup-supply-chain-tools.sh' | |
| - 'scripts/tests/test-setup-supply-chain-tools.sh' | |
| - '.github/actions/deploy-prod/action.yml' | |
| - '.github/workflows/dr-rebuild.yaml' | |
| - '.github/workflows/validate-image-verifier-liveness.yaml' | |
| - 'scripts/tests/test-openbao-oidc-role.sh' | |
| - 'scripts/tests/test-openbao-standby-canary.sh' | |
| - 'scripts/tests/test-openbao-strategy-transition.sh' | |
| - 'scripts/tests/openbao-strategy-transition/**' | |
| - 'scripts/tests/openbao-transport-runtime/**' | |
| - 'scripts/tests/test-umami-rendered-placement.sh' | |
| - 'scripts/tests/test-reloader-rendered-placement.sh' | |
| - 'scripts/tests/test-reloader-placement-selection.sh' | |
| - 'scripts/tests/test-human-api-access-boundary.sh' | |
| - 'scripts/tests/test-world-zone-signatures.sh' | |
| - 'scripts/tests/test-world-zone-trial.sh' | |
| - 'docs/world-at-ruin-trial.md' | |
| # The boundary test asserts over these two guides, so a docs-only | |
| # change to either must trigger it or those assertions can drift. | |
| - 'docs/oidc-kubectl.md' | |
| - 'docs/talos-access.md' | |
| - 'scripts/tests/test-actual-budget-auth-route.sh' | |
| - 'scripts/tests/test-umami-sso-dashboard-route.sh' | |
| - 'scripts/tests/test-data-product-controller-trial.sh' | |
| - 'scripts/tests/test-data-product-ui-host.sh' | |
| - 'scripts/tests/test-data-product-ui-rollout.sh' | |
| - 'scripts/tests/test-data-product-runtime-digests.sh' | |
| - 'scripts/verify-data-product-ui-rollout.sh' | |
| - 'scripts/tests/test-oauth2-proxy-sso-redirects.sh' | |
| - 'scripts/tests/test-umami-tracking-probe.sh' | |
| - 'scripts/tests/fixtures/umami-curl-stub.sh' | |
| - 'scripts/verify-umami-tracking.sh' | |
| - 'scripts/tests/test-actual-budget-user-namespaces.sh' | |
| # The provider DeploymentRuntimeConfigs, this gate and its kyverno | |
| # fixture are one contract: a container-only seccompProfile is | |
| # denied by the fail-closed webhook and takes the whole provider | |
| # fleet to zero pods on the next revision bump (#3470). | |
| - 'scripts/tests/test-provider-drc-pod-seccomp.sh' | |
| - 'scripts/tests/test-tetragon-agent-priority.sh' | |
| - 'tests/validate-pod-security-provider-deployment/**' | |
| - 'scripts/tests/test-pvc-prune-safety.sh' | |
| - 'scripts/tests/test-flux-kustomization-prune.sh' | |
| - 'scripts/tests/test-headlamp-plugin-removal.sh' | |
| - 'scripts/tests/test-github-config-role-activation-parity.sh' | |
| - 'scripts/tests/test-restrict-tenant-secret-stores.sh' | |
| - 'scripts/tests/test-arc-listener-public-content.sh' | |
| - 'scripts/tests/test-arc-controller-availability.sh' | |
| - 'scripts/validate-kyverno-fixture-evaluation.sh' | |
| - 'scripts/tests/test-validate-kyverno-fixture-evaluation.sh' | |
| # Every kyverno fixture, so a change to one alone still runs kyverno test | |
| # and the fixture guard. | |
| - 'tests/**' | |
| - 'scripts/tests/test-restrict-tenant-network-policies.sh' | |
| - '.github/workflows/prove-cilium-default-deny.yaml' | |
| - '.github/scripts/fetch-cilium-deny-candidate.sh' | |
| - 'scripts/prove-cilium-default-deny.sh' | |
| - 'scripts/tests/test-fetch-cilium-deny-candidate.sh' | |
| - 'scripts/tests/test-prove-cilium-default-deny.sh' | |
| - 'scripts/tests/test-add-default-deny-shape.sh' | |
| - 'scripts/tests/test-add-baseline-context.sh' | |
| - 'scripts/tests/test-longhorn-ui-baseline-context.sh' | |
| - 'scripts/tests/test-longhorn-client-placement.sh' | |
| - 'scripts/tests/test-cilium-hubble-baseline-context.sh' | |
| - 'scripts/tests/test-guard-longhorn-ui-baseline-context.sh' | |
| - 'scripts/guard-longhorn-ui-baseline-context.sh' | |
| - 'scripts/tests/test-guard-coroot-baseline-context.sh' | |
| - 'scripts/guard-coroot-baseline-context.sh' | |
| - 'scripts/tests/test-admit-coroot-baseline-context.sh' | |
| - 'scripts/admit-coroot-baseline-context.sh' | |
| # The fixtures are the other half of that test: a PR touching only | |
| # resources.yaml, patched-resources.yaml, values.yaml or | |
| # kyverno-test.yaml would otherwise leave this filter false, so | |
| # neither kyverno test nor the shell gate would run on its own change. | |
| - 'tests/add-baseline-context/**' | |
| # The baseline-worker label policies, their fixture and the shell | |
| # gate that proves the rule actually fires are one contract. | |
| - 'tests/label-baseline-workers/**' | |
| - 'scripts/tests/test-label-baseline-workers.sh' | |
| # Exact Longhorn registrar mutation, near-miss fixtures, chart | |
| # version coupling and least-privilege background-controller RBAC. | |
| - 'tests/longhorn-registrar-prestop/**' | |
| - 'scripts/tests/test-longhorn-registrar-prestop-policy.sh' | |
| # Exact CSI-sidecar C-0211 retrofit, its near-miss fixtures and the | |
| # resourceNames-pinned background-controller grant (#3946). | |
| - 'tests/csi-sidecar-baseline-context/**' | |
| - 'scripts/tests/test-csi-sidecar-baseline-context.sh' | |
| # The same retrofit for the longhorn-csi-plugin DaemonSet, its fixtures | |
| # and the reused pinned grant (#3918). | |
| - 'tests/longhorn-csi-plugin-baseline-context/**' | |
| - 'scripts/tests/test-longhorn-csi-plugin-baseline-context.sh' | |
| - 'scripts/maintain-longhorn-clients/**' | |
| # The same retrofit for the default engine-image DaemonSet, its | |
| # fixtures and its own pinned grant (#3918). | |
| - 'tests/longhorn-engine-image-baseline-context/**' | |
| - 'scripts/tests/test-longhorn-engine-image-baseline-context.sh' | |
| # The hcloud consumers that require that label (#3287). | |
| - 'scripts/tests/test-hcloud-baseline-worker-affinity.sh' | |
| # The admission guard for every hcloud consumer, and its fixtures. | |
| - 'tests/require-baseline-workers-for-hcloud-volumes/**' | |
| - 'tests/audit-privileged-rbac/**' | |
| - 'tests/auto-vpa/**' | |
| - 'scripts/tests/test-audit-privileged-rbac.sh' | |
| # Renders the charts whose roles that policy excludes and applies the | |
| # policy to their RBAC, so a chart bump fails its own PR (#3792). | |
| - 'scripts/tests/test-chart-rendered-rbac-policy.sh' | |
| # The reviewed grants of the chart roles that policy excludes, and the | |
| # reviewed contributors to the built-in admin and edit roles (#3808). | |
| - 'tests/chart-rendered-rbac-policy/**' | |
| # The declarative fixtures are the other half of that test: a PR that | |
| # touches only resources.yaml or kyverno-test.yaml would otherwise leave | |
| # the k8s filter false, so neither kyverno test nor the shell gate runs. | |
| - 'tests/restrict-tenant-network-policies/**' | |
| # The Homepage layout and the policy's allow-list are one contract: | |
| # either side changing alone is the drift this test exists to catch. | |
| - 'scripts/tests/test-restrict-homepage-service-groups.sh' | |
| # The bookmark validator and its tests guard the same ConfigMap (#2909). | |
| - 'scripts/validate-homepage-bookmarks.sh' | |
| - 'scripts/validate-homepage-bookmarks/**' | |
| - 'scripts/tests/test-validate-homepage-bookmarks.sh' | |
| - 'scripts/tests/homepage-bookmarks/**' | |
| - 'scripts/tests/test-kyverno-umami-mutation-rbac.sh' | |
| - 'scripts/tests/test-kyverno-github-team-report-rbac.sh' | |
| - 'scripts/tests/test-tenant-route-hostname-boundary.sh' | |
| - 'scripts/tests/test-umami-provisioning-bootstrap.sh' | |
| # The snapshot manifests and this test are one contract: the | |
| # mirror may only stop depending on OWNING the snapshot while | |
| # both writers keep making it group-readable (#3202). | |
| - 'scripts/tests/test-vault-snapshot-group-readable.sh' | |
| - 'scripts/tests/test-kyverno-admission-vpa.sh' | |
| - 'scripts/tests/kyverno-admission-vpa-rules.yaml' | |
| - 'scripts/tests/test-vpa-updater-event-rbac.sh' | |
| # The image-verifier liveness checker and its test are one | |
| # contract: the checker's whole value is failing loudly, so a | |
| # change to either must re-run both. | |
| - 'scripts/validate-image-verifier-liveness.sh' | |
| - 'scripts/validate-image-verification-policy/**' | |
| - 'scripts/tests/test-validate-image-verifier-liveness.sh' | |
| # Stages exact admission and pull-time signer routes before the | |
| # compatibility-image workloads consume them. | |
| - 'scripts/tests/test-coroot-image-verifier-prerequisite.sh' | |
| # The trigger-set test for the liveness WORKFLOW (listed above), so a | |
| # PR that edits only the test still runs it. | |
| - 'scripts/tests/test-validate-image-verifier-liveness-workflow.sh' | |
| # The enforcement probe and its test are one contract for the | |
| # same reason as the pair above: the probe reports on a security | |
| # control, and BOTH of its possible mistakes are silent — a PASS | |
| # where nothing was verified, and a FAIL where the result was | |
| # merely ambiguous. The probe's workflow belongs here too: it is a | |
| # production-secret talosctl caller checked by the setup-talosctl | |
| # call-site guard, so a change to it alone must re-run that guard. | |
| - 'scripts/probe-image-signature-enforcement.sh' | |
| - 'scripts/tests/test-probe-image-signature-enforcement.sh' | |
| - '.github/workflows/probe-image-signature-enforcement.yaml' | |
| # The probe re-checks its negative control with this script just | |
| # before the pull, so a change to it re-runs the probe's suite. | |
| - 'scripts/verify-image-unsigned.sh' | |
| # The #2284 ipcache diagnostic, its test and its workflow are one | |
| # contract: the test pins that the script stays read-only and prints | |
| # no address into a public log, so a change to any of them re-runs it. | |
| - 'scripts/diagnose-cilium-ext-authz-ipcache.sh' | |
| - 'scripts/tests/test-diagnose-cilium-ext-authz-ipcache.sh' | |
| - '.github/workflows/diagnose-cilium-ext-authz-ipcache.yaml' | |
| # The #2284 cross-node datapath probe, its test and its workflow are | |
| # one contract for the same reason: the test pins what the probe may | |
| # write to production, that it cleans up, and that no address reaches | |
| # the public log. | |
| - 'scripts/probe-cilium-externalauth-crossnode.sh' | |
| - 'scripts/tests/test-probe-cilium-externalauth-crossnode.sh' | |
| - '.github/workflows/probe-cilium-externalauth-crossnode.yaml' | |
| # The #3144 admission probe, its test and its workflow are one | |
| # contract: the test pins that every write the probe sends is a | |
| # server-side dry run and that no identity reaches the public log. | |
| - 'scripts/probe-github-team-external-identity.sh' | |
| - 'scripts/tests/test-probe-github-team-external-identity.sh' | |
| - '.github/workflows/probe-github-team-external-identity.yaml' | |
| # The #3604 user-namespace map read, its test and its workflow are one | |
| # contract: the test pins that the script issues one `get` and four | |
| # fixed `exec … cat` reads, and that no node-side id reaches the log. | |
| - 'scripts/read-actual-budget-userns-maps.sh' | |
| - 'scripts/tests/test-read-actual-budget-userns-maps.sh' | |
| - '.github/workflows/read-actual-budget-userns-maps.yaml' | |
| # The #3790 datapath throttling measurement, its test and its workflow | |
| # are one contract: the test pins that the script stays read-only, its | |
| # arithmetic, and that no node or pod name reaches the public log. | |
| - 'scripts/measure-datapath-cpu-throttling.sh' | |
| - 'scripts/tests/test-measure-datapath-cpu-throttling.sh' | |
| - '.github/workflows/measure-datapath-cpu-throttling.yaml' | |
| - 'scripts/generate-kubescape-exceptions/**' | |
| - 'scripts/tests/test-kubescape-self-hosted-scan-persistence.sh' | |
| - 'scripts/tests/test-kubescape-storage-hotfix.sh' | |
| - '.github/workflows/publish-kubescape-storage-hotfix.yaml' | |
| - 'scripts/tests/test-coroot-node-agent-hotfix.sh' | |
| - '.github/workflows/publish-coroot-node-agent-hotfix.yaml' | |
| - 'k8s/bases/infrastructure/coroot/CorootNodeAgent.Dockerfile' | |
| - 'k8s/bases/infrastructure/coroot/node-agent-v1.35.8-alert-remediation.patch' | |
| - 'scripts/tests/test-kubescape-node-agent-hotfix.sh' | |
| - '.github/workflows/publish-kubescape-node-agent-hotfix.yaml' | |
| - 'scripts/kubescape-backlog-bridge/**' | |
| # The updater and its annotation helper are one fail-closed | |
| # contract for the vendored CDI/KubeVirt manifests. | |
| - 'scripts/annotate-vendored-checkov/**' | |
| - 'scripts/update-vendored-operators.sh' | |
| # The cert-approver image-pin guard reads the updater's constants, | |
| # so both halves re-run when either changes. | |
| - 'scripts/guard-cert-approver-image-pin.sh' | |
| - 'scripts/tests/test-guard-cert-approver-image-pin.sh' | |
| # The Trivy dispositions and the local CI-equivalent scan are one | |
| # contract: a path-scoped ignore is inert unless both callers load it. | |
| - '.mega-linter.yml' | |
| - '.trivyignore' | |
| - '.trivyignore.yaml' | |
| - 'scripts/megalinter-scan-counts.sh' | |
| - 'scripts/tests/test-megalinter-scan-counts-ignorefile.sh' | |
| - 'scripts/tests/test-trivyignore-vendored-operator-boundary.sh' | |
| # The vault-config KSV-0020 disposition is path-scoped but the identity it rests | |
| # on is PER CONTAINER, so the entry and its premises test move together. | |
| - 'scripts/tests/test-trivyignore-vault-config-identity-boundary.sh' | |
| # The vault-backup KSV-0020 disposition rests on the same per-container identity, | |
| # and its guard also asserts that KSV-0021 stays UNdispositioned there. | |
| - 'scripts/tests/test-trivyignore-vault-backup-identity-boundary.sh' | |
| # The first-party RBAC dispositions and the roles they describe are one | |
| # contract: the disposition is only valid while the role stays bound and | |
| # scoped the way it was reviewed. The roles themselves are already covered | |
| # by the k8s/** entry above. | |
| - 'scripts/tests/test-trivyignore-first-party-rbac-boundary.sh' | |
| # The tenant role and the test that keeps its networkpolicies grant read-only | |
| # are one contract; the role is covered by k8s/**, the test is listed here. | |
| - 'scripts/tests/test-tenant-edit-networkpolicy-write.sh' | |
| # The KSV-01010 ConfigMap dispositions are path-scoped, so the entry and its | |
| # paired control move together — a widened glob would suppress the same check | |
| # on application ConfigMaps that genuinely carry credentials. | |
| - 'scripts/tests/test-trivyignore-configmap-content-boundary.sh' | |
| # RGD sources, their extracted-template scanner, and the exact finding ratchet are | |
| # one gate. k8s/** covers source edits; policy data and these entries cover its other | |
| # inputs so a changed scanner premise cannot skip the check that consumes it. | |
| - '.trivy/data/**' | |
| - 'scripts/scan-rgd-templates.sh' | |
| - 'scripts/rgd-template-static-scan-baseline.tsv' | |
| - 'scripts/rgd-template-static-scan-remote-resources.tsv' | |
| - 'scripts/tests/test-rgd-template-static-scan.sh' | |
| - 'scripts/tests/test-rgd-template-static-scan-wiring.sh' | |
| # The version-drift guard reads its constants out of | |
| # megalinter-scan-counts.sh, so the three move together. | |
| - 'scripts/check-megalinter-version-drift.sh' | |
| - 'scripts/tests/test-check-megalinter-version-drift.sh' | |
| # Both halves, so editing the guard alone — or its test alone — | |
| # still runs the check that covers it. | |
| - 'scripts/normalize-sarif-paths.sh' | |
| - 'scripts/tests/test-normalize-sarif-paths.sh' | |
| - 'scripts/summarize-sarif-findings.sh' | |
| - 'scripts/tests/test-summarize-sarif-findings.sh' | |
| - 'go.mod' | |
| - 'go.sum' | |
| - 'scripts/tests/test-cilium-bandwidth-manager-component.sh' | |
| - 'scripts/tests/test-cilium-metadata-egress-policy.sh' | |
| # Both halves, so editing the guard alone — or its test alone — | |
| # still runs the check that covers it. | |
| - 'scripts/guard-cilium-clusterwide-default-deny.sh' | |
| - 'scripts/tests/test-guard-cilium-clusterwide-default-deny.sh' | |
| - 'scripts/tests/cnpg-network-policy/**' | |
| - 'scripts/tests/test-cilium-mutual-auth-policy.sh' | |
| - 'scripts/tests/test-cilium-mutual-auth-policy-regressions.sh' | |
| - 'scripts/tests/test-coroot-postgres-scrape-policy.sh' | |
| - 'scripts/tests/test-crossview-coroot-postgres-integration.sh' | |
| - 'scripts/tests/test-crossview-postgres-durable-storage.sh' | |
| - 'scripts/tests/test-crossview-login-alerter.sh' | |
| - 'scripts/tests/test-crossview-login-alerter-runtime.sh' | |
| - 'scripts/tests/test-coroot-api-key-contract.sh' | |
| - 'scripts/tests/test-coroot-availability-remediation.sh' | |
| - 'scripts/tests/test-backstage-retirement.sh' | |
| - 'scripts/tests/test-descheduler-availability.sh' | |
| - 'scripts/tests/test-coroot-risk-acceptance-reconciler.sh' | |
| - 'scripts/tests/test-cilium-homogeneous-devices-activation.sh' | |
| - 'scripts/tests/test-cilium-homogeneous-devices-autoscaler-gate.sh' | |
| - 'scripts/tests/test-cilium-homogeneous-devices-flux-wait.sh' | |
| - 'scripts/tests/test-cilium-ebpf-host-routing-activation.sh' | |
| - 'scripts/guard-cilium-homogeneous-device-rollout.sh' | |
| # Both halves, so editing the reporter alone — or its test alone — | |
| # still runs the check that covers it. | |
| - 'scripts/report-cilium-rollout-gate-suppression.sh' | |
| - 'scripts/tests/test-cilium-rollout-gate-suppression-signal.sh' | |
| - 'scripts/wait-for-platform-flux-revision.sh' | |
| # Both halves, so editing the reconcile wrapper alone — or its | |
| # test alone — still runs the check that covers it. | |
| - 'scripts/reconcile-flux-workloads.sh' | |
| - 'scripts/tests/test-flux-reconcile-control-plane-restart.sh' | |
| # Both halves, so editing the CoreDNS restart alone — or its test | |
| # alone — still runs the check that covers it. | |
| - 'scripts/restart-uncapped-talos-coredns.sh' | |
| - 'scripts/tests/test-restart-uncapped-talos-coredns.sh' | |
| # Both halves, so editing the root-source verify reassert alone — | |
| # or its test alone — still runs the check that covers it. | |
| - 'scripts/reassert-flux-root-verify.sh' | |
| - 'scripts/tests/test-reassert-flux-root-verify.sh' | |
| # The values it checks live under k8s/**; this entry makes an edit to | |
| # the test alone run it too. | |
| - 'scripts/tests/test-kube-system-addon-cpu-limits.sh' | |
| - 'scripts/tests/test-coroot-cost-convergence.sh' | |
| - 'scripts/tests/test-crossplane-sync-exporter.sh' | |
| - 'scripts/tests/test-crossplane-sync-alerter.sh' | |
| - 'scripts/tests/test-stranded-autoscaler-node-alerter.sh' | |
| - 'scripts/tests/test-crossplane-egress-policy.sh' | |
| - 'scripts/tests/crossplane-egress-policy-rules.yaml' | |
| - 'scripts/tests/test-coroot-alert-remediation.sh' | |
| - 'scripts/tests/test-coroot-dns-resolution.sh' | |
| - 'scripts/tests/fixtures/coroot-dns-probe/**' | |
| - 'scripts/tests/test-coroot-final-remediation.sh' | |
| - 'scripts/tests/test-cnpg-degraded-alert.sh' | |
| - 'scripts/tests/test-stranded-volume-attach-alert.sh' | |
| - 'scripts/tests/test-prune-protected-orphan-alert.sh' | |
| - 'scripts/tests/test-coroot-alert-autosuppressor.sh' | |
| - 'scripts/tests/test-coroot-application-health-reconciler.sh' | |
| - 'scripts/tests/test-coroot-helper-image-pull-policy.sh' | |
| - 'scripts/tests/test-velero-data-mover-resources.sh' | |
| - 'scripts/tests/test-velero-cancellation-release.sh' | |
| - 'scripts/tests/test-velero-node-agent-priority.sh' | |
| - 'scripts/tests/test-coroot-reservation-refresh.sh' | |
| - 'scripts/tests/test-velero-maintenance-oom-alert.sh' | |
| - 'scripts/tests/test-cronjob-failure-alert.sh' | |
| # That test also asserts the alerting runbook points at the alert's | |
| # manifest, so a runbook-only edit must still run it. | |
| - 'docs/dr/alerting.md' | |
| # The Job manifest, the Secret it imports and this test are one | |
| # contract: the credential is only out of the environment while all | |
| # three agree. | |
| - 'scripts/tests/test-minio-create-bucket-credentials.sh' | |
| # Both halves, so editing the guard alone — or its test alone — | |
| # still runs the check that covers it. | |
| - 'scripts/dr-rebuild-supersession-guard.sh' | |
| - 'scripts/tests/test-dr-rebuild-supersession-guard.sh' | |
| # Both halves, for the same reason as the guard above. | |
| - 'scripts/cp-recreate-drill-preflight.sh' | |
| - 'scripts/tests/test-cp-recreate-drill-preflight.sh' | |
| # Both halves, for the same reason as the guard above. | |
| - 'scripts/verify-published-evidence.sh' | |
| - 'scripts/tests/test-verify-published-evidence.sh' | |
| - 'scripts/tests/test-publication-subject.sh' | |
| # Both halves, for the same reason. This gate runs on the deploy path | |
| # itself, so a PR that edits it without running its test would ship a | |
| # change to the last check standing between a bad matcher and prod. | |
| - 'scripts/verify-matcher-accepts-digest.sh' | |
| - 'scripts/tests/test-verify-matcher-accepts-digest.sh' | |
| # The gate words its failures through this classifier (#3545), so an | |
| # edit to it, its own test, or the cosign output fixtures both tests | |
| # read must still run the gate's test. | |
| - 'scripts/cosign-failure-lib.sh' | |
| - 'scripts/tests/test-cosign-failure-lib.sh' | |
| - 'scripts/tests/fixtures/cosign-verify-failures/**' | |
| # The pin guard itself runs unconditionally in the validate job, so | |
| # only its TEST is selected here. Both halves are listed anyway: a PR | |
| # that edits the guard must also run the test that pins its ablations, | |
| # or a widened allow-list would ship with every check still green. | |
| - 'scripts/guard-shared-publish-workflow-pin.sh' | |
| - 'scripts/shared-publish-workflow-matchers.tsv' | |
| - 'scripts/tests/test-shared-publish-workflow-pin-guard.sh' | |
| # Both halves, for the same reason as the pin guard above: the guard step itself | |
| # runs unconditionally, and listing the pair here is what keeps a guard edit | |
| # inside the k8s-gated validation matrix alongside the manifests it judges. | |
| - 'scripts/guard-publish-workflow-approved-revisions.sh' | |
| - 'scripts/publish-workflow-approved-revisions.lib.sh' | |
| - 'scripts/write-publish-workflow-matchers.sh' | |
| - 'scripts/tests/test-write-publish-workflow-matchers.sh' | |
| - 'scripts/tests/test-publish-workflow-approved-revisions-guard.sh' | |
| # The fixed canonical-publisher approvals the same library reads, and their test (#4502). | |
| - 'scripts/publish-workflow-canonical-approvals.tsv' | |
| - 'scripts/tests/test-publish-workflow-canonical-approvals.sh' | |
| # The generator that writes the set the guard above reads, and its test — both | |
| # halves, so editing either alone still runs the fail-closed proof (an unresolved | |
| # consumer writes nothing) that step 🔐 Validate the approved-revisions generator pins. | |
| - 'scripts/generate-publish-workflow-approved-revisions.sh' | |
| - 'scripts/tests/test-publish-workflow-approved-revisions.sh' | |
| - 'scripts/tests/test-canonical-publisher-regeneration.sh' | |
| - 'scripts/publish-workflow-approved-revisions.identities.tsv' | |
| # The scheduled regeneration workflow and its contract test — both halves, so a | |
| # PR that edits only the workflow (a widened token grant, an interpolated branch, | |
| # a continue-on-error) still runs the ablated test that refuses each of those. | |
| - '.github/workflows/regenerate-publish-workflow-approved-revisions.yaml' | |
| - 'scripts/tests/test-regenerate-publish-workflow-approved-revisions.sh' | |
| # The release watcher that dispatches it, and its test — both halves. | |
| - '.github/workflows/watch-actions-release.yaml' | |
| - 'scripts/watch-actions-release-candidate.sh' | |
| - 'scripts/tests/test-watch-actions-release-candidate.sh' | |
| # The generated set itself. The contract test reads it to derive which | |
| # consumer repositories the workflow's App token must be scoped to, so a | |
| # PR that only adds a consumer here — the exact change that would leave | |
| # the token unable to read that consumer — must still run that check. | |
| - 'scripts/publish-workflow-approved-revisions.tsv' | |
| # Both halves, for the same reason as the guards above. | |
| - 'scripts/guard-oci-repository-verify.sh' | |
| - 'scripts/tests/test-guard-oci-repository-verify.sh' | |
| # Both halves, for the same reason as the guard above. | |
| - 'scripts/validate-kubescape-frameworks/**' | |
| # Both halves, so editing the guard alone — or its test alone — | |
| # still runs the check that covers it. | |
| - 'scripts/guard-checkov-skip-reasons.sh' | |
| - 'scripts/tests/test-guard-checkov-skip-reasons.sh' | |
| # Both halves, for the same reason as the guard above. | |
| - 'scripts/guard-limitrange-premise.sh' | |
| - 'scripts/tests/test-guard-limitrange-premise.sh' | |
| # Both halves, for the same reason as the guard above. | |
| - 'scripts/guard-gitrepository-commit-pin.sh' | |
| - 'scripts/tests/test-guard-gitrepository-commit-pin.sh' | |
| # Both halves, for the same reason as the guard above. | |
| - 'scripts/guard-render-remote-resources.sh' | |
| - 'scripts/tests/test-guard-render-remote-resources.sh' | |
| # The reviewed disposition list the guard reads: editing a row | |
| # changes what the guard permits, so it must re-run. | |
| - 'scripts/render-remote-resource-exceptions.tsv' | |
| # Both halves and the reviewed exception list, for the same reasons. | |
| - 'scripts/guard-floating-image-tags.sh' | |
| - 'scripts/tests/test-guard-floating-image-tags.sh' | |
| - 'scripts/floating-image-tag-exceptions.tsv' | |
| # Both halves, for the same reason as the guards above. | |
| - 'scripts/guard-helm-post-renderers.sh' | |
| - 'scripts/tests/test-guard-helm-post-renderers.sh' | |
| - 'scripts/tests/test-kubescape-storage-placement.sh' | |
| - 'scripts/build-controller-helm.sh' | |
| - 'scripts/render-helm-chart/**' | |
| # Both halves and the reviewed exception list, for the same reasons. The | |
| # guard renders charts with the renderer the two entries above build. | |
| - 'scripts/guard-pdb-selector-match.sh' | |
| - 'scripts/tests/test-guard-pdb-selector-match.sh' | |
| - 'scripts/pdb-selector-exceptions.tsv' | |
| # Both halves, for the same reason as the guards above. | |
| - 'scripts/guard-pod-security-exception-scope.sh' | |
| - 'scripts/tests/test-guard-pod-security-exception-scope.sh' | |
| # Both halves, for the same reason as the guards above. The guard also | |
| # reads the kubescape HelmRelease and every Namespace manifest, both | |
| # covered by 'k8s/**'. | |
| - 'scripts/guard-baseline-context-optin-scan-scope.sh' | |
| - 'scripts/tests/test-guard-baseline-context-optin-scan-scope.sh' | |
| # Both halves and the reviewed list, for the same reasons. The guard also | |
| # reads the kubescape HelmRelease, covered by 'k8s/**'. | |
| - 'scripts/guard-kubescape-scan-scope.sh' | |
| - 'scripts/tests/test-guard-kubescape-scan-scope.sh' | |
| - 'scripts/kubescape-unscanned-namespaces.tsv' | |
| # The live result-coverage check (#4262). Only its fixture test runs in CI, since CI | |
| # has no cluster; it also reads the reviewed list above. | |
| - 'scripts/check-kubescape-result-coverage.sh' | |
| - 'scripts/tests/test-check-kubescape-result-coverage.sh' | |
| # The live orphaned-result report (#3697). Both halves, and only the fixture | |
| # test runs here for the same reason. | |
| - 'scripts/report-kubescape-scan-orphans.sh' | |
| - 'scripts/tests/test-report-kubescape-scan-orphans.sh' | |
| # Both halves, for the same reason as the guards above. The guard also | |
| # reads the exception manifest itself, covered by 'k8s/**'. | |
| - 'scripts/guard-pod-security-exception-documented.sh' | |
| - 'scripts/tests/test-guard-pod-security-exception-documented.sh' | |
| # Both halves, for the same reason as the guards above. | |
| - 'scripts/guard-crossplane-provider-name-parity.sh' | |
| - 'scripts/tests/test-guard-crossplane-provider-name-parity.sh' | |
| - 'scripts/tests/test-crossplane-aws-family-version-parity.sh' | |
| # Both halves, for the same reason as the guards above. | |
| - 'scripts/guard-unverified-provider-digest-pin.sh' | |
| - 'scripts/tests/test-guard-unverified-provider-digest-pin.sh' | |
| # Both halves, for the same reason as the guards above. This guard | |
| # also reads scripts/refresh-flux-ghcr-auth.sh, already selected | |
| # below, and k8s/bases/apps/kustomization.yaml, covered by 'k8s/**'. | |
| - 'scripts/guard-ghcr-fanout-component-gate.sh' | |
| - 'scripts/tests/test-guard-ghcr-fanout-component-gate.sh' | |
| - 'scripts/validate-eks-ci-role-policy/**' | |
| - 'scripts/tests/isolated-chart-namespace-rules.yaml' | |
| - 'scripts/tests/test-isolated-chart-namespace-rules.sh' | |
| - 'scripts/tests/production-authorization-rules.yaml' | |
| - 'scripts/tests/test-production-authorization-rules.sh' | |
| - 'tests/**' | |
| - 'scripts/validate-alert-coverage.sh' | |
| - 'scripts/tests/validate-alert-coverage/**' | |
| - 'scripts/refresh-flux-ghcr-auth.sh' | |
| - 'scripts/refresh-flux-ghcr-auth-safety.sh' | |
| - 'scripts/ghcr-auth-lib.sh' | |
| - 'scripts/run-ksail-prod-with-pull-auth.sh' | |
| - '.github/actions/deploy-prod/**' | |
| - '.github/workflows/dr-rebuild.yaml' | |
| - '.github/workflows/ci.yaml' | |
| # cd.yaml carries the manual-deploy gate itself. Without it here, a PR | |
| # that deletes that gate does not run the job whose test guards it. | |
| - '.github/workflows/cd.yaml' | |
| # Same reasoning for validate-main.yaml, which carries the | |
| # push-to-main gate: TestAuthorizationGateRunsOnPushToMain guards it, | |
| # and that test only runs when this filter matches. | |
| - '.github/workflows/validate-main.yaml' | |
| bridge_validation: | |
| - 'scripts/refresh-flux-ghcr-auth.sh' | |
| - 'scripts/refresh-flux-ghcr-auth-safety.sh' | |
| - 'scripts/ghcr-auth-lib.sh' | |
| - 'scripts/run-ksail-prod-with-pull-auth.sh' | |
| - 'scripts/use-prod-stable-api-endpoint.sh' | |
| - 'scripts/tests/test-use-prod-stable-api-endpoint.sh' | |
| - 'scripts/tests/test-refresh-flux-ghcr-auth-safety.sh' | |
| - 'scripts/tests/refresh-flux-ghcr-auth/**' | |
| - 'scripts/verify-gateway-routes-applied.sh' | |
| - 'scripts/tests/test-verify-gateway-routes-applied.sh' | |
| - 'docs/dr/runbook.md' | |
| - '.github/actions/deploy-prod/**' | |
| - '.github/workflows/dr-rebuild.yaml' | |
| - '.github/workflows/ci.yaml' | |
| # Every workflow that calls the stable-endpoint helper is checked by | |
| # that helper's wiring test, and new callers keep appearing. | |
| # Matching all workflows keeps that check running when only a caller | |
| # changes; the test fails if this line is removed. | |
| - '.github/workflows/**' | |
| talos: | |
| - 'talos/**' | |
| - 'talos-local/**' | |
| - 'scripts/reconcile-talos-kernel-args/**' | |
| - 'scripts/tests/test-talos-render-kernel-args.sh' | |
| - 'scripts/register-talos-factory-schematic.sh' | |
| - 'scripts/verify-talos-schematic-readback.sh' | |
| - 'scripts/tests/test-talos-factory-schematic-registration.sh' | |
| - 'scripts/tests/test-talos-schematic-readback.sh' | |
| - 'scripts/verify-kubelet-config-readback.sh' | |
| - 'scripts/tests/test-kubelet-config-readback.sh' | |
| - 'scripts/tests/test-talos-apparmor-lsm.sh' | |
| - 'scripts/tests/test-talos-apparmor-lsm-regressions.sh' | |
| - 'ksail.prod.yaml' | |
| - 'ksail.yaml' | |
| - '.github/scripts/setup-talosctl.sh' | |
| - 'scripts/validate-talos-kubernetes-compatibility/**' | |
| - '.github/actions/deploy-prod/**' | |
| - 'go.mod' | |
| - 'go.sum' | |
| - '.github/workflows/ci.yaml' | |
| - '.github/workflows/cd.yaml' | |
| # What the Docker reproduction of the Crossview login outage reads: | |
| # the chart pin and values, the prod apps overlay and the shared | |
| # components it applies to every HelmRelease, the sensor and its test. | |
| crossview: | |
| - 'k8s/bases/apps/crossview/**' | |
| - 'k8s/bases/apps/kustomization.yaml' | |
| - 'k8s/bases/components/annotations-transformers/**' | |
| - 'k8s/bases/components/helmrelease-drift-detection/**' | |
| - 'k8s/bases/components/helmrelease-flux-defaults/**' | |
| - 'k8s/providers/hetzner/apps/crossview/**' | |
| - 'k8s/providers/hetzner/apps/kustomization.yaml' | |
| - 'k8s/providers/hetzner/infrastructure/coroot/cron-job-crossview-login-alerter.yaml' | |
| - 'k8s/clusters/prod/bootstrap/config-map.yaml' | |
| - 'scripts/tests/test-crossview-login-alerter-runtime.sh' | |
| - '.github/workflows/ci.yaml' | |
| # The full offline fixture suite runs alongside changes, which retains the real production | |
| # render and ShellCheck. Both deployment and the required gate wait for these regressions. | |
| # Like changes, this uses kubectl, yq and jq supplied by the ubuntu-latest runner. | |
| validate-consumer-discovery: | |
| name: 🔐 Validate Consumer Discovery Regressions | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read # checkout repository | |
| steps: | |
| - name: 📑 Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: 🔐 Validate the full consumer discovery fixture suite | |
| run: | | |
| bash scripts/tests/test-guard-consumer-discovery-conservation.sh | |
| validate-publication-contract: | |
| name: 🖋️ Validate Publication Contract | |
| # A DEDICATED job, mirroring cd.yaml's, and that is the point rather than a | |
| # style choice. The runner's $GITHUB_ENV and $GITHUB_PATH bridges carry | |
| # across steps, so any step sharing a job with this validator could shadow | |
| # `go` for it while the validator step itself still read as correct. Keeping | |
| # the job to nothing but its own validator is what the contract's | |
| # gateJobRunsOnlyItsValidator rule asserts, and it can only hold here. | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read # checkout repository | |
| steps: | |
| - name: 📑 Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: ⚙️ Setup Go | |
| uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version-file: go.mod | |
| cache: false | |
| - name: 🖋️ Validate DR signing contract | |
| # Identical invocation to cd.yaml's, deliberately: the two routes to | |
| # production must be gated by the same check, or they drift and the | |
| # weaker one becomes the real policy. | |
| run: | | |
| go test ./scripts/validate-dr-signing | |
| go run ./scripts/validate-dr-signing .github/workflows/dr-rebuild.yaml ksail.prod.yaml | |
| validate-matcher-efficacy: | |
| name: 🔏 Validate Cosign Matcher Efficacy | |
| # A DEDICATED job because this is the only validator that needs to READ the | |
| # registry, and that permission should not be inherited by the jobs that do | |
| # not need it. | |
| # | |
| # Every static check we had passed on a matcher that verified ZERO artifacts | |
| # and halted all GitOps delivery on prod for ~5.5 hours (#3005). This asks | |
| # the one question those cannot: does the matcher, as written in the | |
| # manifest, accept a signer that really signed this artifact? | |
| # | |
| # SKIPPED ON FORK PULL REQUESTS, and still enforced before anything merges. | |
| # Reading this artifact needs a token carrying package read; a fork PR's | |
| # GITHUB_TOKEN does not have one, so running it there would fail every | |
| # external contribution on a permission it cannot be granted. The merge | |
| # queue is not a fork context, so the gate still holds at the only moment | |
| # that decides what production actually gets. | |
| if: github.event_name == 'merge_group' || github.event.pull_request.head.repo.fork == false | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read # checkout repository | |
| packages: read # read the manifests artifact's signature from GHCR | |
| steps: | |
| - name: 📑 Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: ⚙️ Setup Go | |
| uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version-file: go.mod | |
| cache: false | |
| - name: 🔏 Install cosign | |
| uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 | |
| - name: 🔏 Test the cosign failure classifier | |
| # The check below words its failure by WHY cosign failed (#3545). This | |
| # pins that a genuine identity mismatch keeps the matcher finding, and | |
| # that an outage, a refused credential or a server error never borrows | |
| # it — at the classifier and through the check step itself, with cosign | |
| # stubbed. No registry, network or credential is involved. | |
| run: | | |
| shellcheck -x scripts/cosign-failure-lib.sh scripts/tests/test-cosign-failure-lib.sh | |
| bash scripts/tests/test-cosign-failure-lib.sh | |
| - name: 🔏 Check the configured matcher verifies the real artifact | |
| # 🔴 THE PATTERNS ARE NEVER SPELLED HERE. They are printed out of the | |
| # manifests we deploy and handed straight to cosign, so this tests the | |
| # deployed rule rather than a copy of it that can drift. | |
| # | |
| # cosign is the matcher's real consumer — Flux hands it these same two | |
| # regexes — so letting cosign decide keeps the check's semantics | |
| # identical to production's instead of reimplementing them. | |
| env: | |
| REGISTRY_USER: ${{ github.actor }} | |
| REGISTRY_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| ARTIFACT: ghcr.io/devantler-tech/platform/manifests:latest | |
| run: | | |
| set -euo pipefail | |
| go test ./scripts/validate-matcher-efficacy | |
| MATCHER_JSON="$(go run ./scripts/validate-matcher-efficacy --print-matcher \ | |
| ksail.prod.yaml \ | |
| k8s/providers/hetzner/infrastructure/controllers/flux-instance/flux-instance.yaml)" | |
| # 🔴 DECODE AS JSON, never line-per-value. A matcher value may legitimately | |
| # contain a newline — a YAML literal block is valid for these fields — and a | |
| # line-based read would take only its first line, verifying against a | |
| # truncated regex while Flux enforces the whole thing. That is a matcher | |
| # inert in production and green here: exactly what this job refuses. | |
| # `jq -e` fails on a null or missing field, so a malformed handoff stops | |
| # the build instead of silently yielding an empty pattern. | |
| ISSUER="$(jq -er '.issuer' <<<"${MATCHER_JSON}")" | |
| SUBJECT="$(jq -er '.subject' <<<"${MATCHER_JSON}")" | |
| if [ -z "${ISSUER}" ] || [ -z "${SUBJECT}" ]; then | |
| echo "::error::could not read the matcher out of the manifests" | |
| exit 1 | |
| fi | |
| echo "matcher issuer: ${ISSUER}" | |
| echo "matcher subject: ${SUBJECT}" | |
| echo "${REGISTRY_TOKEN}" | cosign login ghcr.io -u "${REGISTRY_USER}" --password-stdin | |
| # THE CHECK. Fails when the deployed matcher would not verify the | |
| # artifact it guards — which is #3005, caught before the deploy. | |
| # | |
| # Every failure fails the job; only the WORDING depends on why cosign | |
| # failed. The matcher finding is reserved for output showing cosign | |
| # read the signature and refused its identity. A ghcr.io timeout used | |
| # to be reported as the matcher failing to verify (#3545), so an | |
| # outage, a refused credential or an unrecognised failure is now named | |
| # as what it is. | |
| source scripts/cosign-failure-lib.sh | |
| COSIGN_LOG="$(mktemp)" | |
| trap 'rm -f "${COSIGN_LOG}"' EXIT | |
| if ! cosign verify \ | |
| --certificate-oidc-issuer-regexp "${ISSUER}" \ | |
| --certificate-identity-regexp "${SUBJECT}" \ | |
| "${ARTIFACT}" 2>&1 > /dev/null | tee "${COSIGN_LOG}" >&2; then | |
| FAILURE_CLASS="$(cosign_failure_class "${COSIGN_LOG}")" | |
| if [ "${FAILURE_CLASS}" = "rejected" ]; then | |
| echo "::error::the cosign matcher in the manifests does not verify ${ARTIFACT}." | |
| echo "::error::a matcher that matches nothing is inert: present, green, and enforcing nothing." | |
| else | |
| cosign_report_no_verdict "${FAILURE_CLASS}" "${ARTIFACT}" "${COSIGN_LOG}" | |
| fi | |
| exit 1 | |
| fi | |
| echo "the configured matcher verifies the artifact" | |
| # THE NEGATIVE CONTROL, run on every build rather than asserted once. | |
| # A deliberately wrong subject MUST be refused; if it is accepted the | |
| # check above proves nothing and is itself the vacuous control this | |
| # job exists to prevent. | |
| if cosign verify \ | |
| --certificate-oidc-issuer-regexp "${ISSUER}" \ | |
| --certificate-identity-regexp '^https://github\.com/devantler-tech/NOT-A-REAL-REPO/.*$' \ | |
| "${ARTIFACT}" > /dev/null 2>&1; then | |
| echo "::error::negative control PASSED — a deliberately wrong subject was accepted." | |
| echo "::error::this check cannot distinguish a good matcher from a broken one. Fix it before trusting it." | |
| exit 1 | |
| fi | |
| # 🔴 THAT NON-ZERO EXIT IS NOT YET EVIDENCE OF A REFUSAL. A network, | |
| # registry or auth failure also exits non-zero, and would report the | |
| # control as having passed while proving nothing — the control would | |
| # then be vacuous in exactly the way it exists to detect. Re-run the | |
| # POSITIVE check: if the same path still verifies, the refusal above | |
| # was a real refusal rather than an outage. This is a liveness | |
| # sandwich, deliberately not a match on cosign's error text, which is | |
| # not a stable interface. | |
| if ! cosign verify \ | |
| --certificate-oidc-issuer-regexp "${ISSUER}" \ | |
| --certificate-identity-regexp "${SUBJECT}" \ | |
| "${ARTIFACT}" > /dev/null; then | |
| echo "::error::the negative control exited non-zero, but the artifact can no longer be" | |
| echo "::error::verified either — so this run cannot prove the control refused for the" | |
| echo "::error::right reason. Treating an unprovable control as a failure, not a pass." | |
| exit 1 | |
| fi | |
| echo "negative control refused a wrong subject, and the positive path still verifies" | |
| echo "=> the check above is not vacuous" | |
| - name: ⚖️ Read Kyverno CLI version | |
| id: kyverno-version | |
| run: bash .github/scripts/kyverno-version.sh | |
| - name: ⚖️ Install Kyverno CLI for package admission checks | |
| uses: kyverno/action-install-cli@fcee92fca5c883169ef9927acf543e0b5fc58289 # v0.2.0 | |
| with: | |
| release: ${{ steps.kyverno-version.outputs.release }} | |
| - name: 🔐 Verify proposed first-party Crossplane packages | |
| # The package image becomes the provider controller's Pod image. Check | |
| # both Talos's first-match pull rule and Kyverno's actual admission | |
| # policy against the proposed reference, before a rollout can wedge it. | |
| # This inherits the registry-read job's fork/merge-group boundary. | |
| timeout-minutes: 10 | |
| env: | |
| REGISTRY_USER: ${{ github.actor }} | |
| REGISTRY_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| DOCKER_CONFIG: ${{ runner.temp }}/package-signature-docker | |
| run: | | |
| set -euo pipefail | |
| mkdir -p "$DOCKER_CONFIG" | |
| trap 'rm -rf "$DOCKER_CONFIG"' EXIT | |
| printf '%s' "$REGISTRY_TOKEN" | cosign login ghcr.io -u "$REGISTRY_USER" --password-stdin | |
| bash scripts/check-first-party-package-signatures.sh | |
| - name: 🔐 Verify proposed Upbound Crossplane packages | |
| # The Upbound AWS providers run with the platform's AWS CI identity. | |
| # Each proposed package must carry Upbound's own build signature, and a | |
| # wrong identity must be refused on every run (#4189). Their registry | |
| # is public, so this needs no credential. | |
| timeout-minutes: 10 | |
| run: bash scripts/check-upbound-package-signatures.sh | |
| validate-eks-authorization: | |
| name: 🔐 Validate Production Authorization | |
| needs: [changes] | |
| if: >- | |
| needs.changes.outputs.k8s == 'true' && | |
| (github.event_name == 'pull_request' || github.event_name == 'merge_group') | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read # checkout repository | |
| steps: | |
| - name: 📑 Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: ⚙️ Setup Go | |
| uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version-file: go.mod | |
| cache: false | |
| - name: ⚙️ Install pinned kubectl renderer | |
| shell: bash | |
| env: | |
| KUBECTL_VERSION: "v1.36.2" | |
| KUBECTL_SHA256: "1e9045ec32bea85da43de85f0065358529ea7c7a152eca78154fba5b58c27d82" | |
| run: | | |
| set -euo pipefail | |
| kubectl_path="$RUNNER_TEMP/kubectl" | |
| curl -fsSL \ | |
| "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl" \ | |
| -o "$kubectl_path" | |
| printf '%s %s\n' "$KUBECTL_SHA256" "$kubectl_path" | sha256sum --check | |
| sudo install "$kubectl_path" /usr/local/bin/kubectl | |
| kubectl version --client | |
| - name: 🔐 Validate static production authorization controls | |
| # The Go validator pins the EKS CI role and the authored authorization | |
| # graph before either a PR or merge-group commit may proceed. The rendered | |
| # surface is approved entry by entry in | |
| # scripts/validate-eks-ci-role-policy/approved-surface.txt, so two PRs that | |
| # move different documents merge without a conflict and the merge-group | |
| # run validates their combination (#3182). | |
| # Both commands run unconditionally, and the step fails if either did. | |
| # They are independent outputs over the same surface: `go test` is the | |
| # verdict, `go run` prints the exact ledger lines to approve. A real | |
| # surface mismatch fails BOTH, so short-circuiting on the first left the | |
| # one failure that most needs explaining unexplained (#3879). Covered by | |
| # scripts/tests/test-authorization-gate-diagnostics.sh. | |
| run: | | |
| set -euo pipefail | |
| status=0 | |
| go test ./scripts/validate-eks-ci-role-policy || status=1 | |
| go run ./scripts/validate-eks-ci-role-policy . || status=1 | |
| exit "$status" | |
| - name: ⚙️ Setup KSail for effective authorization validation | |
| shell: bash | |
| env: | |
| # renovate: datasource=github-releases depName=devantler-tech/ksail extractVersion=^v(?<version>.+)$ | |
| KSAIL_VERSION: "7.202.58" | |
| GITHUB_TOKEN: ${{ github.token }} | |
| run: .github/scripts/setup-ksail.sh | |
| - name: ⚖️ Read Kyverno CLI version for chart admission | |
| id: chart-kyverno-version | |
| run: bash .github/scripts/kyverno-version.sh | |
| - name: ⚖️ Install Kyverno CLI for chart admission | |
| uses: kyverno/action-install-cli@fcee92fca5c883169ef9927acf543e0b5fc58289 # v0.2.0 | |
| with: | |
| release: ${{ steps.chart-kyverno-version.outputs.release }} | |
| - name: 🔐 Validate isolated chart child namespaces | |
| # Render the immutable chart explicitly so its actual children satisfy | |
| # both namespace containment and production workload admission before | |
| # this PR or speculative merge-group revision can deploy it. | |
| timeout-minutes: 10 | |
| run: | | |
| shellcheck scripts/tests/test-isolated-chart-namespace-rules.sh | |
| bash scripts/tests/test-isolated-chart-namespace-rules.sh | |
| - name: 🔐 Validate the production authorization rule suite | |
| # Split from the render below so the job summary attributes a failure to | |
| # the rule suite rather than to an unreachable chart registry. | |
| run: | | |
| shellcheck scripts/tests/test-production-authorization-rules.sh | |
| bash scripts/tests/test-production-authorization-rules.sh | |
| - name: 🚦 Validate Actual Budget user-namespace pilot | |
| # Exercise the active component and its rollback on both the PR and the | |
| # speculative merge-group revision before deploy-prod can publish it. | |
| timeout-minutes: 5 | |
| run: | | |
| shellcheck scripts/tests/test-actual-budget-user-namespaces.sh | |
| bash scripts/tests/test-actual-budget-user-namespaces.sh | |
| - name: 🔐 Validate Helm-rendered production authorization controls | |
| # KSail renders chart children before evaluating the CEL rules, closing | |
| # the visibility gap in kubectl kustomize's HelmRelease-only output. | |
| # | |
| # This reaches every upstream chart registry, and the job gates | |
| # deploy-prod, so an unreachable registry would otherwise hang a | |
| # required check and hold the merge queue for the job's full budget. | |
| timeout-minutes: 10 | |
| run: ksail --config ksail.prod.yaml workload validate | |
| validate: | |
| name: 🧪 Validate Manifests | |
| needs: [changes] | |
| # Static, no-cluster validation. The local Docker cluster is now a thin | |
| # manual test-bed (see AGENTS.md) rather than a full prod stand-in, so CI no | |
| # longer boots it; instead it schema-validates and security-scans the | |
| # manifests. This needs no secrets, so — unlike the old system test — it also | |
| # runs on fork PRs. | |
| if: >- | |
| github.event_name == 'pull_request' && | |
| (needs.changes.outputs.k8s == 'true' || | |
| needs.changes.outputs.bridge_validation == 'true') | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read # checkout repository | |
| security-events: write # upload the Kubescape SARIF to Code Scanning | |
| steps: | |
| - name: 📑 Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: ⚙️ Setup Go | |
| # Used by the embedded-JSON validator and Kubescape exceptions generator. | |
| uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version-file: go.mod | |
| cache: false | |
| - name: 📌 Install the reviewed YAML parser | |
| if: needs.changes.outputs.k8s == 'true' | |
| env: | |
| GOBIN: ${{ runner.temp }}/platform-validation-tools | |
| run: | | |
| go install github.com/mikefarah/yq/v4@v4.54.1 | |
| printf '%s\n' "$GOBIN" >> "$GITHUB_PATH" | |
| PATH="$GOBIN:$PATH" yq --version | |
| - name: 🔐 Validate KSail installer integrity guard | |
| if: needs.changes.outputs.k8s == 'true' | |
| run: | | |
| shellcheck .github/scripts/setup-ksail.sh scripts/tests/test-setup-ksail.sh | |
| bash scripts/tests/test-setup-ksail.sh | |
| - name: 🔐 Validate Cilium SNI hostname coverage | |
| if: needs.changes.outputs.k8s == 'true' | |
| run: | | |
| go test ./scripts/guard-cilium-sni | |
| go run ./scripts/guard-cilium-sni k8s | |
| - name: 🔐 Validate talosctl installer integrity guard | |
| if: needs.changes.outputs.k8s == 'true' | |
| run: | | |
| shellcheck .github/scripts/setup-talosctl.sh scripts/tests/test-setup-talosctl.sh | |
| bash scripts/tests/test-setup-talosctl.sh | |
| - name: 🔐 Validate supply-chain tool integrity | |
| if: needs.changes.outputs.k8s == 'true' | |
| run: | | |
| shellcheck .github/scripts/setup-supply-chain-tools.sh scripts/tests/test-setup-supply-chain-tools.sh | |
| bash scripts/tests/test-setup-supply-chain-tools.sh | |
| - name: 🔐 Exercise verified supply-chain tools | |
| if: needs.changes.outputs.k8s == 'true' | |
| run: | | |
| .github/scripts/setup-supply-chain-tools.sh | |
| /usr/local/bin/syft scan dir:. --output cyclonedx-json=tool-integrity.cdx.json | |
| jq -e '.bomFormat == "CycloneDX" and (.components | length > 0)' tool-integrity.cdx.json | |
| - name: 🔐 Validate Flux GHCR credential bridge | |
| # Pure local tests with fake ksail/docker/kubectl commands: no SOPS key, | |
| # registry credential, cluster access, or network is used. | |
| run: | | |
| bash -n \ | |
| scripts/ghcr-auth-lib.sh \ | |
| scripts/refresh-flux-ghcr-auth-safety.sh \ | |
| scripts/refresh-flux-ghcr-auth.sh \ | |
| scripts/run-ksail-prod-with-pull-auth.sh \ | |
| scripts/use-prod-stable-api-endpoint.sh \ | |
| scripts/wait-for-prod-api-stability.sh \ | |
| scripts/tests/test-use-prod-stable-api-endpoint.sh \ | |
| scripts/tests/test-wait-for-prod-api-stability.sh \ | |
| scripts/tests/test-refresh-flux-ghcr-auth-safety.sh | |
| shellcheck \ | |
| scripts/ghcr-auth-lib.sh \ | |
| scripts/refresh-flux-ghcr-auth-safety.sh \ | |
| scripts/refresh-flux-ghcr-auth.sh \ | |
| scripts/run-ksail-prod-with-pull-auth.sh \ | |
| scripts/use-prod-stable-api-endpoint.sh \ | |
| scripts/wait-for-prod-api-stability.sh \ | |
| scripts/tests/test-use-prod-stable-api-endpoint.sh \ | |
| scripts/tests/test-wait-for-prod-api-stability.sh \ | |
| scripts/tests/test-refresh-flux-ghcr-auth-safety.sh | |
| bash scripts/tests/test-use-prod-stable-api-endpoint.sh | |
| bash scripts/tests/test-wait-for-prod-api-stability.sh | |
| bash scripts/tests/test-refresh-flux-ghcr-auth-safety.sh | |
| go test -timeout=20m ./scripts/tests/refresh-flux-ghcr-auth | |
| - name: 🚦 Validate the gateway route-application check | |
| # Local test with a fake kubectl: no cluster access. The deploy runs | |
| # this check last, so a gateway that stopped applying routes fails the | |
| # deploy instead of going unnoticed (#4198). | |
| run: | | |
| shellcheck \ | |
| scripts/verify-gateway-routes-applied.sh \ | |
| scripts/tests/test-verify-gateway-routes-applied.sh | |
| bash scripts/tests/test-verify-gateway-routes-applied.sh | |
| - name: 🧩 Validate embedded JSON blobs | |
| # ConfigMaps that embed whole JSON documents as block scalars (e.g. the | |
| # Headlamp Kubescape exceptions ConfigMap's exceptionPolicies) are opaque | |
| # strings to schema validation, so a stray comma ships silently and only | |
| # surfaces at consumption time — where an empty exceptions view reads | |
| # identical to a clean posture. Local Go tests and validation need no | |
| # cluster or credentials. See scripts/validate-embedded-json/ (#2480). | |
| run: | | |
| shellcheck scripts/validate-embedded-json.sh | |
| go test ./scripts/validate-embedded-json | |
| bash scripts/validate-embedded-json.sh | |
| - name: 🧷 Validate vendored operator Checkov dispositions | |
| if: needs.changes.outputs.k8s == 'true' | |
| # Exercise the annotator over multi-document fixtures, then prove the | |
| # committed bundles are the pinned upstream bytes plus only the | |
| # configured resource-scoped dispositions. Any other hand edit or a | |
| # vendor rename must fail offline. | |
| run: | | |
| bash -n scripts/update-vendored-operators.sh | |
| shellcheck scripts/update-vendored-operators.sh \ | |
| scripts/guard-cert-approver-image-pin.sh \ | |
| scripts/tests/test-guard-cert-approver-image-pin.sh | |
| go test ./scripts/annotate-vendored-checkov | |
| bash scripts/tests/test-guard-cert-approver-image-pin.sh | |
| scripts/update-vendored-operators.sh --validate-committed | |
| - name: 🔎 Validate Trivy ignore-file wiring | |
| if: needs.changes.outputs.k8s == 'true' | |
| # The path-scoped YAML format is not auto-loaded. Exercise the real | |
| # reproduction helper at its command boundary so a future argument | |
| # cleanup cannot silently turn every scoped disposition back on. | |
| run: bash scripts/tests/test-megalinter-scan-counts-ignorefile.sh | |
| - name: 🧱 Validate vendored-operator disposition boundary | |
| if: needs.changes.outputs.k8s == 'true' | |
| # The KubeVirt/CDI RBAC dispositions are path-scoped to those two | |
| # vendored bundles. If one ever loses its paths key or widens, the | |
| # same checks go quiet on the first-party cluster roles this | |
| # repository authors, where they are still open findings — and the | |
| # count would drop, which reads exactly like progress. yq-only | |
| # structural checks always run here; the trivy paired control runs | |
| # wherever trivy is installed. | |
| run: | | |
| shellcheck scripts/tests/test-trivyignore-vendored-operator-boundary.sh | |
| bash scripts/tests/test-trivyignore-vendored-operator-boundary.sh | |
| - name: 🧱 Validate vault-config identity disposition boundary | |
| if: needs.changes.outputs.k8s == 'true' | |
| # The KSV-0020 disposition on the vault-config Job is scoped by PATH, but the reason it | |
| # is accepted is per CONTAINER: only the two openbao containers run at the image-baked | |
| # uid 100, and the rest take the pod's high 65532 default. trivy cannot express that, | |
| # so a container added later at a low UID would be silently suppressed and the count | |
| # would simply not rise — which reads exactly like nothing happened. The premise is | |
| # asserted per container here; the paired trivy control runs wherever trivy is | |
| # installed and skips loudly where it is not. | |
| run: | | |
| shellcheck scripts/tests/test-trivyignore-vault-config-identity-boundary.sh | |
| bash scripts/tests/test-trivyignore-vault-config-identity-boundary.sh | |
| - name: 🧱 Validate vault-backup identity disposition boundary | |
| if: needs.changes.outputs.k8s == 'true' | |
| # The two workloads pin the same openbao digest: only snapshot may use its image-baked | |
| # UID/GID 100:1000, while mirror keeps the high pod default and receives group 1000 only | |
| # through fsGroup. The guard keeps both identity dispositions path-scoped and requires an | |
| # identical unlisted probe to stay reported. | |
| run: | | |
| shellcheck scripts/tests/test-trivyignore-vault-backup-identity-boundary.sh | |
| bash scripts/tests/test-trivyignore-vault-backup-identity-boundary.sh | |
| - name: 🧱 Validate first-party RBAC disposition premises | |
| if: needs.changes.outputs.k8s == 'true' | |
| # The four first-party RBAC dispositions rest on facts trivy cannot see: the tenant | |
| # role is only ever RoleBinding-bound, cluster-reader grants no write verb, and KRO | |
| # runs with no standing access. If one of those stops holding, the finding stays | |
| # suppressed while its reason is gone and the count never moves — so assert the | |
| # premises rather than the prose. | |
| run: | | |
| shellcheck scripts/tests/test-trivyignore-first-party-rbac-boundary.sh | |
| bash scripts/tests/test-trivyignore-first-party-rbac-boundary.sh | |
| - name: 🧱 Validate the tenant identity cannot write standard NetworkPolicies | |
| if: needs.changes.outputs.k8s == 'true' | |
| # Cilium unions standard NetworkPolicy allow rules with CiliumNetworkPolicy rules, | |
| # and restrict-tenant-network-policies constrains only the latter — so a write | |
| # grant on networkpolicies is a bypass of the whole tenant boundary. This renders | |
| # both providers' infrastructure layers and refuses any tenant-aggregated | |
| # ClusterRole granting a write verb on it (#3396). | |
| run: | | |
| shellcheck scripts/tests/test-tenant-edit-networkpolicy-write.sh | |
| bash scripts/tests/test-tenant-edit-networkpolicy-write.sh | |
| - name: 🧱 Validate ConfigMap sensitive-content disposition boundary | |
| if: needs.changes.outputs.k8s == 'true' | |
| # KSV-01010 matches key and value TEXT, so it cannot tell a public ACME contact | |
| # address from a credential. The three dispositioned ConfigMaps are excepted BY | |
| # PATH, and that is the whole safety property: if the scoping ever widened, an | |
| # application ConfigMap that genuinely embeds a secret would go quiet too, and the | |
| # count would simply drop — which reads exactly like progress. The paired control | |
| # scans the same bytes at an excepted and a non-excepted path, so only the path can | |
| # explain the difference. It skips where trivy is absent rather than passing blindly. | |
| run: | | |
| shellcheck scripts/tests/test-trivyignore-configmap-content-boundary.sh | |
| bash scripts/tests/test-trivyignore-configmap-content-boundary.sh | |
| - name: ⚙️ Setup Trivy for nested RGD templates | |
| if: needs.changes.outputs.k8s == 'true' | |
| uses: aquasecurity/setup-trivy@81e514348e19b6112ce2a7e3ecbafe19c1e1f567 # v0.3.1 | |
| with: | |
| version: v0.74.0 | |
| cache: true | |
| - name: 🔬 Validate nested RGD workload templates | |
| if: needs.changes.outputs.k8s == 'true' | |
| # Trivy does not descend through a ResourceGraphDefinition custom resource. Extract every | |
| # nested template, scan without the repository ignorefile, and compare complete findings, | |
| # resource-graph controls, and committed substitution inputs to the explicit ratchet. The | |
| # behavioral test also applies registry, placement, and policy-regression probes. | |
| run: | | |
| shellcheck scripts/scan-rgd-templates.sh scripts/tests/test-rgd-template-static-scan.sh | |
| bash scripts/tests/test-rgd-template-static-scan.sh | |
| - name: 🔢 Validate scanner version-drift guard | |
| if: needs.changes.outputs.k8s == 'true' | |
| # Guards the guard, offline. The live comparison runs on main | |
| # (validate-main.yaml) because it needs to read Actions logs, which a | |
| # fork PR's token cannot do — gating PRs on it would fail closed on | |
| # every fork. What is checked here is the parsing contract, whose | |
| # dangerous direction is a log-format change reading as "no drift". | |
| run: | | |
| shellcheck scripts/check-megalinter-version-drift.sh | |
| bash scripts/tests/test-check-megalinter-version-drift.sh | |
| - name: 🔐 Validate OpenBao OIDC admin role | |
| if: needs.changes.outputs.k8s == 'true' | |
| # bound_claims is a map. Passing JSON-looking text as one CLI argument | |
| # serializes it as a string and leaves the role unenforced. | |
| run: bash scripts/tests/test-openbao-oidc-role.sh | |
| - name: 🐤 Validate staged OpenBao standby repair | |
| if: needs.changes.outputs.k8s == 'true' | |
| run: | | |
| shellcheck scripts/tests/test-openbao-standby-canary.sh | |
| shellcheck scripts/tests/test-openbao-strategy-transition.sh | |
| bash scripts/tests/test-openbao-standby-canary.sh | |
| - name: 🧯 Validate daily and break-glass API boundaries | |
| if: needs.changes.outputs.k8s == 'true' | |
| # Daily Kubernetes/Talos access must stay read-only, while the root | |
| # credentials remain explicit break-glass artifacts. | |
| run: bash scripts/tests/test-human-api-access-boundary.sh | |
| - name: 🕹️ Validate the zone release signer boundary | |
| if: needs.changes.outputs.k8s == 'true' | |
| run: | | |
| shellcheck scripts/tests/test-world-zone-signatures.sh | |
| bash scripts/tests/test-world-zone-signatures.sh | |
| - name: 🕹️ Validate the private zone trial boundaries | |
| if: needs.changes.outputs.k8s == 'true' | |
| run: | | |
| shellcheck scripts/tests/test-world-zone-trial.sh | |
| bash scripts/tests/test-world-zone-trial.sh | |
| - name: 🧩 Validate Headlamp plugin removal | |
| if: needs.changes.outputs.k8s == 'true' | |
| # Removing runtime-downloaded plugins must also prune upgrade state and | |
| # preserve Crossview's authenticated fresh-install entry point. | |
| run: bash scripts/tests/test-headlamp-plugin-removal.sh | |
| - name: 🔑 Validate github-config Role covers the activated MR kinds | |
| if: needs.changes.outputs.k8s == 'true' | |
| # The tenant Role enumerates managed-resource kinds instead of `*`, so | |
| # activating a kind without granting it would fail at apply time inside | |
| # a separately published OCI artifact. Catch the divergence here. Bash, | |
| # yq and the runner's kubectl; no cluster or secrets required. | |
| run: | | |
| shellcheck scripts/tests/test-github-config-role-activation-parity.sh | |
| bash scripts/tests/test-github-config-role-activation-parity.sh | |
| - name: ⚖️ Read Kyverno CLI version | |
| if: needs.changes.outputs.k8s == 'true' | |
| id: kyverno-version | |
| run: bash .github/scripts/kyverno-version.sh | |
| - name: ⚖️ Install Kyverno CLI | |
| if: needs.changes.outputs.k8s == 'true' | |
| # Both admission checks and policy fixtures consume the shared CLI pin. | |
| # Review it with the Kyverno chart's appVersion when updating the group. | |
| uses: kyverno/action-install-cli@fcee92fca5c883169ef9927acf543e0b5fc58289 # v0.2.0 | |
| with: | |
| release: ${{ steps.kyverno-version.outputs.release }} | |
| - name: 🛑 Validate owned ARC retirement admission | |
| if: needs.changes.outputs.k8s == 'true' | |
| run: | | |
| shellcheck scripts/tests/test-arc-retirement-fence.sh | |
| bash scripts/tests/test-arc-retirement-fence.sh | |
| - name: 🔒 Validate Umami provisioning serialization | |
| if: needs.changes.outputs.k8s == 'true' | |
| run: | | |
| shellcheck scripts/tests/test-umami-provisioning-bootstrap.sh | |
| bash scripts/tests/test-umami-provisioning-bootstrap.sh | |
| - name: 🔖 Validate Homepage bookmarks | |
| if: needs.changes.outputs.k8s == 'true' | |
| # bookmarks.yaml is an embedded block scalar, so schema validation cannot | |
| # see a missing icon, a non-https link, or a group name that collides with | |
| # a service group. The tests prove each check fires; the validator gates | |
| # the real ConfigMap (#2909). | |
| run: | | |
| shellcheck scripts/validate-homepage-bookmarks.sh scripts/tests/test-validate-homepage-bookmarks.sh | |
| go test ./scripts/validate-homepage-bookmarks | |
| bash scripts/tests/test-validate-homepage-bookmarks.sh | |
| bash scripts/validate-homepage-bookmarks.sh | |
| - name: 🔐 Validate vault snapshots stay group-readable | |
| if: needs.changes.outputs.k8s == 'true' | |
| run: | | |
| shellcheck scripts/tests/test-vault-snapshot-group-readable.sh | |
| bash scripts/tests/test-vault-snapshot-group-readable.sh | |
| - name: ⚖️ Validate cluster policies | |
| if: needs.changes.outputs.k8s == 'true' | |
| # Runs the kyverno test fixtures under tests/. Until now nothing | |
| # executed them, so tests/validate-replica-floor had never actually run | |
| # in CI — a policy regression could ship green. No cluster or secrets | |
| # required; the CLI evaluates the committed ClusterPolicy files offline. | |
| # kyverno test counts a row whose rule never evaluated the resource | |
| # (REASON=Excluded) as passing and silently drops a declared resource | |
| # it never loaded, so the fixture guard fails a pass/fail row it never | |
| # evaluated, a rule name no loaded policy defines, and a declared | |
| # resource with no row. | |
| run: | | |
| kyverno test ./tests | |
| shellcheck scripts/validate-kyverno-fixture-evaluation.sh scripts/tests/test-validate-kyverno-fixture-evaluation.sh | |
| bash scripts/tests/test-validate-kyverno-fixture-evaluation.sh | |
| bash scripts/validate-kyverno-fixture-evaluation.sh tests | |
| shellcheck scripts/tests/test-hcloud-volume-priority.sh | |
| bash scripts/tests/test-hcloud-volume-priority.sh | |
| shellcheck scripts/tests/test-tetragon-agent-priority.sh | |
| bash scripts/tests/test-tetragon-agent-priority.sh | |
| shellcheck scripts/tests/test-audit-privileged-rbac.sh | |
| bash scripts/tests/test-audit-privileged-rbac.sh | |
| shellcheck scripts/tests/test-arc-controller-availability.sh | |
| bash scripts/tests/test-arc-controller-availability.sh | |
| shellcheck scripts/tests/test-restrict-tenant-secret-stores.sh | |
| bash scripts/tests/test-restrict-tenant-secret-stores.sh | |
| shellcheck scripts/tests/test-restrict-tenant-network-policies.sh | |
| bash scripts/tests/test-restrict-tenant-network-policies.sh | |
| shellcheck .github/scripts/fetch-cilium-deny-candidate.sh scripts/prove-cilium-default-deny.sh scripts/tests/test-fetch-cilium-deny-candidate.sh scripts/tests/test-prove-cilium-default-deny.sh | |
| bash scripts/tests/test-fetch-cilium-deny-candidate.sh | |
| bash scripts/tests/test-prove-cilium-default-deny.sh | |
| shellcheck scripts/tests/test-add-default-deny-shape.sh | |
| bash scripts/tests/test-add-default-deny-shape.sh | |
| shellcheck scripts/tests/test-restrict-homepage-service-groups.sh | |
| bash scripts/tests/test-restrict-homepage-service-groups.sh | |
| shellcheck scripts/tests/test-kyverno-umami-mutation-rbac.sh | |
| bash scripts/tests/test-kyverno-umami-mutation-rbac.sh | |
| shellcheck scripts/tests/test-kyverno-github-team-report-rbac.sh | |
| bash scripts/tests/test-kyverno-github-team-report-rbac.sh | |
| shellcheck scripts/tests/test-tenant-route-hostname-boundary.sh | |
| bash scripts/tests/test-tenant-route-hostname-boundary.sh | |
| shellcheck scripts/tests/test-actual-budget-auth-route.sh | |
| bash scripts/tests/test-actual-budget-auth-route.sh | |
| shellcheck scripts/tests/test-umami-sso-dashboard-route.sh | |
| bash scripts/tests/test-umami-sso-dashboard-route.sh | |
| shellcheck scripts/tests/test-data-product-controller-trial.sh | |
| bash scripts/tests/test-data-product-controller-trial.sh | |
| shellcheck scripts/tests/test-data-product-ui-host.sh | |
| bash scripts/tests/test-data-product-ui-host.sh | |
| shellcheck scripts/verify-data-product-ui-rollout.sh scripts/tests/test-data-product-ui-rollout.sh | |
| bash scripts/tests/test-data-product-ui-rollout.sh | |
| shellcheck scripts/tests/test-data-product-runtime-digests.sh | |
| bash scripts/tests/test-data-product-runtime-digests.sh | |
| shellcheck scripts/tests/test-oauth2-proxy-sso-redirects.sh | |
| bash scripts/tests/test-oauth2-proxy-sso-redirects.sh | |
| shellcheck scripts/tests/test-umami-tracking-probe.sh | |
| bash scripts/tests/test-umami-tracking-probe.sh | |
| shellcheck scripts/verify-umami-tracking.sh | |
| shellcheck scripts/tests/test-provider-drc-pod-seccomp.sh | |
| bash scripts/tests/test-provider-drc-pod-seccomp.sh | |
| shellcheck scripts/tests/test-add-baseline-context.sh | |
| bash scripts/tests/test-add-baseline-context.sh | |
| shellcheck scripts/tests/test-label-baseline-workers.sh | |
| bash scripts/tests/test-label-baseline-workers.sh | |
| shellcheck scripts/tests/test-longhorn-registrar-prestop-policy.sh | |
| bash scripts/tests/test-longhorn-registrar-prestop-policy.sh | |
| shellcheck scripts/tests/test-csi-sidecar-baseline-context.sh | |
| bash scripts/tests/test-csi-sidecar-baseline-context.sh | |
| shellcheck scripts/tests/test-longhorn-csi-plugin-baseline-context.sh | |
| bash scripts/tests/test-longhorn-csi-plugin-baseline-context.sh | |
| shellcheck scripts/tests/test-longhorn-engine-image-baseline-context.sh | |
| bash scripts/tests/test-longhorn-engine-image-baseline-context.sh | |
| shellcheck scripts/tests/test-hcloud-baseline-worker-affinity.sh | |
| bash scripts/tests/test-hcloud-baseline-worker-affinity.sh | |
| shellcheck scripts/guard-longhorn-ui-baseline-context.sh scripts/tests/test-guard-longhorn-ui-baseline-context.sh scripts/tests/test-longhorn-ui-baseline-context.sh | |
| bash scripts/tests/test-guard-longhorn-ui-baseline-context.sh | |
| bash scripts/tests/test-longhorn-ui-baseline-context.sh | |
| shellcheck scripts/tests/test-cilium-hubble-baseline-context.sh | |
| bash scripts/tests/test-cilium-hubble-baseline-context.sh | |
| shellcheck scripts/guard-coroot-baseline-context.sh scripts/tests/test-guard-coroot-baseline-context.sh | |
| bash scripts/tests/test-guard-coroot-baseline-context.sh | |
| shellcheck scripts/admit-coroot-baseline-context.sh scripts/tests/test-admit-coroot-baseline-context.sh | |
| bash scripts/tests/test-admit-coroot-baseline-context.sh | |
| - name: 🛡️ Validate chart-rendered RBAC | |
| if: needs.changes.outputs.k8s == 'true' | |
| # Pulls the charts whose roles the privileged-RBAC policy excludes and applies | |
| # the policy to their rendered RBAC (#3792). It reaches five upstream chart | |
| # registries, so it is bounded like the registry-facing production render: a | |
| # stalled registry fails this step instead of holding the whole job. | |
| timeout-minutes: 10 | |
| run: | | |
| shellcheck scripts/tests/test-chart-rendered-rbac-policy.sh | |
| bash scripts/tests/test-chart-rendered-rbac-policy.sh | |
| - name: 🚦 Validate default-off Cilium bandwidth manager | |
| if: needs.changes.outputs.k8s == 'true' | |
| # Render the committed and temporary opt-in states so the staged | |
| # bandwidth-manager + BBR component cannot silently become active or | |
| # regress the Hetzner WireGuard settings. Bash plus the kubectl bundled | |
| # on the GitHub-hosted runner; no cluster or secrets required. | |
| run: bash scripts/tests/test-cilium-bandwidth-manager-component.sh | |
| - name: 🔏 Validate image-verifier liveness checker | |
| if: needs.changes.outputs.k8s == 'true' | |
| # The checker asserts that containerd's image-verifier can enforce AT | |
| # ALL (#2856). It runs here, inside the required job, rather than in its | |
| # own workflow: a check that gates nothing is advisory, and this one | |
| # guards a control whose failure mode is silence. Fakes talosctl and | |
| # kubectl from fixtures — no cluster, no secrets. The second test pins | |
| # the liveness workflow's daily schedule, so it cannot be switched off | |
| # silently (#3336); it reads the workflow with yq and needs neither. | |
| run: | | |
| shellcheck scripts/validate-image-verifier-liveness.sh scripts/tests/test-validate-image-verifier-liveness.sh | |
| shellcheck scripts/tests/test-validate-image-verifier-liveness-workflow.sh | |
| bash scripts/tests/test-validate-image-verifier-liveness.sh | |
| bash scripts/tests/test-validate-image-verifier-liveness-workflow.sh | |
| - name: 🔏 Validate Coroot compatibility-image verifier prerequisite | |
| if: needs.changes.outputs.k8s == 'true' | |
| run: | | |
| shellcheck scripts/tests/test-coroot-image-verifier-prerequisite.sh | |
| bash scripts/tests/test-coroot-image-verifier-prerequisite.sh | |
| - name: 🔏 Validate image-signature enforcement probe | |
| if: needs.changes.outputs.k8s == 'true' | |
| # The probe itself needs a cluster and WRITES to a node, so it never | |
| # runs here. Its own test does not: talosctl is faked from fixtures, so | |
| # the 20 cases run in the required job like the liveness checker's. | |
| # | |
| # It gates a merge for a sharper reason than most tests. The probe | |
| # reports on supply-chain enforcement and both of its possible mistakes | |
| # are silent: a PASS where a cached image meant nothing was verified, | |
| # and a FAIL where the refusal was merely ambiguous. A regression in | |
| # either direction would be invisible, so an advisory check is not | |
| # enough. | |
| run: | | |
| shellcheck scripts/probe-image-signature-enforcement.sh scripts/tests/test-probe-image-signature-enforcement.sh | |
| bash scripts/tests/test-probe-image-signature-enforcement.sh | |
| - name: 🔎 Validate Cilium ExternalAuth ipcache diagnostic | |
| if: needs.changes.outputs.k8s == 'true' | |
| # The diagnostic needs prod and runs only on dispatch. Its test fakes | |
| # kubectl from fixtures, and pins what makes dispatching it safe: only | |
| # `get` plus one exact `exec cilium-dbg bpf ipcache list`, and no address | |
| # in the public log. Both conclusive verdicts carry a one-fixture control. | |
| run: | | |
| shellcheck scripts/diagnose-cilium-ext-authz-ipcache.sh scripts/tests/test-diagnose-cilium-ext-authz-ipcache.sh | |
| bash scripts/tests/test-diagnose-cilium-ext-authz-ipcache.sh | |
| - name: 🧪 Validate Cilium ExternalAuth cross-node probe | |
| if: needs.changes.outputs.k8s == 'true' | |
| # The probe needs prod and runs only on dispatch, and unlike the ipcache | |
| # diagnostic it WRITES while it runs. Its test fakes kubectl from fixtures | |
| # and pins what makes dispatching it acceptable: no exec/patch and only | |
| # run-scoped deletes, cleanup whenever anything was created, nothing created | |
| # over leftovers, pods pinned to the right nodes, no address in the public | |
| # log, and each conclusive verdict against a one-fixture control. | |
| run: | | |
| shellcheck scripts/probe-cilium-externalauth-crossnode.sh scripts/tests/test-probe-cilium-externalauth-crossnode.sh | |
| bash scripts/tests/test-probe-cilium-externalauth-crossnode.sh | |
| - name: 🔏 Validate GitHub Team external-identity probe | |
| if: needs.changes.outputs.k8s == 'true' | |
| # The probe needs prod and runs only on dispatch. Its test fakes kubectl | |
| # from fixtures and pins what makes dispatching it acceptable: only `get` | |
| # plus `patch`/`create` with `--dry-run=server`, a refusal counted only | |
| # when it names the policy, and no identity in the public log. | |
| run: | | |
| shellcheck scripts/probe-github-team-external-identity.sh scripts/tests/test-probe-github-team-external-identity.sh | |
| bash scripts/tests/test-probe-github-team-external-identity.sh | |
| - name: 🪪 Validate Actual Budget user-namespace map read | |
| if: needs.changes.outputs.k8s == 'true' | |
| # The read needs prod and runs only on dispatch. Its test fakes kubectl | |
| # from fixtures and pins what makes dispatching it acceptable: one `get` | |
| # and four fixed `exec … cat` reads, an unreadable or unparsable map | |
| # never reported as mapped, and no node-side id in the public log. | |
| run: | | |
| shellcheck scripts/read-actual-budget-userns-maps.sh scripts/tests/test-read-actual-budget-userns-maps.sh | |
| bash scripts/tests/test-read-actual-budget-userns-maps.sh | |
| - name: 📏 Validate datapath CPU throttling measurement | |
| if: needs.changes.outputs.k8s == 'true' | |
| # The measurement needs prod and runs only on dispatch. Its test fakes kubectl | |
| # from fixtures and pins the ratio arithmetic, every INCONCLUSIVE path, that it | |
| # issues reads only, that no node or pod name reaches the public log, and the | |
| # workflow's guards. | |
| run: | | |
| shellcheck scripts/measure-datapath-cpu-throttling.sh scripts/tests/test-measure-datapath-cpu-throttling.sh | |
| bash scripts/tests/test-measure-datapath-cpu-throttling.sh | |
| - name: 🔐 Validate Cilium mutual-auth policy scope | |
| if: needs.changes.outputs.k8s == 'true' | |
| # An authentication stanza is still an allow rule. Reject a cluster-wide | |
| # empty fromEndpoints selector before it can bypass workload allow-lists. | |
| run: | | |
| bash scripts/tests/test-cilium-mutual-auth-policy-regressions.sh | |
| bash scripts/tests/test-cilium-mutual-auth-policy.sh | |
| - name: 🔐 Validate workload metadata egress isolation | |
| if: needs.changes.outputs.k8s == 'true' | |
| # Render both provider infrastructure entry points. The control must | |
| # preserve the exact CSI node exception and deny other workloads without | |
| # selecting Talos hosts or leaking into the local Docker provider. | |
| run: | | |
| shellcheck scripts/tests/test-cilium-metadata-egress-policy.sh | |
| bash scripts/tests/test-cilium-metadata-egress-policy.sh | |
| - name: 🔐 Validate no clusterwide Cilium policy silently default-denies | |
| if: needs.changes.outputs.k8s == 'true' | |
| # Cilium turns on default-deny for every direction a policy has rules in, | |
| # deny rules included, so a deny-only clusterwide policy revokes ALL traffic | |
| # in that direction — the mechanism behind the 2026-08-27 DNS outage | |
| # (#3404, #3407). The per-policy test above checks one document's shape; | |
| # this renders every Flux entrypoint and requires each deny-only direction | |
| # of every clusterwide policy to state enableDefaultDeny explicitly (#3408). | |
| run: | | |
| shellcheck scripts/guard-cilium-clusterwide-default-deny.sh scripts/tests/test-guard-cilium-clusterwide-default-deny.sh | |
| bash scripts/guard-cilium-clusterwide-default-deny.sh . | |
| bash scripts/tests/test-guard-cilium-clusterwide-default-deny.sh | |
| - name: 🐘 Validate Coroot Postgres scrape policy | |
| if: needs.changes.outputs.k8s == 'true' | |
| # Schema validation cannot detect a valid selector that matches no pod. | |
| # Pin both cross-namespace policies to the operator's generated labels. | |
| run: | | |
| shellcheck scripts/tests/test-coroot-postgres-scrape-policy.sh | |
| bash scripts/tests/test-coroot-postgres-scrape-policy.sh | |
| shellcheck scripts/tests/test-crossview-coroot-postgres-integration.sh | |
| bash scripts/tests/test-crossview-coroot-postgres-integration.sh | |
| - name: 💾 Validate Crossview PostgreSQL durable storage | |
| if: needs.changes.outputs.k8s == 'true' | |
| # Render the pinned chart through Flux's production post-renderers so | |
| # persistence cannot regress to an emptyDir or an overlapping rollout. | |
| run: | | |
| shellcheck scripts/tests/test-crossview-postgres-durable-storage.sh | |
| bash scripts/tests/test-crossview-postgres-durable-storage.sh | |
| - name: 🔓 Validate the Crossview login alert | |
| if: needs.changes.outputs.k8s == 'true' | |
| # Crossview's only probe never touches its database, so an empty schema | |
| # broke every sign-in for six days with the pod Ready (#3315). This pins | |
| # the sensor's verdicts and wiring against stubbed answers. | |
| run: | | |
| shellcheck scripts/tests/test-crossview-login-alerter.sh scripts/tests/test-crossview-login-alerter-runtime.sh | |
| bash scripts/tests/test-crossview-login-alerter.sh | |
| - name: 🔓 Reproduce the Crossview login outage | |
| if: needs.changes.outputs.crossview == 'true' | |
| # Measured at about 30s; the bound only stops a hung pull holding the job. | |
| timeout-minutes: 15 | |
| # Runs the pinned app and database in Docker, empties the database | |
| # under the running app and runs the deployed sensor against it, so a | |
| # chart bump that changes what /api/auth/check reports fails here. Only | |
| # Crossview changes pay for the image pulls. | |
| run: bash scripts/tests/test-crossview-login-alerter-runtime.sh | |
| - name: 🔑 Validate Coroot API key and registry egress | |
| if: needs.changes.outputs.k8s == 'true' | |
| # Schema validation accepts a Coroot resource whose agents carry no API | |
| # key. Pin agents, project and audit forwarder to one Secret, and keep | |
| # the operator's registry discovery egress exact. | |
| run: | | |
| shellcheck scripts/tests/test-coroot-api-key-contract.sh | |
| bash scripts/tests/test-coroot-api-key-contract.sh | |
| - name: 🛡️ Validate actionable Coroot availability remediations | |
| if: needs.changes.outputs.k8s == 'true' | |
| # Pin the cost-aware decisions that are safe to express declaratively: | |
| # clustered security alerting, leader-elected snapshot failover, and an | |
| # actually disabled Origin CA controller. Intentional singletons are | |
| # dispositioned in platform#3812 instead of being scaled blindly. | |
| run: | | |
| shellcheck scripts/tests/test-coroot-availability-remediation.sh | |
| bash scripts/tests/test-coroot-availability-remediation.sh | |
| shellcheck scripts/tests/test-backstage-retirement.sh | |
| bash scripts/tests/test-backstage-retirement.sh | |
| shellcheck scripts/tests/test-descheduler-availability.sh | |
| bash scripts/tests/test-descheduler-availability.sh | |
| shellcheck scripts/tests/test-umami-rendered-placement.sh | |
| bash scripts/tests/test-umami-rendered-placement.sh | |
| shellcheck scripts/tests/test-coroot-risk-acceptance-reconciler.sh | |
| bash scripts/tests/test-coroot-risk-acceptance-reconciler.sh | |
| - name: 🌐 Validate active Cilium device selection | |
| if: needs.changes.outputs.k8s == 'true' || needs.changes.outputs.bridge_validation == 'true' | |
| # Pin the production activation, merge order, and OnDelete rollout gate | |
| # so a later cleanup cannot silently restore private-only devices or | |
| # trigger an automatic fleet-wide Cilium restart. | |
| run: | | |
| bash scripts/tests/test-cilium-homogeneous-devices-activation.sh | |
| bash scripts/tests/test-cilium-homogeneous-devices-autoscaler-gate.sh | |
| bash scripts/tests/test-cilium-homogeneous-devices-flux-wait.sh | |
| bash scripts/tests/test-cilium-ebpf-host-routing-activation.sh | |
| - name: 🔁 Validate the deploy tolerates a control-plane restart | |
| if: needs.changes.outputs.k8s == 'true' | |
| # A change to the Flux control plane restarts the controllers the deploy's | |
| # own reconcile runs through, cancelling it (#3478). Pin that the deploy | |
| # retries that self-inflicted failure exactly once — and, in the other | |
| # direction, that a genuine reconcile failure is still never retried. | |
| run: bash scripts/tests/test-flux-reconcile-control-plane-restart.sh | |
| - name: 🧢 Validate the DR rebuild caps Talos-bootstrapped CoreDNS | |
| if: needs.changes.outputs.k8s == 'true' | |
| # Talos admits CoreDNS before the kube-system LimitRange exists, so a fresh | |
| # prod cluster runs it without a CPU limit (#3567). Pin that the rebuild | |
| # restarts CoreDNS only when a running pod is uncapped, restarts nothing | |
| # else, and fails closed on any input it cannot read. | |
| run: bash scripts/tests/test-restart-uncapped-talos-coredns.sh | |
| - name: 🛡️ Validate the deploy repairs a bad root-source verify | |
| if: needs.changes.outputs.k8s == 'true' | |
| # The root source's cosign matcher is delivered through that same source, | |
| # so only an out-of-band repair can replace a bad live value (#3014). Pin | |
| # that the deploy repairs it through the FluxInstance, writes nothing on a | |
| # healthy cluster, and fails closed on anything it cannot read. | |
| run: bash scripts/tests/test-reassert-flux-root-verify.sh | |
| - name: 🧢 Validate kube-system add-ons declare their CPU limit | |
| if: needs.changes.outputs.k8s == 'true' | |
| # The kube-system LimitRange only reaches pods admitted after it exists, so the | |
| # non-datapath add-ons carry their CPU limit in their own chart values (#3789). | |
| # Pin the exact CPU request next to every limit: the static prod VPAs keep the | |
| # ratio, and a limit alone, or a request raised to the limit, would let the VPA | |
| # throttle the add-on. Also pin that the Cilium agent and Envoy DaemonSets | |
| # stay unlimited here. | |
| run: bash scripts/tests/test-kube-system-addon-cpu-limits.sh | |
| - name: 📣 Validate the rollout gate cannot suppress sync silently | |
| if: needs.changes.outputs.k8s == 'true' | |
| # The gate skips the pipeline's only Talos machine-config sync. Pin that | |
| # it can never do so without a job-summary notice, that the suppression | |
| # carries a declared start date, and that the bound past which it warns | |
| # is real — a skipped step in a green deploy is otherwise invisible. | |
| run: bash scripts/tests/test-cilium-rollout-gate-suppression-signal.sh | |
| - name: 📊 Validate Coroot cost convergence | |
| if: needs.changes.outputs.k8s == 'true' | |
| # Coroot is the single cost surface. Render both infrastructure overlays | |
| # so OpenCost cannot return through a route, workload, policy or optional | |
| # local integration, while the production pricing reconciler stays live. | |
| run: bash scripts/tests/test-coroot-cost-convergence.sh | |
| - name: 🔄 Validate Crossplane sync exporter metrics path | |
| if: needs.changes.outputs.k8s == 'true' | |
| # A wrong GVK, a renamed series, or a scrape target that drifts from the | |
| # kube-state-metrics listen address all export nothing — and no series | |
| # reads exactly like a fleet with nothing wrong. Schema validation cannot | |
| # see any of it, so render the component contract instead. | |
| run: bash scripts/tests/test-crossplane-sync-exporter.sh | |
| - name: 📡 Validate Crossplane sync alert delivery | |
| if: needs.changes.outputs.k8s == 'true' | |
| run: bash scripts/tests/test-crossplane-sync-alerter.sh | |
| - name: 🖥️ Validate the stranded autoscaler node alert | |
| if: needs.changes.outputs.k8s == 'true' | |
| # Its declared pools must equal ksail.prod.yaml's, so removing a pool | |
| # cannot leave that pool's nodes unreported (#3178). | |
| run: bash scripts/tests/test-stranded-autoscaler-node-alerter.sh | |
| - name: 🩺 Validate the CNPG degraded-cluster alert | |
| if: needs.changes.outputs.k8s == 'true' | |
| # This CronJob is the only signal for a database degraded without | |
| # crashlooping, including a Ready cluster whose continuous WAL archive | |
| # is failing. A broken check prints the same "none degraded" line as a | |
| # healthy fleet. Run its real script against a stubbed API and delivery | |
| # so both silent-zero guards and the credential's handling are pinned by | |
| # behaviour rather than by schema. | |
| run: bash scripts/tests/test-cnpg-degraded-alert.sh | |
| - name: 🩺 Validate the stranded-volume-attach alert | |
| if: needs.changes.outputs.k8s == 'true' | |
| # The only direct signal for the 2026-07-01 shape — a volume stranded on | |
| # a departed node, logged by the rescheduled pod for nine hours while | |
| # nothing alerted (#2363, #2754). Replays that shape and every ordinary | |
| # shape against a stubbed API and delivery, so the fire, each quiet | |
| # verdict and both silent-zero guards are pinned by behaviour. | |
| run: bash scripts/tests/test-stranded-volume-attach-alert.sh | |
| - name: 🧹 Validate the prune-protected-orphan alert | |
| if: needs.changes.outputs.k8s == 'true' | |
| # The only signal that a retirement's manual delete step was missed — | |
| # tofu-controller kept running, unpatchable, for 74 days (#3480, #3503). | |
| # Replays a young and a stale left-behind resource, a vanished | |
| # Kustomization, every quiet shape and every silent-zero guard against | |
| # a stubbed API, state record and delivery. | |
| run: | | |
| shellcheck scripts/tests/test-prune-protected-orphan-alert.sh | |
| bash scripts/tests/test-prune-protected-orphan-alert.sh | |
| - name: 🔕 Validate narrow Coroot alert suppressions | |
| if: needs.changes.outputs.k8s == 'true' | |
| # Exercise the rendered script: exact by-design network and lifecycle | |
| # signals suppress, while active extra upstreams and message/label near | |
| # matches remain visible. | |
| run: | | |
| shellcheck scripts/tests/test-coroot-alert-autosuppressor.sh | |
| bash scripts/tests/test-coroot-alert-autosuppressor.sh | |
| - name: 🩺 Validate declarative Coroot application health | |
| if: needs.changes.outputs.k8s == 'true' | |
| # Exact reviewed app signals receive per-app thresholds; any novel log | |
| # pattern resets its threshold so the Applications view fails visible. | |
| run: | | |
| shellcheck scripts/tests/test-coroot-application-health-reconciler.sh | |
| bash scripts/tests/test-coroot-application-health-reconciler.sh | |
| - name: 📦 Validate Coroot helper image caching | |
| if: needs.changes.outputs.k8s == 'true' | |
| # A pinned digest on a warm node must not depend on Docker Hub token | |
| # issuance for every scheduled Coroot maintenance run. | |
| run: | | |
| shellcheck scripts/tests/test-coroot-helper-image-pull-policy.sh | |
| bash scripts/tests/test-coroot-helper-image-pull-policy.sh | |
| - name: 💾 Validate the Velero maintenance OOM alert | |
| if: needs.changes.outputs.k8s == 'true' | |
| # Replays an OOMKilled attempt plus a later successful retry against the | |
| # manifest's real script so BackupRepository readiness cannot mask it. | |
| run: bash scripts/tests/test-velero-maintenance-oom-alert.sh | |
| - name: 🔁 Validate the reconcile-CronJob failure alert | |
| if: needs.changes.outputs.k8s == 'true' | |
| # A self-healing CronJob that fails on every tick was visible only as | |
| # retained Failed Jobs (#2915). Replays consecutive failures, a single | |
| # absorbed failure, a stalled loop and every API failure against the | |
| # manifest's real script, under bash and POSIX sh. | |
| run: | | |
| shellcheck scripts/tests/test-cronjob-failure-alert.sh | |
| bash scripts/tests/test-cronjob-failure-alert.sh | |
| - name: 💾 Validate Velero data-mover resources | |
| if: needs.changes.outputs.k8s == 'true' | |
| # Pin the short-lived Kopia mover's authored memory ceiling and bind | |
| # the node-agent pod generation to its external ConfigMap. Otherwise a | |
| # correct resource fix can remain unloaded forever on stable workers. | |
| run: | | |
| shellcheck scripts/tests/test-velero-data-mover-resources.sh | |
| bash scripts/tests/test-velero-data-mover-resources.sh | |
| - name: 💾 Validate the released Velero cancellation fix | |
| if: needs.changes.outputs.k8s == 'true' | |
| run: | | |
| shellcheck scripts/tests/test-velero-cancellation-release.sh | |
| bash scripts/tests/test-velero-cancellation-release.sh | |
| - name: 💾 Validate node-local backup agent admission priority | |
| if: needs.changes.outputs.k8s == 'true' | |
| run: | | |
| shellcheck scripts/tests/test-velero-node-agent-priority.sh | |
| bash scripts/tests/test-velero-node-agent-priority.sh | |
| - name: 🔄 Validate the Coroot reservation refresh | |
| if: needs.changes.outputs.k8s == 'true' | |
| run: | | |
| shellcheck scripts/tests/test-coroot-reservation-refresh.sh | |
| bash scripts/tests/test-coroot-reservation-refresh.sh | |
| - name: 🔑 Validate the MinIO bucket Job's credential handling | |
| if: needs.changes.outputs.k8s == 'true' | |
| # checkov CKV_K8S_35 was dispositioned here by moving the root | |
| # credential out of the environment and into a mounted file. That | |
| # only holds while the Job imports the file, keeps the credential | |
| # out of argv, and waits on a command that actually reaches MinIO — | |
| # none of which schema validation can see. | |
| run: bash scripts/tests/test-minio-create-bucket-credentials.sh | |
| - name: 🧭 Validate the DR rebuild supersession gate | |
| if: needs.changes.outputs.k8s == 'true' | |
| # concurrency alone cannot stop a pending DR dispatch from rebuilding on | |
| # top of the rebuild that just recovered prod. The guard supplies that, | |
| # and this pins both its decision and the wiring that reaches it — a | |
| # guard the destructive job does not depend on protects nothing. | |
| run: bash scripts/tests/test-dr-rebuild-supersession-guard.sh | |
| - name: 🧯 Validate the control-plane recreate drill preflight | |
| if: needs.changes.outputs.k8s == 'true' | |
| # The drill deletes a production control-plane node. This pins the | |
| # read-only decision that must say ready before it starts: each guard | |
| # (target, quorum, backup) refuses on its own fixture, and bad input is | |
| # never read as ready. | |
| run: | | |
| shellcheck scripts/cp-recreate-drill-preflight.sh scripts/tests/test-cp-recreate-drill-preflight.sh | |
| bash scripts/tests/test-cp-recreate-drill-preflight.sh | |
| - name: 🔍 Validate the published-evidence gate | |
| if: needs.changes.outputs.k8s == 'true' | |
| # Every publication step can report success while leaving no usable | |
| # evidence in the registry, and the promotion that follows is what | |
| # production starts following within the minute. This pins the gate's | |
| # verdict in both rollout states and the wiring that reaches it — a gate | |
| # that runs after latest already moved has verified nothing that matters. | |
| run: | | |
| bash scripts/tests/test-publication-subject.sh | |
| SUBJECT_NAME="$(yq -r '.runs.steps[] | select(.id == "staging_reference").env.SUBJECT_NAME' .github/actions/deploy-prod/publish-platform-manifests/action.yml)" | |
| export SUBJECT_NAME | |
| bash scripts/tests/test-verify-published-evidence.sh | |
| - name: 🔏 Validate the matcher-accepts-staged-digest gate | |
| if: needs.changes.outputs.k8s == 'true' | |
| # The matcher-efficacy job asks the same question of the MUTABLE tag, which | |
| # at promotion time still names the PREVIOUSLY published artifact — so it | |
| # cannot see a change to the publishing identity itself (#3289). This pins | |
| # the gate that asks it of the staged digest instead, including its negative | |
| # control and the liveness sandwich that stops an outage being counted as a | |
| # refusal, the wording that keeps an outage from being reported as a matcher | |
| # finding (#3545), plus the wiring that puts it before the promotion. | |
| run: bash scripts/tests/test-verify-matcher-accepts-digest.sh | |
| - name: 🔐 Validate the shared-publish-workflow pin guard | |
| if: needs.changes.outputs.k8s == 'true' | |
| # The guard fails by PASSING: a widened cosign matcher still verifies, so | |
| # nothing else in CI notices. This pins the shapes it must reject — above | |
| # all the SHA-or-tag alternation removed in #3022, which is the exact | |
| # prior contents of all eight subjects and would otherwise revert in | |
| # silence — plus the floor and the wiring that reaches it. | |
| run: bash scripts/tests/test-shared-publish-workflow-pin-guard.sh | |
| - name: 🧷 Validate checkov skip reasons are quoted and intact | |
| if: needs.changes.outputs.k8s == 'true' | |
| # A `checkov.io/skipN` whose reason parses away still suppresses its | |
| # finding, because the check id sits before the `#`. The suppression then | |
| # reads as reviewed while asserting nothing, and no other check in CI can | |
| # see it. The guard removes that failure rather than detecting it: every skip | |
| # value must be an explicitly QUOTED scalar, because a quoted `#` is literal. | |
| # (Detecting truncation from source text is impossible — a complete reason plus | |
| # a trailing comment is lexically identical to a truncated one.) This runs the | |
| # guard over the tree and pins its verdict in all three directions — pass, | |
| # reject, and cannot-check — because a guard that has only ever seen passing | |
| # input has proved nothing about what it rejects. | |
| run: | | |
| bash scripts/guard-checkov-skip-reasons.sh k8s | |
| bash scripts/tests/test-guard-checkov-skip-reasons.sh | |
| - name: 📌 Validate every GitRepository is pinned to a commit | |
| if: needs.changes.outputs.k8s == 'true' | |
| # A GitRepository naming a mutable ref reconciles whatever the branch tip is, | |
| # so the trust boundary becomes the source repository's own enforcement. | |
| # #2707 closed that on the unifi source by pinning the full commit, which fixes | |
| # the content cryptographically and makes moving it a reviewed Platform change. | |
| # That mitigation lived in one YAML file with nothing asserting it, so reverting | |
| # it — deliberately or as collateral — would silently restore the exposure with | |
| # nothing in CI noticing (#3124). This runs the guard over the tree and pins its | |
| # verdict in all three directions, including the anti-vacuity case: finding NO | |
| # GitRepository at all is cannot-check, never a clean pass. | |
| run: | | |
| bash scripts/guard-gitrepository-commit-pin.sh k8s | |
| bash scripts/tests/test-guard-gitrepository-commit-pin.sh | |
| - name: 🌐 Validate no render path fetches over the network | |
| if: needs.changes.outputs.k8s == 'true' | |
| # Two entries in the prod controllers render fetch third-party manifests at | |
| # render time, one of them from Cloudflare's `trunk` BRANCH. That makes the | |
| # render irreproducible (the same commit can yield two different rendered | |
| # surfaces), puts unreviewed third-party content into the prod authorization | |
| # surface the EKS CI role validator exists to pin, and makes a required | |
| # security gate hostage to a CDN — which is how it was found, when | |
| # raw.githubusercontent.com returned 429 and took four authorization tests | |
| # down on clean `main` (#3196). | |
| # | |
| # The two known entries carry rows in the reviewed exceptions list, so this | |
| # runs green today and a THIRD one fails the build. A row must name a | |
| # tracking issue, and a row whose URL is no longer referenced is itself a | |
| # violation — so the list cannot quietly become the place remotes accumulate. | |
| run: | | |
| bash scripts/guard-render-remote-resources.sh k8s | |
| bash scripts/tests/test-guard-render-remote-resources.sh | |
| - name: 🔗 Validate the pod-security exception still matches its mutation's scope | |
| if: needs.changes.outputs.k8s == 'true' | |
| # The exception suppresses C-0016/C-0055 everywhere EXCEPT the twelve | |
| # namespaces its compensating mutation does not reach. #2824 happened because | |
| # those two scopes drifted apart in silence, and the narrowing that fixed it | |
| # left the agreement resting on a code comment. Both directions of drift read | |
| # as compliance: a namespace added to the mutation's exclusions and not the | |
| # exception re-creates the original defect, and one removed from the | |
| # exclusions but left in the exception enforces controls where the mutation | |
| # now runs, producing findings that are not real gaps. | |
| # | |
| # The comparison is NOT against the exclusion list: add-pod-security-context | |
| # excludes fourteen namespaces, and two of them (cert-manager and keda) | |
| # are reached by the by-name add-imageuid rule. Reading exclusions | |
| # alone reports two false differences on a correct tree, and the natural way | |
| # to silence that is to add them to the exception — the very drift this | |
| # catches (#3224). | |
| run: | | |
| bash scripts/guard-pod-security-exception-scope.sh | |
| bash scripts/tests/test-guard-pod-security-exception-scope.sh | |
| - name: 🔬 Validate every unscanned baseline-context opt-in declares it | |
| if: needs.changes.outputs.k8s == 'true' | |
| # The per-namespace baseline-context rollout has a documented read-back step: | |
| # opt the namespace in, then confirm the posture signal shows C-0211's residual | |
| # dropping. For a namespace Kubescape never scans, that step cannot be performed | |
| # and does not fail — it comes back clean, which reads as validation. | |
| # | |
| # `kubescape` is opted in and sits inside the operator's own excludeNamespaces. | |
| # Before #3925 its opt-in comment read exactly like the three scanned namespaces' | |
| # comments, with the caveat three files away in helm-release.yaml, about a | |
| # different change. #3919 was written on the same missing fact — that labelling | |
| # kube-system would move a verdict that is never produced — and settling it took | |
| # a live-cluster measurement. This makes the consequence checkable at the opt-in. | |
| # | |
| # Scanned opt-ins need no declaration, so the guard compares the intersection | |
| # rather than the label alone; both inputs are required non-empty first, because | |
| # an empty set compared against an empty set passes vacuously forever. | |
| run: | | |
| bash scripts/guard-baseline-context-optin-scan-scope.sh | |
| bash scripts/tests/test-guard-baseline-context-optin-scan-scope.sh | |
| - name: 🗺️ Validate every unscanned namespace is reviewed | |
| if: needs.changes.outputs.k8s == 'true' | |
| # A namespace in the operator's excludeNamespaces produces no scan result, and the | |
| # posture score cannot tell that apart from a clean namespace. So the exclusions must | |
| # match scripts/kubescape-unscanned-namespaces.tsv exactly, each row with a reason, | |
| # and a namespace can only leave scan coverage through a reviewed change (#3215). | |
| # The base HelmRelease is the only place the field may be set: an overlay patch or a | |
| # valuesFrom that sets it would narrow the deployed scope while this read the base. | |
| run: | | |
| bash scripts/guard-kubescape-scan-scope.sh | |
| bash scripts/tests/test-guard-kubescape-scan-scope.sh | |
| - name: 🧮 Test the Kubescape result-coverage check | |
| if: needs.changes.outputs.k8s == 'true' | |
| # The check compares live Kubescape results against the objects that should have one | |
| # and fails on a missing or stale result (#4262). It needs a cluster, so CI runs only | |
| # its fixture test, including the negative control: one expected result removed from a | |
| # passing set must fail, and an empty or short read must be UNKNOWN, never clean. | |
| run: bash scripts/tests/test-check-kubescape-result-coverage.sh | |
| - name: 👻 Test the Kubescape orphaned-result report | |
| if: needs.changes.outputs.k8s == 'true' | |
| # The report compares every stored posture result against the live list of its kind | |
| # and names the results whose object is gone (#3697). It needs a cluster, so CI runs | |
| # only its fixture test: a result whose object exists is never orphaned, a refused, | |
| # partial or malformed read ends as UNKNOWN rather than as a pass, kubectl is only | |
| # ever asked to read, and no object name reaches the report. | |
| run: | | |
| shellcheck scripts/report-kubescape-scan-orphans.sh scripts/tests/test-report-kubescape-scan-orphans.sh | |
| bash scripts/tests/test-report-kubescape-scan-orphans.sh | |
| - name: 🔎 Validate every suppressed control is documented as failing open | |
| if: needs.changes.outputs.k8s == 'true' | |
| # A control added under spec.posture that the file's fail-open warning does | |
| # not name. The exception makes each suppressed control report `passed` with | |
| # subStatus "w/exceptions", so reading status alone yields a clean result over | |
| # an unfixed population — 35 of 35 workloads when C-0211 was added (#3516). | |
| # The check is scoped to the marked warning block, not the whole header: when | |
| # C-0211 was added it WAS mentioned elsewhere in that header, so a file-wide | |
| # match passes on the very tree that motivated the guard. | |
| run: | | |
| bash scripts/guard-pod-security-exception-documented.sh | |
| bash scripts/tests/test-guard-pod-security-exception-documented.sh | |
| - name: 🧬 Validate Crossplane provider names match their derived names | |
| if: needs.changes.outputs.k8s == 'true' | |
| # A Crossplane Provider declared under a shortened alias instead of the name | |
| # Crossplane derives from its package (`<org>-<repo>`) is invisible until | |
| # something depends on it — then the dependency manager, which resolves by | |
| # derived NAME while the Lock is keyed by package SOURCE, creates a SECOND | |
| # Provider for a source already present. Building the dependency graph then | |
| # fails with `node <source> already exists`, which marks EVERY provider | |
| # Unhealthy and stops all Crossplane reconciliation, not just the duplicated | |
| # one. That is #3454: it went live 21 seconds into the 2.3.4 -> 2.4.0 bump and | |
| # ran for 15 hours, because nothing in GitHub can see a dead provider fleet. | |
| # | |
| # The same guard pins the other half, which the rename itself breaks: provider | |
| # RBAC is named after the revision (`crossplane:provider:<name>-<hash>:system`), | |
| # so two Kubescape exception lists match the provider name by anchored regex. | |
| # Renaming without updating both fails nothing — the manifests build, the | |
| # provider runs, and a C-0015 finding just reappears days later. Those two | |
| # lists are synced by hand, so one-updated-one-forgotten is the realistic drift. | |
| run: | | |
| bash scripts/guard-crossplane-provider-name-parity.sh k8s | |
| bash scripts/tests/test-guard-crossplane-provider-name-parity.sh | |
| - name: 📌 Validate unverified Crossplane providers are digest-pinned | |
| if: needs.changes.outputs.k8s == 'true' | |
| # Only first-party and Upbound provider packages have a signature check. | |
| # Any other publisher's package must be pinned by digest, so the bytes that | |
| # were reviewed are the bytes the cluster installs (#4189). | |
| run: | | |
| bash scripts/guard-unverified-provider-digest-pin.sh k8s | |
| bash scripts/tests/test-guard-unverified-provider-digest-pin.sh | |
| - name: 🧬 Validate the AWS Crossplane family runs one release | |
| if: needs.changes.outputs.k8s == 'true' | |
| # provider-aws-iam depends on provider-family-aws and must run its release. | |
| # Renovate groups their bumps into one PR, but grouping does not force equal | |
| # versions, so a split release would otherwise merge as a version skew. | |
| run: | | |
| bash scripts/tests/test-crossplane-aws-family-version-parity.sh | |
| - name: 🔗 Validate the GHCR fan-out follows the apps-base component gate | |
| if: needs.changes.outputs.k8s == 'true' | |
| # An app that pulls private first-party images is declared twice: enabled | |
| # in k8s/bases/apps/kustomization.yaml, and listed in FANOUT_NAMESPACES so | |
| # the post-reconcile reassertion proves its pull credential arrived. Two | |
| # hand-edited files, one switch, and nothing made them agree. | |
| # | |
| # The expensive direction is staging a component OFF and leaving it | |
| # listed. The pre-publish invocation defers a missing new consumer | |
| # (--record-runtime-proof), but the reassertion in deploy-prod runs with | |
| # --reuse-runtime-proof and has no such exemption, so it marks the | |
| # fan-out incomplete and FAILS EVERY OTHERWISE-SUCCESSFUL DEPLOY. Nothing | |
| # else catches it: the manifests build, CI is green, and the failure only | |
| # appears on the next production deploy. That is exactly what staging the | |
| # data-product-controller off introduced (#3476). | |
| run: | | |
| shellcheck scripts/guard-ghcr-fanout-component-gate.sh | |
| shellcheck scripts/tests/test-guard-ghcr-fanout-component-gate.sh | |
| bash scripts/guard-ghcr-fanout-component-gate.sh | |
| bash scripts/tests/test-guard-ghcr-fanout-component-gate.sh | |
| - name: 📐 Validate LimitRange-premised suppressions actually have a LimitRange | |
| if: needs.changes.outputs.k8s == 'true' | |
| # Several skips here are dispositioned not as accepted risk but as "the check | |
| # is blind to an admission-time default" — sound reasoning that holds only | |
| # where the LimitRange actually applies. The natural justification is that a | |
| # LimitRange is a base resource and so reaches every overlay, which is an | |
| # inference about REACHABILITY that reads as obviously true and is not: an | |
| # overlay that cherry-picks named base directories does not get it. Nothing | |
| # then fails — the suppression keeps suppressing and the workload runs with no | |
| # limit in exactly the overlay whose goal was prod-like admission. The guard | |
| # walks each provider's kustomize graph and pins its verdict in all three | |
| # directions, including the two anti-vacuity cases where a matcher that | |
| # matched nothing would otherwise look like a healthy tree. | |
| run: | | |
| bash scripts/guard-limitrange-premise.sh k8s | |
| bash scripts/tests/test-guard-limitrange-premise.sh | |
| - name: 📋 Validate Kubescape finding summary | |
| if: needs.changes.outputs.k8s == 'true' | |
| # The summary is what makes the CI log and the Security tab agree (#2846), | |
| # and it fails by under-reporting rather than by crashing — a dropped | |
| # control looks exactly like a clean scan. Pin the grouping, the counts and | |
| # the two SARIF shapes that turn into hard jq errors if indexed naively: a | |
| # result with no ruleId, and a ruleId absent from the rule table. | |
| run: bash scripts/tests/test-summarize-sarif-findings.sh | |
| - name: 🧭 Validate SARIF path normalisation | |
| if: needs.changes.outputs.k8s == 'true' | |
| # normalize-sarif-paths.sh is a fail-closed guard, and until now shellcheck | |
| # was its only gate — which cannot see a jq path that was never written. | |
| # That is exactly how fixes[] stayed un-rooted while the guard reported | |
| # success (#2863). Pin the rewrite over BOTH path-bearing fields, plus | |
| # idempotency, the hard error, and the clean-scan case. | |
| run: bash scripts/tests/test-normalize-sarif-paths.sh | |
| - name: 🐘 Validate CNPG operator network boundary | |
| if: needs.changes.outputs.k8s == 'true' | |
| run: go test ./scripts/tests/cnpg-network-policy | |
| - name: 📢 Validate reconciliation-Alert namespace coverage | |
| if: needs.changes.outputs.k8s == 'true' | |
| # notification-controller has no namespace wildcard, so the Alert must list | |
| # every namespace holding a HelmRelease. A namespace missing from that list | |
| # is invisible: the release fails, rolls back, reports Ready, and nothing | |
| # pages (ksail-operator, 2026-07-14). Drift is a build failure, not a | |
| # comment. Go behavioral tests + Bash with the runner's kubectl/yq; no | |
| # cluster or secrets required. | |
| run: | | |
| go test ./scripts/tests/validate-alert-coverage | |
| shellcheck scripts/validate-alert-coverage.sh | |
| bash scripts/validate-alert-coverage.sh | |
| - name: ⚙️ Setup KSail | |
| # Renovate-managed (datasource github-releases; grouped 'ksail' with the | |
| # deploy-prod / dr-rebuild pins). The validate step below renders | |
| # HelmReleases in-process (Kustomize + Helm) so the actually-applied | |
| # manifests are validated. This was briefly non-deterministic — a | |
| # buffer-aliasing data race in kubeconform's resource.FromStream (it yields | |
| # resources aliasing the reused bufio.Scanner buffer, parsed concurrently | |
| # with the producer's next Scan()) corrupted the validated stream on the | |
| # >4MB HelmRelease-dense overlays (ksail#5362) — and was worked around with | |
| # --skip-helm-render (platform PR #2270). KSail v7.163.1 contains the | |
| # ksail#5978 containment: split the multi-document stream and clone each | |
| # document before concurrent validation. Upstream | |
| # yannh/kubeconform#363 is still pending, so require consecutive | |
| # stable-green runs here. TRIPWIRE: if validate/scan swing again, re-add | |
| # --skip-helm-render and reopen ksail#5362. | |
| shell: bash | |
| env: | |
| # renovate: datasource=github-releases depName=devantler-tech/ksail extractVersion=^v(?<version>.+)$ | |
| KSAIL_VERSION: "7.202.58" | |
| GITHUB_TOKEN: ${{ github.token }} | |
| run: | | |
| .github/scripts/setup-ksail.sh | |
| - name: 🔐 Validate ARC listener public-content disposition | |
| if: needs.changes.outputs.k8s == 'true' | |
| # Use the pinned native scanner installed above. The regression must | |
| # prove both admission enforcement and the exact scanner disposition, | |
| # including the unexcepted mirror and genuine-credential controls. | |
| run: | | |
| shellcheck scripts/tests/test-arc-listener-public-content.sh | |
| bash scripts/tests/test-arc-listener-public-content.sh | |
| - name: 📥 Restore kubeconform schema cache | |
| # ksail's kubeconform client fetches JSON schemas from | |
| # raw.githubusercontent.com/yannh/kubernetes-json-schema on every cold run | |
| # and has nowhere to persist them across ephemeral CI runners — heavy, | |
| # repeated traffic across every PR is what tripped upstream 429 | |
| # rate-limiting portfolio-wide (2026-07-09). Restore/save are split (rather | |
| # than the single `actions/cache` step) so the save below can run with | |
| # `if: always()` — the validate step below is exactly what's exercising | |
| # this cache and can itself fail on an uncached/rate-limited schema fetch; | |
| # a save gated on job success would then never persist the schemas it did | |
| # manage to fetch, and every retry would cold-start again. | |
| id: kubeconform-cache | |
| uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: ~/.cache/ksail/kubeconform | |
| key: kubeconform-schema-cache-v1-${{ github.run_id }} | |
| restore-keys: | | |
| kubeconform-schema-cache-v1- | |
| - name: 🔒 Validate Crossplane provider egress policy | |
| if: needs.changes.outputs.k8s == 'true' | |
| # This test exercises KSail's in-process Helm renderer, so it must run | |
| # after the pinned KSail setup and schema-cache restore above. The | |
| # reviewed FQDN set must remain narrow while provider traffic stays on | |
| # the direct L3/L4 path that completes upstream TLS handshakes. | |
| run: bash scripts/tests/test-crossplane-egress-policy.sh | |
| - name: 🧠 Validate Kyverno admission VPA safety | |
| if: needs.changes.outputs.k8s == 'true' | |
| # Render the production VPA and the actual Kyverno chart Deployment so | |
| # right-sizing cannot silently replace the fail-closed admission | |
| # controller's memory limit. KSail is installed above because its CEL | |
| # rules run against Helm-rendered documents, not only the HelmRelease. | |
| run: | | |
| shellcheck scripts/tests/test-kyverno-admission-vpa.sh | |
| bash scripts/tests/test-kyverno-admission-vpa.sh | |
| - name: 📝 Validate VPA updater Event authorization | |
| if: needs.changes.outputs.k8s == 'true' | |
| # The updater emits an InPlaceResizedByVPA Event in the workload's own | |
| # namespace. The chart's create-only grant cannot patch a repeated | |
| # Event, so render both routes and pin the exact cluster-wide write set. | |
| run: | | |
| shellcheck scripts/tests/test-vpa-updater-event-rbac.sh | |
| bash scripts/tests/test-vpa-updater-event-rbac.sh | |
| - name: ✅ Validate manifests (local + prod overlays) | |
| # Schema-aware kubeconform validation with Flux variable substitution, | |
| # building both cluster overlays. Fully static: no cluster, no SOPS key | |
| # (Secrets are skipped); schema fetches are cached by the step above so | |
| # repeat runs make no network calls for already-seen schemas. | |
| # | |
| # This PR deliberately removes the temporary --skip-helm-render | |
| # workaround so the actually-applied Helm-rendered manifests are | |
| # validated again. KSail v7.163.1 carries the ksail#5978 | |
| # stream-splitting containment for ksail#5362 while | |
| # yannh/kubeconform#363 remains open; merge only after this gate has | |
| # consecutive stable-green evidence. | |
| run: | | |
| ksail workload validate | |
| ksail --config ksail.prod.yaml workload validate | |
| - name: 🛡️ Validate Kubescape self-hosted scan persistence | |
| if: needs.changes.outputs.k8s == 'true' | |
| # The in-cluster scanner has no SaaS backend. Versions before v4.0.12 | |
| # can override local-only intent and abort before persisting posture | |
| # results, while the scheduler Job still reports success (#3275). | |
| run: | | |
| shellcheck scripts/tests/test-kubescape-self-hosted-scan-persistence.sh | |
| bash scripts/tests/test-kubescape-self-hosted-scan-persistence.sh | |
| - name: 🧹 Validate Kubescape host-data cleanup | |
| if: needs.changes.outputs.k8s == 'true' | |
| # Autoscaled Nodes disappear without removing node-agent's cluster-scoped | |
| # host-data objects. Prove the cleanup snapshots every inventory before | |
| # deleting only exact records whose Node no longer exists (#3686). | |
| run: | | |
| shellcheck scripts/tests/test-kubescape-hostdata-cleanup.sh | |
| bash scripts/tests/test-kubescape-hostdata-cleanup.sh | |
| - name: 💾 Validate Kubescape storage compatibility image | |
| if: needs.changes.outputs.k8s == 'true' | |
| # Storage v0.0.303+ removes APIs this deployment still serves, while | |
| # v0.0.297 drops contended SQLite writes after its default timeout. | |
| # Keep the narrow compatibility build and its immutable deployment pin | |
| # executable and reviewable until an upstream compatible release exists. | |
| run: | | |
| shellcheck scripts/tests/test-kubescape-storage-hotfix.sh | |
| bash scripts/tests/test-kubescape-storage-hotfix.sh | |
| - name: 🔭 Validate Coroot node-agent compatibility image | |
| if: needs.changes.outputs.k8s == 'true' | |
| # Talos does not expose journald, this cluster blocks its metadata | |
| # endpoint, and stripped Go binaries cannot accept TLS uprobes. Keep | |
| # those optional limitations visible as warnings while increasing the | |
| # L7 event buffer that protects actionable telemetry under bursts. | |
| run: | | |
| shellcheck scripts/tests/test-coroot-node-agent-hotfix.sh | |
| bash scripts/tests/test-coroot-node-agent-hotfix.sh | |
| - name: 🛡️ Validate Kubescape node-agent compatibility image | |
| if: needs.changes.outputs.k8s == 'true' | |
| # The pinned upstream release reports expected short-lived-container | |
| # lifecycle races and Kubernetes optimistic-concurrency conflicts as | |
| # application errors. Keep the narrow severity-classification build, | |
| # its source pin, and its tests executable until upstream supersedes it. | |
| run: | | |
| shellcheck scripts/tests/test-kubescape-node-agent-hotfix.sh | |
| bash scripts/tests/test-kubescape-node-agent-hotfix.sh | |
| - name: 🚨 Validate Coroot alert remediations | |
| if: needs.changes.outputs.k8s == 'true' | |
| run: | | |
| shellcheck scripts/tests/test-coroot-alert-remediation.sh | |
| bash scripts/tests/test-coroot-alert-remediation.sh | |
| shellcheck scripts/tests/test-coroot-dns-resolution.sh | |
| bash scripts/tests/test-coroot-dns-resolution.sh | |
| shellcheck scripts/tests/test-coroot-final-remediation.sh | |
| bash scripts/tests/test-coroot-final-remediation.sh | |
| - name: 📤 Save kubeconform schema cache | |
| # Runs even if the validate step above failed (see the restore step's | |
| # note) so any schemas fetched before a rate-limit hit still persist for | |
| # the next run's restore-keys prefix match. Skipped only on an exact | |
| # primary-key hit (impossible here since the key includes | |
| # `github.run_id`, but keeps the step correct if that ever changes). | |
| if: "!cancelled() && steps.kubeconform-cache.outputs.cache-hit != 'true'" | |
| uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: ~/.cache/ksail/kubeconform | |
| key: ${{ steps.kubeconform-cache.outputs.cache-primary-key }} | |
| - name: 🔎 Scan manifests (Kubescape NSA + MITRE) — hard gate | |
| # Static security scan, gated on the compliance score: the job FAILS if a | |
| # change drops the posture below the threshold, so new findings must be | |
| # fixed (or justified) before merge. | |
| # | |
| # TWO frameworks, because one does not cover the exception surface. The | |
| # ClusterSecurityException CRs name 76 distinct controls; NSA-CISA | |
| # evaluates 17 of them, so 59 excepted controls were outside the gate | |
| # entirely — including every RBAC control the CRs exist to govern, whose | |
| # findings were therefore neither gated nor reported. MITRE ATT&CK brings | |
| # 7 of those under it (measured 2026-08-10): C-0007, C-0015, C-0031, | |
| # C-0037, C-0045, C-0048, C-0053 — of which C-0053, C-0015 and C-0007 | |
| # carry real residual findings. 52 excepted controls remain in neither | |
| # framework; #2823 tracks that residue. | |
| # | |
| # NB the two counts differ: MITRE adds 10 controls to the SCAN (nsa 20, | |
| # mitre 17, union 30), but only 7 of them are ones we except. | |
| # | |
| # The scan consumes the platform's justified exceptions: the | |
| # ClusterSecurityException CRs (k8s/bases/infrastructure/ | |
| # cluster-security-exceptions/ — the single source of truth) are | |
| # converted at scan time into Kubescape's native exceptions format by | |
| # scripts/generate-kubescape-exceptions (#2264). With runtime- | |
| # enforced and except-only findings suppressed, the threshold gates the | |
| # residual REAL posture instead of sitting ~15 points under it. | |
| # | |
| # Two surfaces apply these exceptions: this scan, and the Headlamp view | |
| # via the generated headlamp-exceptions ConfigMap. The kubescape-operator's | |
| # stored scan resources (workloadconfigurationscans and the two summary | |
| # kinds) do NOT — their appliedIgnoreRules is empty, so they carry | |
| # pre-exception verdicts. Never read a posture number off those to judge | |
| # an exception's effect; use this scan's output (#2823). | |
| # | |
| # The threshold is a regression FLOOR, not the live score, and it gates | |
| # the COMBINED score across the frameworks listed above. The Kubescape | |
| # compliance score has historically been environment-dependent (Linux CI | |
| # runner vs macOS) and shifts with the ksail render, so CI is the source | |
| # of truth. Three Linux CI runs on 2026-10-05 with ksail 7.195.1 report | |
| # 98.78% combined (NSA 99.75%, MITRE 98.14%): runs 37263913110, | |
| # 37264739448 and 37279505685. The combined floor is 98%, preserving | |
| # measured margin without treating the score as complete compliance. | |
| # Re-baseline against CI, not a local run, and never lower the floor to | |
| # accommodate a finding. (NB it may also need re-baselining after a ksail | |
| # version bump.) | |
| # | |
| # Known un-excepted residual: C-0012 on the local-dev MinIO Deployment | |
| # (deliberate dev-only credential — tracked for a root-cause fix, not | |
| # an exception). Ratchet the floor up as genuine gaps close; never | |
| # lower it. | |
| # | |
| # `--format sarif,json` writes two reports from this one gating run | |
| # (#3058): kubescape.sarif, which the upload step below turns into Code | |
| # Scanning alerts, and kubescape.sarif.json, which carries the scores | |
| # the threshold is judged against. With JSON among the formats the log | |
| # also prints the `Overall compliance-score` line, which a SARIF-only | |
| # run omits, and the report step below adds the per-framework scores | |
| # to the log and the job summary. Re-baseline the floor from those | |
| # numbers on a CI run. The SARIF findings are the same set a SARIF-only | |
| # run produces (compared locally on 2026-09-23, ksail 7.191.0). | |
| # | |
| # The threshold must still gate after any change to this step: ablated | |
| # 2026-08-10 at thresholds 95/99/100, giving exit 0/1/1 against a | |
| # combined score of 97, and again 2026-09-23 with both formats (exit 1 | |
| # at 100 against 98.7). A silent pass here would be the worst failure | |
| # this step could have, so repeat that ablation if the flag or format | |
| # ever changes. | |
| run: | | |
| go test ./scripts/generate-kubescape-exceptions | |
| go run ./scripts/generate-kubescape-exceptions -o /tmp/kubescape-exceptions.json | |
| ksail workload scan --framework nsa,mitre --exceptions /tmp/kubescape-exceptions.json --compliance-threshold 98 --format sarif,json -o "${RUNNER_TEMP}/kubescape.sarif" | |
| - name: 📊 Report the Kubescape compliance score | |
| # Runs whether the gate above passed or failed, so the log and the job | |
| # summary always state the scores the gate judged (#3058). A missing or | |
| # unreadable report warns rather than failing: the gate already decided. | |
| if: "!cancelled()" | |
| run: | | |
| report="${RUNNER_TEMP}/kubescape.sarif.json" | |
| if scores="$(jq -er ' | |
| "Combined compliance score: \(.summaryDetails.complianceScore * 100 | round / 100) (floor \(.metadata.scanMetadata.complianceThreshold))", | |
| (.summaryDetails.frameworks[] | "\(.name): \(.complianceScore * 100 | round / 100)") | |
| ' "${report}" 2>/dev/null)"; then | |
| printf '%s\n' "${scores}" | |
| { | |
| echo '### Kubescape compliance' | |
| while IFS= read -r line; do printf -- '- %s\n' "${line}"; done <<<"${scores}" | |
| } >>"${GITHUB_STEP_SUMMARY}" | |
| else | |
| echo "::warning::No readable Kubescape JSON report, so the compliance score cannot be shown." | |
| fi | |
| - name: 🌉 Validate the Kubescape backlog bridge | |
| # The bridge derives themed, fingerprint-deduped backlog entries from | |
| # live-only Kubescape findings (#2854). It needs no cluster here: the | |
| # tests pin the two properties that make it safe to run unattended — | |
| # fingerprints stay stable across runs and input order (so unchanged | |
| # state never re-files), and a spec-stripped LIST input fails closed | |
| # instead of reporting a clean cluster. | |
| run: go test ./scripts/kubescape-backlog-bridge | |
| - name: 🧭 Make the SARIF paths repo-root-relative | |
| # Kubescape reports paths relative to the directory it scanned, so a | |
| # finding in k8s/bases/... arrives as bases/... . Code Scanning resolves | |
| # an alert against the REPO ROOT, so uploading those unchanged creates | |
| # alerts pointing at files that do not exist — measured on this PR's | |
| # first run: the upload was green, four alerts existed, all four dead. | |
| # | |
| # The same scan run locally emitted the k8s/ prefix, so the shape is not | |
| # stable across ksail versions. The script resolves each path against | |
| # the working tree rather than assuming either shape, so it is a no-op | |
| # if ksail later emits root-relative paths, and it hard-fails rather | |
| # than uploading a path it cannot resolve. | |
| id: normalize-sarif | |
| if: "!cancelled()" | |
| run: | | |
| shellcheck scripts/normalize-sarif-paths.sh | |
| # `!cancelled()` also covers the scan step failing BEFORE ksail writes | |
| # the SARIF — a failing `go test`, or a ksail crash. There is nothing to | |
| # normalize then, and exiting 2 with "no such file" would make this the | |
| # loudest error in the log and bury the one that actually matters. | |
| if [ ! -f "${RUNNER_TEMP}/kubescape.sarif" ]; then | |
| echo "::notice::No SARIF written — the scan step failed before producing one. Leaving its error as the visible failure." | |
| echo "sarif=absent" >> "${GITHUB_OUTPUT}" | |
| exit 0 | |
| fi | |
| # Keep the scan's own output before normalization touches it. The | |
| # normalizer DROPS results with an empty artifactLocation.uri (see its | |
| # CONTRACT) because Code Scanning cannot anchor an alert without a | |
| # file. That is right for the upload and wrong for the summary: a | |
| # cluster-level control with no manifest behind it is exactly the kind | |
| # of finding that is otherwise invisible, so summarizing the | |
| # normalized file would under-report the very case this reporting | |
| # exists for. Upload reads the normalized file; the summary reads this. | |
| cp "${RUNNER_TEMP}/kubescape.sarif" "${RUNNER_TEMP}/kubescape.raw.sarif" | |
| # Emitted BEFORE the normalizer runs, because it is the summary's gate | |
| # and the summary must survive a normalizer failure. An unresolvable | |
| # path aborts the step, so anything echoed after that line never | |
| # happens — and gating the summary on `sarif=ready` would skip it | |
| # exactly when an upstream path-shape change made the findings most | |
| # worth reading. | |
| echo "raw=ready" >> "${GITHUB_OUTPUT}" | |
| bash scripts/normalize-sarif-paths.sh "${RUNNER_TEMP}/kubescape.sarif" k8s . | |
| # Only now is the file both PRESENT and root-relative. The upload gates | |
| # on this output, so a normalizer hard-failure (an unresolvable path) | |
| # cannot fall through to publishing the un-normalized SARIF — which | |
| # would create exactly the dead-path alerts this step exists to | |
| # prevent. `exit 0` above deliberately leaves it `absent` instead: | |
| # a missing SARIF is the scan's failure to report, not this step's. | |
| echo "sarif=ready" >> "${GITHUB_OUTPUT}" | |
| - name: 🛡️ Upload Kubescape findings to Code Scanning | |
| # The score gate above answers "did posture regress"; it cannot show | |
| # WHICH control failed on WHICH file, and a finding that does not move | |
| # the score is invisible in it entirely. Code Scanning gives each | |
| # control an alert with file and line, dedupes it across runs, and | |
| # closes it automatically when the finding is gone (#2829). | |
| # | |
| # `!cancelled()` rather than `success()`: the findings are wanted most | |
| # precisely when the gate above FAILED. ksail writes the SARIF before it | |
| # applies the threshold, so the file exists on that path too. | |
| # | |
| # Skipped on fork PRs — this job deliberately runs on them (it needs no | |
| # secrets), but a fork's GITHUB_TOKEN cannot be granted | |
| # security-events: write, so attempting the upload there would fail the | |
| # build for a permission no fork can have. | |
| # | |
| # The fork check is the ONLY narrowing: the scan step above is | |
| # unconditional within this job, so gating this step on a path filter | |
| # would silently drop the findings for any run the scan still made. | |
| # | |
| # KNOWN CHARACTERISTIC: cluster-RBAC controls that are not backed by a | |
| # manifest file (C-0002 is the one observed) emit a result with an EMPTY | |
| # artifactLocation.uri — there is no file for such a finding to anchor | |
| # to. The step above drops those; with this repo's exceptions applied | |
| # the scan produces none anyway (measured: 0 of 4 findings; the same | |
| # scan with exceptions removed produces 1 of 70). | |
| # Gated on the normalize step's OUTPUT, not merely on `!cancelled()`. | |
| # `!cancelled()` stays true when normalization FAILS, which would upload | |
| # the un-normalized SARIF and recreate the dead-path alerts that step | |
| # exists to prevent; it is also true when no SARIF was written at all, | |
| # which would invoke the uploader on a missing file. `sarif == 'ready'` | |
| # is reached only after the file exists AND normalization succeeded. | |
| # Note this deliberately still uploads when the SCAN step failed: a | |
| # compliance-threshold failure exits non-zero but writes a valid SARIF, | |
| # and those findings are precisely the ones worth publishing. | |
| if: >- | |
| !cancelled() && | |
| steps.normalize-sarif.outputs.sarif == 'ready' && | |
| github.event.pull_request.head.repo.full_name == github.repository | |
| uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 | |
| with: | |
| sarif_file: ${{ runner.temp }}/kubescape.sarif | |
| # Distinguishes these alerts from the CodeQL and zizmor analyses that | |
| # already publish to this repository's Security tab, so each tool's | |
| # results are tracked and closed independently. | |
| category: kubescape-nsa | |
| - name: 📋 Summarize Kubescape findings | |
| # Kubescape's own console table shows only a subset of what it finds: | |
| # measured on this repository, a scan whose SARIF carried 4 findings | |
| # printed "All controls passed. No issues found". Since the upload above | |
| # turns those findings into alerts, the log and the Security tab | |
| # disagreed — and the log is what a developer reads on a PR (#2846). | |
| # | |
| # This reports from the SARIF the run already produced, so the two | |
| # surfaces cannot drift: same file, same findings. | |
| # | |
| # Gated on the normalizer's output rather than `!cancelled()` alone, for | |
| # the same reason the upload is: that output is the only signal that the | |
| # file both exists and is well-formed. It runs even when the upload step | |
| # did not — a fork PR cannot upload, but its author still benefits from | |
| # seeing what the scan found, which is why the script's wording does not | |
| # assert that an upload happened. | |
| # | |
| # Informational only. The compliance threshold on the scan step remains | |
| # the sole merge gate. | |
| # | |
| # The script exits non-zero on a usage error or a SARIF it cannot parse, | |
| # and leaves it to the caller whether that is fatal. This caller says no: | |
| # a summary that cannot be built says nothing about posture, and the gate | |
| # that does — the compliance threshold — has already run against the same | |
| # file. `continue-on-error` keeps the failure loud (the step is still | |
| # marked failed and annotated on the run) without blocking a merge on a | |
| # reporting bug. Without it, this step silently becomes a second gate, | |
| # contradicting the sentence above it. | |
| continue-on-error: true | |
| if: >- | |
| !cancelled() && | |
| steps.normalize-sarif.outputs.raw == 'ready' | |
| run: | | |
| shellcheck scripts/summarize-sarif-findings.sh | |
| bash scripts/summarize-sarif-findings.sh "${RUNNER_TEMP}/kubescape.raw.sarif" | |
| naming: | |
| name: 🏷️ Validate Naming Conventions | |
| needs: [changes] | |
| # Static folder/file naming gate: kebab-case dirs, one-resource-per-file, | |
| # type-led filenames, and flux-kustomization disambiguation. The shared Go | |
| # action needs no cluster or secrets and also runs on fork PRs. Go setup/build | |
| # can download dependencies. See .github/manifest-naming.yaml and AGENTS.md | |
| # 'File and Directory Naming Conventions'. | |
| if: github.event_name == 'pull_request' && needs.changes.outputs.k8s == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read # checkout repository | |
| steps: | |
| - name: 🛡️ Harden runner | |
| uses: step-security/harden-runner@351661ca32ac09a36dc5ee2d536e3128f2a3c8ed # v2.22.0 | |
| with: | |
| egress-policy: audit | |
| - name: 📑 Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: 🏷️ Validate naming conventions | |
| id: naming | |
| uses: devantler-tech/.github/actions/validate-naming@9ff2ad68306cc73df6814861c7804a11a25e0788 # v7.10.3 | |
| - name: Confirm naming validation completed | |
| env: | |
| VALIDATED: ${{ steps.naming.outputs.validated }} | |
| run: test "$VALIDATED" = true | |
| validate-talos: | |
| name: 🧪 Validate Talos Machine Config | |
| needs: [changes] | |
| # Renders the Talos machine config with every talos/ (prod) and talos-local/ | |
| # patch applied and runs `talosctl validate` on the result — on the PR for | |
| # fast feedback and on the merge-group revision that production deploys. | |
| # A broken patch (or an env expansion that would render empty, like #2462's | |
| # ${WG_SERVER_PRIVATE_KEY}) is therefore caught before deploy. Placeholder | |
| # key material stands in for the env-expanded secrets, so this needs no | |
| # real secrets and runs on fork PRs too (issue #2477). | |
| if: (github.event_name == 'pull_request' && needs.changes.outputs.talos == 'true') || github.event_name == 'merge_group' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read # checkout repository | |
| steps: | |
| - name: 📑 Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: ⚙️ Setup talosctl | |
| run: | | |
| .github/scripts/setup-talosctl.sh | |
| - name: ⚙️ Setup Go for version compatibility validation | |
| uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version-file: go.mod | |
| - name: ✅ Validate Kubernetes against the pinned Talos release | |
| run: | | |
| go test ./scripts/validate-talos-kubernetes-compatibility | |
| go run ./scripts/validate-talos-kubernetes-compatibility ksail.prod.yaml | |
| - name: 🛡️ Validate the Talos AppArmor LSM contract | |
| run: | | |
| shellcheck scripts/tests/test-talos-apparmor-lsm.sh scripts/tests/test-talos-apparmor-lsm-regressions.sh | |
| bash scripts/tests/test-talos-apparmor-lsm-regressions.sh | |
| - name: ⏱️ Validate the Talos drain budget | |
| run: | | |
| shellcheck scripts/tests/test-talos-drain-budget.sh | |
| bash scripts/tests/test-talos-drain-budget.sh | |
| - name: 🧱 Validate Talos Image Factory schematic registration | |
| run: | | |
| shellcheck scripts/register-talos-factory-schematic.sh scripts/tests/test-talos-factory-schematic-registration.sh | |
| bash scripts/tests/test-talos-factory-schematic-registration.sh | |
| - name: 🧾 Validate Talos boot-image readback | |
| run: | | |
| shellcheck scripts/verify-talos-schematic-readback.sh scripts/tests/test-talos-schematic-readback.sh | |
| bash scripts/tests/test-talos-schematic-readback.sh | |
| - name: 🧾 Validate kubelet settings readback | |
| run: | | |
| shellcheck scripts/verify-kubelet-config-readback.sh scripts/tests/test-kubelet-config-readback.sh | |
| bash scripts/tests/test-kubelet-config-readback.sh | |
| - name: 🧱 Prove folded Talos render and invalid-patch rejection | |
| run: | | |
| shellcheck scripts/tests/test-talos-render-kernel-args.sh | |
| bash scripts/tests/test-talos-render-kernel-args.sh | |
| - name: ✅ Render + validate patched machine configs | |
| run: | | |
| set -euo pipefail | |
| # Placeholder key material for env-expanded patch values (ksail | |
| # expands these from environment secrets at deploy time — see | |
| # talos/control-planes/wireguard.yaml and | |
| # talos/cluster/authenticate-ghcr-pulls.yaml and | |
| # talos/cluster/mark-ghcr-pull-revision.yaml). | |
| # Real-format but throwaway, so validation exercises the expansion | |
| # path without any real secret on the PR event. | |
| WG_SERVER_PRIVATE_KEY="$(openssl rand -base64 32)" | |
| GHCR_USERNAME="placeholder-ghcr-user" | |
| GHCR_TOKEN="placeholder-ghcr-token" | |
| GHCR_PULL_REVISION="$(printf '%s\n' 'ENC[ci-placeholder-ciphertext]' | shasum -a 256 | awk '{print $1}')" | |
| export WG_SERVER_PRIVATE_KEY GHCR_USERNAME GHCR_TOKEN GHCR_PULL_REVISION | |
| validate_overlay() { | |
| local dir="$1" mode="$2" cluster_config="$3" | |
| echo "::group::${dir} (${mode})" | |
| # Every ${VAR} referenced by the overlay's patches must be set and | |
| # non-empty here — an empty expansion is exactly the class of | |
| # failure that evicted #2462 from the merge queue. | |
| local vars v fmt="" | |
| vars=$(grep -rhoE '[$][{][A-Za-z_][A-Za-z0-9_]*[}]' "$dir" | sort -u | tr -d '{}$') || true | |
| for v in $vars; do | |
| if [ -z "${!v:-}" ]; then | |
| echo "::error::${dir}: \${${v}} is referenced by a patch but unset/empty — deploy would render an invalid machine config" | |
| exit 1 | |
| fi | |
| fmt="${fmt}\${${v}} " | |
| done | |
| local out; out=$(mktemp -d) | |
| talosctl gen config ci-validate https://192.0.2.1:6443 --output-dir "${out}" >/dev/null | |
| # Mirror ksail's patch layering: cluster/ applies to every node, | |
| # control-planes/ and workers/ to their role. Expand env per patch | |
| # and fail on any token that survives substitution. | |
| local role base patches f expanded | |
| for role in control-planes workers; do | |
| case "${role}" in | |
| control-planes) base="${out}/controlplane.yaml" ;; | |
| workers) base="${out}/worker.yaml" ;; | |
| esac | |
| patches=() | |
| for f in "${dir}"/cluster/*.yaml "${dir}/${role}"/*.yaml; do | |
| [ -f "${f}" ] || continue | |
| expanded="${out}/$(echo "${f}" | tr '/' '_')" | |
| # Allowlist-scoped substitution: only the pre-checked vars are | |
| # expanded, so an unexpected placeholder shape survives for the | |
| # post-check below instead of being silently cleared. | |
| if [ -n "${fmt}" ]; then | |
| envsubst "${fmt}" < "${f}" > "${expanded}" | |
| else | |
| cp "${f}" "${expanded}" | |
| fi | |
| if grep -qE '[$][{][A-Za-z_][A-Za-z0-9_]*[}]' "${expanded}"; then | |
| echo "::error::${f}: unexpanded \${VAR} after substitution" | |
| exit 1 | |
| fi | |
| patches+=("--patch" "@${expanded}") | |
| done | |
| if [ "${#patches[@]}" -gt 0 ]; then | |
| talosctl machineconfig patch "${base}" "${patches[@]}" -o "${out}/${role}-patched.yaml" | |
| else | |
| cp "${base}" "${out}/${role}-patched.yaml" | |
| fi | |
| done | |
| # KSail folds the union of both roles' arguments, only when Image | |
| # Factory extensions are present. Reconcile both before validating | |
| # either, matching the reviewed production post-installer fold. | |
| go run ./scripts/reconcile-talos-kernel-args "${cluster_config}" \ | |
| "${out}/control-planes-patched.yaml" "${out}/workers-patched.yaml" | |
| for role in control-planes workers; do | |
| talosctl validate -c "${out}/${role}-patched.yaml" -m "${mode}" >/dev/null | |
| echo "${dir}/${role}: rendered + validated (${mode})" | |
| done | |
| echo "::endgroup::" | |
| } | |
| validate_overlay talos cloud ksail.prod.yaml | |
| validate_overlay talos-local container ksail.yaml | |
| validate-root-verify-merge-group: | |
| name: 🛡️ Validate Root Source Repair (merge group) | |
| needs: [changes] | |
| # Production deploys the speculative merge-group revision, so the PR-only | |
| # repair test cannot gate the revision that reaches the cluster. | |
| if: >- | |
| github.event_name == 'merge_group' && | |
| needs.changes.outputs.k8s == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read # checkout repository | |
| steps: | |
| - name: 📑 Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: 🛡️ Validate the root source repair before deployment | |
| run: bash scripts/tests/test-reassert-flux-root-verify.sh | |
| validate-rgd-templates-merge-group: | |
| name: 🔬 Validate Nested RGD Workload Templates (merge group) | |
| needs: [changes] | |
| # The PR job proves the proposed head, but production deploys the speculative merge-group | |
| # revision. Re-run the complete behavioral contract on that exact revision before deploy-prod | |
| # can acquire the production environment or publish an artifact. | |
| if: >- | |
| github.event_name == 'merge_group' && | |
| needs.changes.outputs.k8s == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read # checkout repository | |
| steps: | |
| - name: 📑 Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: ⚙️ Setup Trivy for nested RGD templates | |
| uses: aquasecurity/setup-trivy@81e514348e19b6112ce2a7e3ecbafe19c1e1f567 # v0.3.1 | |
| with: | |
| version: v0.74.0 | |
| cache: true | |
| - name: 🔬 Validate nested RGD workload templates | |
| run: | | |
| shellcheck scripts/scan-rgd-templates.sh scripts/tests/test-rgd-template-static-scan.sh | |
| bash scripts/tests/test-rgd-template-static-scan.sh | |
| validate-ghcr-fanout-merge-group: | |
| name: 🔗 Validate GHCR Fan-out Component Gate (merge group) | |
| needs: [changes] | |
| # Same reason as the RGD job above: the PR job proves the proposed head, but | |
| # production deploys the speculative merge-group revision. This guard is what | |
| # stands between an enabled-but-unlisted consumer and a post-reconcile | |
| # reassertion that FAILS EVERY SUBSEQUENT DEPLOY, and the pull_request-only | |
| # invocation cannot see a mismatch main inherited from a direct push. cd.yaml | |
| # already gates its manual deploy on this guard (validate-ghcr-fanout-component-gate); | |
| # the merge queue is the path that actually deploys, so it needs the same | |
| # prerequisite rather than a weaker one. | |
| if: >- | |
| github.event_name == 'merge_group' && | |
| needs.changes.outputs.k8s == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read # checkout repository | |
| steps: | |
| - name: 📑 Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: 🔗 Validate the GHCR fan-out follows the apps-base component gate | |
| run: | | |
| shellcheck scripts/guard-ghcr-fanout-component-gate.sh | |
| shellcheck scripts/tests/test-guard-ghcr-fanout-component-gate.sh | |
| bash scripts/guard-ghcr-fanout-component-gate.sh | |
| bash scripts/tests/test-guard-ghcr-fanout-component-gate.sh | |
| validate-floating-image-tags: | |
| name: 🏷️ Validate Floating Image Tags | |
| needs: [changes] | |
| # A moving tag such as `main` lets an upstream push change what production | |
| # runs with no PR, no review and no digest. Upstream kubelet-serving-cert-approver | |
| # did exactly that (#3515), and the Kyverno latest-tag policy only matches | |
| # `latest`, runs in Audit, and skips most system namespaces. This renders every | |
| # cluster overlay the way Flux applies it and fails on any container image that | |
| # is neither digest-pinned nor version-tagged (#3755). It runs on the merge-group | |
| # revision too, and deploy-prod needs it: two individually clean PRs can combine | |
| # into a floating image, and that combined revision is what production receives. | |
| # A deliberate exception is a reviewed row naming the workload and an issue, and | |
| # a row that no longer matches is itself a violation. | |
| if: (github.event_name == 'pull_request' && needs.changes.outputs.k8s == 'true') || github.event_name == 'merge_group' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read # checkout repository | |
| steps: | |
| - name: 📑 Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: 🏷️ Validate no rendered workload runs a floating image tag | |
| run: | | |
| shellcheck scripts/guard-floating-image-tags.sh scripts/tests/test-guard-floating-image-tags.sh | |
| bash scripts/guard-floating-image-tags.sh k8s | |
| bash scripts/tests/test-guard-floating-image-tags.sh | |
| validate-helm-post-renderers: | |
| name: 🧩 Validate Helm Post-Renderers | |
| needs: [changes] | |
| # A HelmRelease's postRenderers run only inside helm-controller, against the chart's | |
| # rendered output, so no other check here ever executes one. #3577 merged fully green with | |
| # a JSON-6902 add beneath a path flux-operator 0.50.0 does not render; the upgrade failed | |
| # and the release could not upgrade at all, while it still reported Ready (#3581). This | |
| # renders the chart of every HelmRelease whose definition changed since the base, with the | |
| # release's own values, and applies its post-renderers the way Flux does. Releases that did | |
| # not change pull no chart. It runs on the merge-group revision too, and deploy-prod needs | |
| # it: a chart bump and a post-renderer change can each pass alone and fail combined. | |
| if: (github.event_name == 'pull_request' && needs.changes.outputs.k8s == 'true') || github.event_name == 'merge_group' | |
| runs-on: ubuntu-latest | |
| # Changed releases pull their charts from upstream registries; a stalled registry fails | |
| # this job instead of holding the merge group. | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: read # checkout repository | |
| steps: | |
| - name: 📑 Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| # The guard renders the base revision too, to find which releases changed. | |
| fetch-depth: 0 | |
| - name: ⚙️ Setup the audited controller's Go toolchain | |
| uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version-file: scripts/render-helm-chart/go.mod | |
| cache: false | |
| - name: 🧩 Install the audited controller's Helm renderer | |
| # Flux 2.8.8 / helm-controller 1.5.5 embeds upstream Helm 4.2.0. The | |
| # runner's ambient Helm is not proof of matching template capabilities. | |
| env: | |
| HELM_VERSION: v4.2.0 | |
| run: | | |
| install_dir="$(mktemp -d)" | |
| cd "$install_dir" | |
| archive="helm-${HELM_VERSION}-linux-amd64.tar.gz" | |
| curl --fail --location --retry 3 --output "$archive" "https://get.helm.sh/$archive" | |
| curl --fail --location --retry 3 --output "$archive.sha256sum" "https://get.helm.sh/$archive.sha256sum" | |
| sha256sum --check "$archive.sha256sum" | |
| tar -xzf "$archive" | |
| echo "$install_dir/linux-amd64" >> "$GITHUB_PATH" | |
| - name: 🧩 Validate the post-renderer guard | |
| run: | | |
| shellcheck scripts/build-controller-helm.sh scripts/guard-helm-post-renderers.sh scripts/tests/test-guard-helm-post-renderers.sh | |
| controller_helm="$RUNNER_TEMP/controller-helm" | |
| bash scripts/build-controller-helm.sh "$controller_helm" | |
| echo "CONTROLLER_HELM=$controller_helm" >> "$GITHUB_ENV" | |
| export CONTROLLER_HELM="$controller_helm" | |
| bash scripts/tests/test-guard-helm-post-renderers.sh | |
| - name: 🧩 Validate Reloader chart selection | |
| run: | | |
| shellcheck scripts/tests/test-reloader-placement-selection.sh | |
| bash scripts/tests/test-reloader-placement-selection.sh | |
| - name: 🧩 Verify Longhorn volume-client placement | |
| env: | |
| BASE_SHA: >- | |
| ${{ github.event.pull_request.base.sha || | |
| github.event.merge_group.base_sha }} | |
| run: | | |
| shellcheck scripts/tests/test-longhorn-client-placement.sh | |
| changed_inputs="$(git diff --name-only "$BASE_SHA" HEAD -- \ | |
| .github/workflows/ci.yaml scripts/tests/test-longhorn-client-placement.sh \ | |
| scripts/build-controller-helm.sh scripts/render-helm-chart \ | |
| k8s/providers/hetzner/infrastructure/controllers/longhorn \ | |
| k8s/providers/hetzner/infrastructure/cluster-policies/restrict-storage-to-baseline-workers.yaml \ | |
| k8s/providers/hetzner/infrastructure/kustomization.yaml \ | |
| k8s/clusters/prod/bootstrap/config-map.yaml ksail.prod.yaml)" | |
| if [[ -n "$changed_inputs" ]]; then | |
| bash scripts/tests/test-longhorn-client-placement.sh | |
| fi | |
| - name: 🧩 Apply the actual Whoami chart's post-renderers | |
| run: | | |
| bash scripts/guard-helm-post-renderers.sh --flux-version 2.8.8 --release whoami/whoami \ | |
| --kube-version "$(yq -r '.spec.cluster.kubernetesVersion' ksail.prod.yaml)" k8s | |
| - name: 🧩 Verify independent reloader placement | |
| env: | |
| BASE_SHA: >- | |
| ${{ github.event.pull_request.base.sha || | |
| github.event.merge_group.base_sha }} | |
| run: | | |
| shellcheck scripts/tests/test-reloader-rendered-placement.sh | |
| # Merge groups run this job even for unrelated changes. Pull the chart | |
| # only when the release, its composition or the regression inputs move. | |
| changed_inputs="$(git diff --name-only "$BASE_SHA" HEAD -- \ | |
| .github/workflows/ci.yaml \ | |
| k8s/bases/infrastructure/controllers/reloader \ | |
| k8s/bases/infrastructure/controllers/kustomization.yaml \ | |
| k8s/providers/hetzner/infrastructure/controllers/kustomization.yaml \ | |
| k8s/providers/docker/infrastructure/controllers/kustomization.yaml \ | |
| k8s/providers/hetzner/infrastructure/controllers/reloader \ | |
| k8s/providers/docker/infrastructure/controllers/reloader \ | |
| k8s/bases/components \ | |
| k8s/clusters/prod/bootstrap/config-map.yaml ksail.prod.yaml \ | |
| scripts/tests/test-reloader-rendered-placement.sh \ | |
| scripts/tests/test-reloader-placement-selection.sh \ | |
| scripts/build-controller-helm.sh \ | |
| scripts/render-helm-chart/go.mod scripts/render-helm-chart/go.sum)" | |
| if [[ -n "$changed_inputs" ]]; then | |
| bash scripts/tests/test-reloader-rendered-placement.sh | |
| fi | |
| - name: 🧩 Verify Kubescape recovery storage placement | |
| env: | |
| BASE_SHA: >- | |
| ${{ github.event.pull_request.base.sha || | |
| github.event.merge_group.base_sha }} | |
| run: | | |
| shellcheck scripts/tests/test-kubescape-storage-placement.sh | |
| changed_inputs="$(git diff --name-only "$BASE_SHA" HEAD -- \ | |
| .github/workflows/ci.yaml \ | |
| k8s/bases/infrastructure/controllers/kubescape \ | |
| k8s/bases/infrastructure/controllers/kustomization.yaml \ | |
| k8s/providers/hetzner/infrastructure/controllers/kustomization.yaml \ | |
| k8s/providers/hetzner/infrastructure/controllers/kubescape \ | |
| k8s/bases/components ksail.prod.yaml \ | |
| scripts/tests/test-kubescape-storage-placement.sh \ | |
| scripts/build-controller-helm.sh \ | |
| scripts/render-helm-chart/go.mod scripts/render-helm-chart/go.sum)" | |
| if [[ -n "$changed_inputs" ]]; then | |
| bash scripts/tests/test-kubescape-storage-placement.sh | |
| fi | |
| - name: 🧩 Apply every changed HelmRelease's post-renderers to its chart | |
| env: | |
| BASE_SHA: >- | |
| ${{ github.event.pull_request.base.sha || | |
| github.event.merge_group.base_sha }} | |
| run: | | |
| base_kube_version="$(git show "${BASE_SHA}:ksail.prod.yaml" | yq -er '.spec.cluster.kubernetesVersion | select(tag == "!!str" and . != "")')" | |
| bash scripts/guard-helm-post-renderers.sh --flux-version 2.8.8 --base "$BASE_SHA" \ | |
| --base-kube-version "$base_kube_version" \ | |
| --kube-version "$(yq -r '.spec.cluster.kubernetesVersion' ksail.prod.yaml)" k8s | |
| validate-pdb-selectors: | |
| name: 🛟 Validate PodDisruptionBudget Selectors | |
| needs: [changes] | |
| # A PodDisruptionBudget whose selector matches no pod is valid YAML: it renders, passes | |
| # schema validation and the Kubescape scan, and its only symptom is expectedPods: 0 on the | |
| # live cluster. homepage's budget shipped that way (#4365). This renders every cluster | |
| # overlay the way Flux applies it and requires each budget to select a workload rendered | |
| # into its namespace. Most budgets here guard a workload only a chart renders, so the | |
| # HelmReleases installed beside an unsatisfied budget are rendered too, with their values | |
| # and post-renderers, and a Flagger Canary's primary is derived from its target (#3596). | |
| # | |
| # Pull requests only. Every run pulls the chart of each release it has to render, over | |
| # twenty today, so on the merge group it would make every production deploy wait on that | |
| # many upstream registries. A chart bump, a selector edit and a new budget each run it on | |
| # their own pull request. | |
| if: github.event_name == 'pull_request' && needs.changes.outputs.k8s == 'true' | |
| runs-on: ubuntu-latest | |
| # A stalled registry fails this job instead of holding it for the default six hours. | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: read # checkout repository | |
| steps: | |
| - name: 📑 Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: ⚙️ Setup the audited controller's Go toolchain | |
| uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version-file: scripts/render-helm-chart/go.mod | |
| cache: false | |
| - name: 🛟 Validate the PodDisruptionBudget selector guard | |
| # The fixture suite pulls no chart: a stand-in packages its fixture charts, and the | |
| # renderer built here renders them. | |
| run: | | |
| shellcheck scripts/guard-pdb-selector-match.sh scripts/tests/test-guard-pdb-selector-match.sh | |
| controller_helm="$RUNNER_TEMP/controller-helm" | |
| bash scripts/build-controller-helm.sh "$controller_helm" | |
| echo "CONTROLLER_HELM=$controller_helm" >> "$GITHUB_ENV" | |
| export CONTROLLER_HELM="$controller_helm" | |
| bash scripts/tests/test-guard-pdb-selector-match.sh | |
| - name: 🛟 Validate every PodDisruptionBudget selects a rendered workload | |
| run: | | |
| kube_version="$(yq -er '.spec.cluster.kubernetesVersion | select(tag == "!!str" and . != "")' ksail.prod.yaml)" | |
| bash scripts/guard-pdb-selector-match.sh --kube-version "$kube_version" k8s | |
| # Every merge-group validator that ci-required-checks enforces is listed here too. | |
| # A required validator the deploy does not wait for runs in parallel with it, so | |
| # production can receive a revision that validator then rejects (#4148). | |
| # scripts/guard-deploy-waits-for-merge-group-validators.sh enforces this. | |
| deploy-prod: | |
| name: 🚀 Deploy to Prod | |
| needs: | |
| [ | |
| changes, | |
| validate-consumer-discovery, | |
| validate-floating-image-tags, | |
| validate-helm-post-renderers, | |
| validate-eks-authorization, | |
| validate-publication-contract, | |
| validate-matcher-efficacy, | |
| validate-talos, | |
| validate-root-verify-merge-group, | |
| validate-rgd-templates-merge-group, | |
| validate-ghcr-fanout-merge-group, | |
| ] | |
| if: github.event_name == 'merge_group' && needs.changes.outputs.k8s == 'true' | |
| runs-on: ubuntu-latest | |
| environment: prod | |
| # Shared with the CD workflow (cd.yaml) so a manual deploy and a | |
| # merge-queue deploy can never run against the prod cluster at once. | |
| # queue: max so a deploy waiting on that lock is queued rather than replaced | |
| # by the next merge — without it, the middle deploy of any burst is dropped | |
| # silently. See the fuller rationale in cd.yaml. | |
| concurrency: | |
| group: prod-deploy | |
| cancel-in-progress: false | |
| queue: max | |
| permissions: | |
| contents: read # checkout repository | |
| packages: write # push OCI artifacts to GHCR | |
| id-token: write # mint OIDC token for cosign keyless signing (Fulcio + Rekor) | |
| attestations: write # write SBOM + SLSA provenance attestations (see deploy-prod action) | |
| actions: read # read a prior run attempt status, to prove an orphaned fence holder is dead (deploy-prod recover-orphaned-fence) | |
| steps: | |
| - name: 📑 Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| fetch-depth: 0 | |
| - name: 🚀 Deploy to Production | |
| # Shared with cd.yaml's manual deploy via this composite action so the | |
| # merge-queue and manual paths can never drift. Secrets are passed as | |
| # inputs because composite actions cannot read `secrets` directly. | |
| uses: ./.github/actions/deploy-prod | |
| with: | |
| sops-age-key: ${{ secrets.SOPS_AGE_KEY }} | |
| kube-config: ${{ secrets.KUBE_CONFIG }} | |
| talos-config: ${{ secrets.TALOS_CONFIG }} | |
| ghcr-token: ${{ secrets.GHCR_TOKEN }} | |
| hcloud-token: ${{ secrets.HCLOUD_TOKEN }} | |
| wg-server-private-key: ${{ secrets.WG_SERVER_PRIVATE_KEY }} | |
| # Release a synchronization Lease that an already-dead deploy left | |
| # held, so one crashed run cannot wedge every later deploy and its own | |
| # heal. This relaxes no guard: the composite still refuses unless no | |
| # other fence is held, the heartbeat has stopped, and the holder's run | |
| # attempt is proven terminal via the API (#3343). | |
| recover-orphaned-fence: "true" | |
| merge-group-queue-membership: | |
| name: 🧾 Check the deployed PR is still queued | |
| needs: [changes, deploy-prod] | |
| # A deploy can SUCCEED after the merge queue has already dropped its PR: the | |
| # queue's check timeout can expire while the deploy waits on the prod-deploy | |
| # lock behind a heal, and a PR can be dequeued by hand. Prod then runs an | |
| # artifact that never reaches `main`, and nothing restores it, because the | |
| # heal below only fires on an unsuccessful deploy (#3091). This job waits | |
| # until the PR merges or leaves the queue, and hands the answer to the heal; | |
| # `CI - Required Checks` does not depend on it, so the wait never holds up the | |
| # merge. A PR that left is answered only once the queue has drained: a later | |
| # group may already be deployed but unmerged, and restoring main before it | |
| # merges would publish main without its change. A failed read | |
| # fails this job visibly and writes no answer, so the heal never runs on a | |
| # guess. It is not part of the required checks: it gates nothing. | |
| if: github.event_name == 'merge_group' && needs.changes.outputs.k8s == 'true' && needs.deploy-prod.result == 'success' | |
| runs-on: ubuntu-latest | |
| # The check polls until the PR merges, or leaves and the queue drains (at most 120 | |
| # reads, 30s apart). | |
| timeout-minutes: 75 | |
| permissions: | |
| contents: read # checkout repository | |
| pull-requests: read # read the PR's merge-queue state | |
| outputs: | |
| evicted: ${{ steps.membership.outputs.evicted }} | |
| steps: | |
| - name: 📑 Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| sparse-checkout: scripts/merge-group-evicted.sh | |
| sparse-checkout-cone-mode: false | |
| - name: 🧾 Read the PR's merge-queue state | |
| id: membership | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| EVICTED_REPOSITORY: ${{ github.repository }} | |
| EVICTED_HEAD_REF: ${{ github.event.merge_group.head_ref }} | |
| EVICTED_GROUP_CREATED_AT: ${{ github.event.merge_group.head_commit.timestamp }} | |
| run: scripts/merge-group-evicted.sh | |
| heal-prod-on-failure: | |
| name: 🩹 Heal Prod (restore main after an unsuccessful or evicted merge-group deploy) | |
| needs: [changes, deploy-prod, validate-publication-contract, merge-group-queue-membership] | |
| # The deploy above runs on the SPECULATIVE merge ref, before the PR lands on | |
| # `main`. When it fails the merge group is ejected — but the OCI `latest` tag | |
| # (and the prod cluster reconciling it) is left pointing at that ejected | |
| # artifact, which never lands on `main`, so prod silently diverges from Git | |
| # until some future merge overwrites it (devantler-tech/platform#2026). This | |
| # unsuccessful-path-only job re-deploys the current tip of `main` via the | |
| # SAME composite, so the speculative artifact is immediately replaced by | |
| # main's signed artifact and prod reconverges with Git. It covers explicit | |
| # cancellation too: GitHub re-evaluates job conditions during normal | |
| # cancellation, and `always()` keeps this cleanup eligible after the deploy | |
| # job becomes cancelled. It has ZERO effect on a green merge (skipped) and | |
| # does NOT relax the gate: deploy-prod's failure still fails `CI - Required | |
| # Checks` below, so the PR is still ejected. It is intentionally NOT part of | |
| # that aggregation — it is best-effort cleanup, not a merge gate. It also | |
| # fires after a SUCCESSFUL deploy whose PR had already left the queue, which | |
| # leaves prod on the same unmerged artifact (#3091); the membership job above | |
| # decides that case. | |
| if: >- | |
| always() && | |
| github.event_name == 'merge_group' && | |
| needs.changes.outputs.k8s == 'true' && | |
| (needs.deploy-prod.result == 'failure' || | |
| needs.deploy-prod.result == 'cancelled' || | |
| (needs.deploy-prod.result == 'success' && | |
| needs.merge-group-queue-membership.outputs.evicted == 'true')) | |
| runs-on: ubuntu-latest | |
| environment: prod | |
| # Same shared lock as deploy-prod / cd.yaml so the heal can never run against | |
| # the prod cluster at the same time as another deploy. `needs: deploy-prod` | |
| # also guarantees the failed deploy has released the lock before the heal | |
| # acquires it, so there is no self-deadlock. | |
| # queue: max matters most here: a heal that is dropped while pending leaves | |
| # main un-restored after a failed deploy, which is the exact state this job | |
| # exists to prevent. See the fuller rationale in cd.yaml. | |
| concurrency: | |
| group: prod-deploy | |
| cancel-in-progress: false | |
| queue: max | |
| permissions: | |
| contents: read # checkout repository | |
| packages: write # push OCI artifacts to GHCR | |
| id-token: write # mint OIDC token for cosign keyless signing (Fulcio + Rekor) | |
| attestations: write # write SBOM + SLSA provenance attestations (see deploy-prod action) | |
| actions: read # read a prior run attempt status, to prove an orphaned fence holder is dead (deploy-prod recover-orphaned-fence) | |
| steps: | |
| - name: 📑 Checkout main | |
| # Deploy the CURRENT tip of `main` (the last state that actually landed in | |
| # Git), not the merge group's base_sha. The deploy action rechecks this | |
| # validated checkout before recovery changes production and at promotion. | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: main | |
| persist-credentials: false | |
| fetch-depth: 0 | |
| - name: 🧭 Record the exact recovery checkout | |
| # Read this checkout even if the shell directory or inherited Git | |
| # environment points at another repository. | |
| id: recovery-baseline | |
| shell: bash | |
| run: | | |
| recovery_sha="$(env -u GIT_DIR -u GIT_WORK_TREE -u GIT_COMMON_DIR \ | |
| git --no-replace-objects -C "${GITHUB_WORKSPACE:?the recovery checkout workspace is required}" \ | |
| rev-parse --verify 'HEAD^{commit}')" | |
| [[ "${recovery_sha}" =~ ^[0-9a-f]{40}$ ]] | |
| printf 'sha=%s\n' "${recovery_sha}" >>"${GITHUB_OUTPUT}" | |
| - name: 🛡️ Validate the root source repair on main | |
| # The merge-group gate tested a speculative checkout; main may have | |
| # advanced before this heal publishes and reconciles its own checkout. | |
| # During this PR's own merge-group run, main still predates the repair | |
| # test. Keep that one legacy heal path working; after the test lands, a | |
| # missing or failing test remains a hard stop. | |
| shell: bash | |
| run: | | |
| if [[ ! -f scripts/tests/test-reassert-flux-root-verify.sh ]]; then | |
| echo '- Root source repair: main predates the root-source repair; continuing the legacy heal path.' >>"${GITHUB_STEP_SUMMARY}" | |
| exit 0 | |
| fi | |
| bash scripts/tests/test-reassert-flux-root-verify.sh | |
| # Signer approval is enforced first by the deploy action from this main | |
| # checkout, so recovery uses the policy version of the revision it restores. | |
| - name: ⚙️ Setup Go for version compatibility validation | |
| uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version-file: go.mod | |
| - name: ✅ Validate main Kubernetes against the pinned Talos release | |
| # The heal deploys the current main checkout, which may have advanced | |
| # since validate-talos inspected the speculative merge-group revision. | |
| run: | | |
| go test ./scripts/validate-talos-kubernetes-compatibility | |
| go run ./scripts/validate-talos-kubernetes-compatibility ksail.prod.yaml | |
| - name: ⚙️ Setup Trivy for nested RGD templates | |
| uses: aquasecurity/setup-trivy@81e514348e19b6112ce2a7e3ecbafe19c1e1f567 # v0.3.1 | |
| with: | |
| version: v0.74.0 | |
| cache: true | |
| - name: 🔬 Validate nested RGD workload templates on main | |
| # `main` may have advanced after the speculative merge-group scan. Re-run the complete | |
| # behavior contract on the exact checkout this heal is about to publish and reconcile. | |
| run: | | |
| shellcheck scripts/scan-rgd-templates.sh scripts/tests/test-rgd-template-static-scan.sh | |
| bash scripts/tests/test-rgd-template-static-scan.sh | |
| - name: ⚙️ Setup KSail for effective authorization validation | |
| shell: bash | |
| env: | |
| # renovate: datasource=github-releases depName=devantler-tech/ksail extractVersion=^v(?<version>.+)$ | |
| KSAIL_VERSION: "7.202.58" | |
| GITHUB_TOKEN: ${{ github.token }} | |
| run: .github/scripts/setup-ksail.sh | |
| - name: ⚖️ Read Kyverno CLI version for recovery chart admission | |
| id: chart-kyverno-version | |
| run: bash .github/scripts/kyverno-version.sh | |
| - name: ⚖️ Install Kyverno CLI for recovery chart admission | |
| uses: kyverno/action-install-cli@fcee92fca5c883169ef9927acf543e0b5fc58289 # v0.2.0 | |
| with: | |
| release: ${{ steps.chart-kyverno-version.outputs.release }} | |
| - name: 🔐 Validate isolated chart child namespaces on main | |
| # Same reason as the RGD scan above, for the other gate this job's | |
| # publish depends on. `validate-eks-authorization` rendered the chart on | |
| # the SPECULATIVE merge ref; this job publishes `ref: main`, which may | |
| # have advanced since. Without this step the isolated-chart exemption | |
| # would be relied on for a revision no render ever inspected — and the | |
| # per-workflow coverage guard would still read ci.yaml as gated, because | |
| # a sibling job ran the validator on a different commit. | |
| timeout-minutes: 10 | |
| run: | | |
| shellcheck scripts/tests/test-isolated-chart-namespace-rules.sh | |
| bash scripts/tests/test-isolated-chart-namespace-rules.sh | |
| - name: 🧪 Validate the data product trial before recovery deploy | |
| # This job checks out main, which may predate the candidate's new test. | |
| # Require it whenever that checkout activates the trial; otherwise | |
| # prove the inactive app and credential fan-out gates still agree. | |
| run: | | |
| trial_active="$(yq -r '[.resources[] | select(. == "data-product-controller/")] | length' k8s/bases/apps/kustomization.yaml)" | |
| case "$trial_active" in | |
| 1) | |
| shellcheck scripts/tests/test-data-product-controller-trial.sh | |
| bash scripts/tests/test-data-product-controller-trial.sh | |
| ;; | |
| 0) | |
| bash scripts/guard-ghcr-fanout-component-gate.sh | |
| echo 'Main has no active data product trial; its credential gate is consistent.' | |
| ;; | |
| *) | |
| echo 'Cannot establish the data product trial gate on main.' >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| - name: 🩹 Re-deploy main to Production | |
| # Reuse the shared deploy composite (push → sign → attest → reconcile → | |
| # cluster update) against main's checkout, so the restored artifact is | |
| # byte-for-byte a normal main deploy (fully signed/attested) — no drift | |
| # from the happy path. | |
| uses: ./.github/actions/deploy-prod | |
| with: | |
| recovery-source-sha: ${{ steps.recovery-baseline.outputs.sha }} | |
| sops-age-key: ${{ secrets.SOPS_AGE_KEY }} | |
| kube-config: ${{ secrets.KUBE_CONFIG }} | |
| talos-config: ${{ secrets.TALOS_CONFIG }} | |
| ghcr-token: ${{ secrets.GHCR_TOKEN }} | |
| hcloud-token: ${{ secrets.HCLOUD_TOKEN }} | |
| wg-server-private-key: ${{ secrets.WG_SERVER_PRIVATE_KEY }} | |
| # Release a synchronization Lease that an already-dead deploy left | |
| # held, so one crashed run cannot wedge every later deploy and its own | |
| # heal. This relaxes no guard: the composite still refuses unless no | |
| # other fence is held, the heartbeat has stopped, and the holder's run | |
| # attempt is proven terminal via the API (#3343). | |
| recover-orphaned-fence: "true" | |
| - name: 🧹 Verify nothing the failed deploy applied outlived the heal | |
| # Re-deploying main restores what main declares. An object the failed | |
| # revision applied that main does not declare is pruned, unless it | |
| # carries `prune: disabled`: Flux then drops it from its inventory and | |
| # leaves it running. This heal once reported success while such a | |
| # HelmRelease sat Ready=False in prod for 38 hours (#3502). Fail the heal | |
| # instead, naming each object, so a green heal means nothing the failed | |
| # revision applied is still running outside Flux. Write times identify | |
| # its residue only when recovery restores the failed group's own base. | |
| # If main advanced, recent untracked objects are UNKNOWN: a newer main | |
| # may have deliberately retired them. Older ones are listed as a warning | |
| # until they are deleted by hand (#3503). Read-only, against the kubeconfig the | |
| # re-deploy restored, after its reconcile waited for every flux-system | |
| # Kustomization to apply main. During this PR's own merge-group run, main | |
| # still predates the check; keep that one legacy heal path working, as | |
| # the root source repair above does. The changes job keeps it unreachable | |
| # once the check is on main, and the heal contract pins this script. | |
| shell: bash | |
| env: | |
| FLUX_ORPHANS_SINCE: ${{ github.event.merge_group.head_commit.timestamp }} | |
| FLUX_ORPHANS_BASE_SHA: ${{ github.event.merge_group.base_sha }} | |
| FLUX_ORPHANS_RECOVERY_SHA: ${{ steps.recovery-baseline.outputs.sha }} | |
| run: | | |
| if [[ ! -f scripts/check-flux-orphaned-objects.sh ]]; then | |
| echo '- Flux orphaned objects: main predates the check; not run.' >>"${GITHUB_STEP_SUMMARY}" | |
| exit 0 | |
| fi | |
| : "${FLUX_ORPHANS_SINCE:?the merge group creation time is required}" | |
| ./scripts/check-flux-orphaned-objects.sh | |
| ci-required-checks: | |
| name: CI - Required Checks | |
| runs-on: ubuntu-latest | |
| needs: | |
| [ | |
| changes, | |
| validate-consumer-discovery, | |
| validate-publication-contract, | |
| validate-matcher-efficacy, | |
| validate-eks-authorization, | |
| validate, | |
| validate-floating-image-tags, | |
| validate-helm-post-renderers, | |
| validate-pdb-selectors, | |
| naming, | |
| validate-talos, | |
| validate-root-verify-merge-group, | |
| validate-rgd-templates-merge-group, | |
| validate-ghcr-fanout-merge-group, | |
| deploy-prod, | |
| ] | |
| if: always() | |
| permissions: {} | |
| steps: | |
| - name: 📊 Evaluate job results | |
| uses: devantler-tech/actions/aggregate-job-checks@1d726483facb9340655489913f609082677be8f5 # v13.10.6 | |
| with: | |
| job-results: >- | |
| ${{ needs.changes.result }} | |
| ${{ needs.validate-consumer-discovery.result }} | |
| ${{ needs.validate-publication-contract.result }} | |
| ${{ needs.validate-matcher-efficacy.result }} | |
| ${{ needs.validate-eks-authorization.result }} | |
| ${{ needs.validate.result }} | |
| ${{ needs.validate-floating-image-tags.result }} | |
| ${{ needs.validate-helm-post-renderers.result }} | |
| ${{ needs.validate-pdb-selectors.result }} | |
| ${{ needs.naming.result }} | |
| ${{ needs.validate-talos.result }} | |
| ${{ needs.validate-root-verify-merge-group.result }} | |
| ${{ needs.validate-rgd-templates-merge-group.result }} | |
| ${{ needs.validate-ghcr-fanout-merge-group.result }} | |
| ${{ needs.deploy-prod.result }} |