Skip to content

🐛 fix(portal): drop sign-in-page:app disable + repin image sha-4e… #113

🐛 fix(portal): drop sign-in-page:app disable + repin image sha-4e…

🐛 fix(portal): drop sign-in-page:app disable + repin image sha-4e… #113

Workflow file for this run

# Chainsaw CI (enabled after E3) — L2 declarative cluster e2e on kind.
#
# CI SUBSTRATE PARITY (2026-07-16): the CI cluster is built with the SAME recipe
# as the live kaddy-dev substrate — hack/cluster/kind-up.sh (kind config with
# disableDefaultCNI:true + kubeProxyMode:none, Cilium + Gateway API CRDs +
# LB-IPAM, cert-manager + kaddy-local-ca, all pinned via
# hack/cluster/versions.env). That un-skips the Cilium/Gateway/Rollouts/local-CA
# dependent suites (gateway, security netpol, rollouts, tls certificate-renewal)
# so the platform's headline behaviors are enforced regression here, not
# live-only proofs.
name: chainsaw
on:
# Runs on push to main (local-merge workflow — no PR required; L2 gate lands
# in parallel and we fix-forward if a later commit trips it) AND on PRs.
push:
branches: [main]
paths:
- 'deploy/**'
- 'tests/chainsaw/**'
- 'hack/cluster/**'
- 'Taskfile.yml'
- '.github/workflows/chainsaw.yaml'
pull_request:
paths:
- 'deploy/**'
- 'tests/chainsaw/**'
- 'hack/cluster/**'
- 'Taskfile.yml'
- '.github/workflows/chainsaw.yaml'
permissions:
contents: read
jobs:
chainsaw:
runs-on: ubuntu-latest
# Substrate bring-up (Cilium + observability + rollouts + workloads) plus the
# full suite run budgets ~20 min; hard-stop well under the 6h default so a
# wedged install fails fast.
timeout-minutes: 35
steps:
# SEC-5: all action refs pinned to full commit SHAs (version as comment),
# Renovate-compatible (github-actions manager updates sha + comment).
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Install Task
uses: arduino/setup-task@b91d5d2c96a56797b48ac1e0e89220bf64044611 # v2.0.0
with:
version: 3.x
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Go
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
with:
go-version: stable
- name: Install Chainsaw
# SEC-4: pin to an exact tag (was @latest) so builds are reproducible
# and Renovate can track the bump.
run: go install github.com/kyverno/chainsaw@v0.2.15
- name: Install kind (pinned)
# The cluster itself is created by hack/cluster/kind-up.sh (parity with
# the live substrate) — helm/kind-action is gone. Only the kind BINARY
# is provisioned here; kubectl, helm and jq are runner-preinstalled.
# SEC-4: exact version, paired with KIND_NODE_IMAGE v1.33.1
# (hack/cluster/versions.env).
run: |
KIND_VERSION="v0.29.0"
sudo curl -sSL -o /usr/local/bin/kind \
"https://kind.sigs.k8s.io/dl/${KIND_VERSION}/kind-linux-amd64"
sudo chmod +x /usr/local/bin/kind
- name: Raise inotify limits (kind on a shared runner)
# The parity substrate runs many watch-heavy pods (Cilium, kps, Loki,
# Alloy, Kyverno, Rollouts) on one kind node; the runner's default
# inotify limits cause spurious "too many open files" crash-loops.
run: |
sudo sysctl -w fs.inotify.max_user_watches=524288
sudo sysctl -w fs.inotify.max_user_instances=512
- name: Create kind cluster (live substrate recipe — Cilium + Gateway API + LB-IPAM + cert-manager + kaddy-local-ca)
# REUSES hack/cluster/kind-up.sh — the exact scripts + versions.env pins
# the live kaddy-dev cluster is built from (that's the parity point).
# On the runner: docker is rootful (podman guards no-op), the loopback
# 30080/30443 extraPortMappings are free, and the LB-IPAM pool is carved
# from the real docker "kind" bridge subnet by install-cilium.sh.
run: |
bash hack/cluster/kind-up.sh
# hack/cluster isolates its kubeconfig at .state/kubeconfig (never the
# runner default) — export it for every following step. Taskfile's
# KUBECONFIG default points at the same file.
echo "KUBECONFIG=${GITHUB_WORKSPACE}/.state/kubeconfig" >> "$GITHUB_ENV"
# Suites still skipped here are gated by `skip: true` until their epic
# lands (identity→E1d, portal→E10, caddy-mvp→e-caddy-mvp, crossplane→needs
# Crossplane in CI, unsigned-image→cosign Enforce flip). Each epic's
# implementation installs its deps here and flips its suite's skip off.
- name: Install Kyverno (labeling/security suites)
run: |
if ls tests/chainsaw/labeling/*.yaml tests/chainsaw/security/*.yaml >/dev/null 2>&1; then
# E1c: the labeling suite applies its own ClusterPolicies, so this
# install must actually succeed — no failure-swallowing || true
# (TEST-8), and the same vendored+pinned manifest the live cluster
# syncs (deploy/kyverno/, v1.18.2). Server-side apply: the CRDs
# exceed client-side annotation limits.
kubectl apply --server-side -f deploy/kyverno/install.yaml
kubectl -n kyverno rollout status deploy/kyverno-admission-controller --timeout=300s
fi
# E3: cert-manager + the kaddy-local-ca ClusterIssuer are already installed
# by kind-up.sh (hack/cluster/install-cert-manager.sh, pinned
# CERT_MANAGER_VERSION) — only the ACME issuer is applied on top.
- name: Apply ACME staging issuer (tls suites)
if: ${{ hashFiles('tests/chainsaw/tls/*.yaml') != '' }}
run: |
# Only the STAGING issuer is exercised by the tls suite. Applying the
# prod issuer too would register an account against the Let's Encrypt
# PRODUCTION ACME API on every PR run — unnecessary prod-API traffic —
# so CI installs staging only. (Live/kind bootstrap applies both.)
kubectl apply -f deploy/cert-manager/cluster-issuer-staging.yaml
# E3: install the observability stack (monitoring suites) directly with Helm,
# reusing the SAME pinned chart versions + values embedded in the Argo CD
# Application manifests (extracted with yq) so CI mirrors the GitOps source of
# truth without standing up Argo CD in the ephemeral cluster.
- name: Install observability stack (monitoring suites)
if: ${{ hashFiles('tests/chainsaw/monitoring/*.yaml') != '' }}
run: |
set -euo pipefail
if ! command -v yq >/dev/null 2>&1; then
# SEC-4: pin to an exact release (was releases/latest) so the install
# is reproducible and Renovate can track the bump.
YQ_VERSION="v4.44.3"
sudo wget -qO /usr/local/bin/yq \
"https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_amd64"
sudo chmod +x /usr/local/bin/yq
fi
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo add grafana https://grafana.github.io/helm-charts
helm repo update
kubectl create namespace monitoring --dry-run=client -o yaml | kubectl apply -f -
# E1c: kps values point Grafana at grafana.admin.existingSecret
# (monitoring/grafana-admin). The live cluster gets it from
# `task bootstrap:e1c`; the ephemeral CI cluster must create it too or
# Grafana never turns Ready and `helm --wait` times out.
kubectl -n monitoring create secret generic grafana-admin \
--from-literal=admin-user=admin \
--from-literal=admin-password="$(openssl rand -base64 24)" \
--dry-run=client -o yaml | kubectl apply -f -
install_app() {
local file="$1"
local repo chart ver
repo="$(yq e '.spec.source.repoURL' "$file")"
chart="$(yq e '.spec.source.chart' "$file")"
ver="$(yq e '.spec.source.targetRevision' "$file")"
yq e '.spec.source.helm.valuesObject' "$file" > /tmp/vals.yaml
helm upgrade --install "$chart" "$chart" --repo "$repo" \
--version "$ver" -n monitoring -f /tmp/vals.yaml --wait --timeout 8m
}
install_app deploy/observability/kube-prometheus-stack.yaml
install_app deploy/observability/loki.yaml
install_app deploy/observability/alloy.yaml
# The Grafana Loki datasource ConfigMap is a plain manifest (not Helm).
kubectl apply -f deploy/observability/grafana-loki-datasource.yaml
# E7: Argo Rollouts controller + the Gateway API trafficRouting plugin —
# same vendored+pinned manifests the live cluster syncs (deploy/rollouts/).
- name: Install Argo Rollouts + Gateway API plugin (rollouts suites)
if: ${{ hashFiles('tests/chainsaw/rollouts/*.yaml') != '' }}
run: |
set -euo pipefail
kubectl apply -f deploy/rollouts/namespace.yaml
# ARCH NOTE: deploy/rollouts/config.yaml pins the plugin binary for the
# LIVE kind node arch (linux-arm64 — Apple-silicon podman). GitHub
# runners are linux-amd64, so rewrite ONLY the arch suffix of the same
# pinned v0.16.0 release asset at apply time. The live manifest stays
# byte-identical; no floating tag is introduced (SEC-4).
sed 's#gatewayapi-plugin-linux-arm64#gatewayapi-plugin-linux-amd64#' \
deploy/rollouts/config.yaml | kubectl apply -f -
# Server-side apply: the Rollout CRD exceeds client-side annotation
# limits. -n argo-rollouts: the vendored upstream install.yaml carries
# NO namespace fields (live, the Argo CD Application destination
# supplies it — deploy/apps/rollouts.yaml). The plugin ConfigMap is
# applied BEFORE the controller starts, so it is loaded on first boot —
# no `rollout restart` needed (live needs one because Argo CD may sync
# the CM after the controller).
kubectl apply --server-side -n argo-rollouts -f deploy/rollouts/install.yaml
kubectl -n argo-rollouts rollout status deploy/argo-rollouts --timeout=300s
# E4/E7: the platform edge + demo workloads the un-skipped suites assert
# against — applied from the SAME GitOps manifests the live cluster syncs.
- name: Apply platform edge + demo workloads (gateway/tls/security/rollouts suites)
if: ${{ hashFiles('tests/chainsaw/gateway/*.yaml', 'tests/chainsaw/security/*.yaml', 'tests/chainsaw/tls/*.yaml', 'tests/chainsaw/rollouts/*.yaml') != '' }}
run: |
set -euo pipefail
# E4 edge: gateway ns + Gateway + HTTPRoutes + clubhouse app + the
# kaddy-local-ca-issued Certificate (gateway + tls + security suites).
# Namespace FIRST: `kubectl apply -f <dir>` walks files alphabetically,
# so namespace.yaml would otherwise land after the resources it holds.
kubectl apply -f deploy/gateway/namespace.yaml
kubectl apply -f deploy/gateway/
kubectl apply -f deploy/workloads/clubhouse/
kubectl apply -f deploy/cert-manager/clubhouse-certificate.yaml
kubectl -n gateway rollout status deploy/clubhouse --timeout=300s
kubectl -n gateway wait --for=condition=Ready certificate/clubhouse-tls --timeout=180s
kubectl -n gateway wait --for=condition=Programmed gateway/clubhouse --timeout=180s
# E7 mulligan progressive-delivery demo (needs the Rollouts CRDs above).
if ls tests/chainsaw/rollouts/*.yaml >/dev/null 2>&1; then
kubectl apply -f deploy/workloads/mulligan/namespace.yaml
kubectl apply -f deploy/workloads/mulligan/
kubectl -n mulligan wait --for=condition=Programmed gateway/mulligan --timeout=180s
# Both Rollouts must be Healthy before the suites patch/abort them.
kubectl -n mulligan wait --for=jsonpath='{.status.phase}'=Healthy \
rollout/mulligan rollout/mulligan-bg --timeout=300s
fi
# E1c: the default-deny NetworkPolicy baseline (security suites) — applied
# AFTER the stacks are up, mirroring the live cutover order. Cilium (the CI
# CNI since parity) actually enforces these.
- name: Apply default-deny network baseline (security suites)
if: ${{ hashFiles('tests/chainsaw/security/*.yaml') != '' }}
run: |
set -euo pipefail
# No Argo CD runs in the ephemeral CI cluster; create the bare argocd
# namespace so its namespace-wide deny floor applies and the
# default-deny suite asserts the same namespace set as live.
kubectl create namespace argocd --dry-run=client -o yaml | kubectl apply -f -
kubectl apply -f deploy/policies/network/gateway.yaml
kubectl apply -f deploy/policies/network/monitoring.yaml
kubectl apply -f deploy/policies/network/argocd.yaml
# websites.yaml is NOT applied: the websites namespace belongs to the
# Crossplane suite, which stays live-only (no Crossplane in CI).
- name: Run Chainsaw suites (default discovery — chainsaw-test.yaml)
run: task test:chainsaw
- name: Run per-scenario Chainsaw suites (non-default file names)
# LATENT-GAP FIX (found by this lane): `chainsaw test <dir>` discovers
# ONLY files named chainsaw-test.yaml, so the per-scenario suites
# (gateway/security/monitoring/rollouts/tls/crossplane — one file per
# scenario, the tests/chainsaw/README authoring style) were NEVER
# executed by `task test:chainsaw`, in CI or anywhere. Run every
# Test-bearing file explicitly — the same invocation each suite's
# live-verification annotation documents. Sequential by construction
# (the rollouts suites mutate shared mulligan objects). skip:true
# suites still skip. Follow-up: fold this into the Taskfile target.
run: |
set -euo pipefail
grep -l '^kind: Test$' \
tests/chainsaw/{gateway,security,monitoring,rollouts,tls,crossplane}/*.yaml \
| sort | while read -r f; do
echo "::group::chainsaw ${f}"
chainsaw test --test-file "$(basename "$f")" "$(dirname "$f")"
echo "::endgroup::"
done