Skip to content

chore(deps): update helm release hcloud-cloud-controller-manager to v1.39.0 #10250

chore(deps): update helm release hcloud-cloud-controller-manager to v1.39.0

chore(deps): update helm release hcloud-cloud-controller-manager to v1.39.0 #10250

Workflow file for this run

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 }}