diff --git a/.github/workflows/helm-validate.yml b/.github/workflows/helm-validate.yml new file mode 100644 index 000000000..d69df41eb --- /dev/null +++ b/.github/workflows/helm-validate.yml @@ -0,0 +1,124 @@ +# Helm chart validation — MANDATORY, BLOCKING. +# +# Required status check on `main` (see scripts/ci/enforce-quality-gates.sh). +# +# Exists to make two real incident classes impossible to merge: +# 1. appVersion computed as "---" (fixed in #1303) — guarded by a strict +# format check on every Chart.yaml. +# 2. Charts that lint/template only on the happy path — guarded by +# `helm lint --strict` + a full `helm template` render of the umbrella. + +name: Helm Validate + +on: + pull_request: + branches: [main] + paths: + - "charts/**" + - "src/backend/services/*/helm/**" + - "src/frontend/helm/**" + - "helmfile/charts/**" + - ".github/workflows/helm-validate.yml" + workflow_dispatch: + +permissions: + contents: read + +env: + KUBECONFORM_VERSION: v0.6.7 + KUBECONFORM_SHA256: 95f14e87aa28c09d5941f11bd024c1d02fdc0303ccaa23f61cef67bc92619d73 + +jobs: + helm-validate: + name: helm-validate + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - uses: azure/setup-helm@fe7b79cd5ee1e45176fcad797de68ecaf3ca4814 # v4.2.0 + with: + version: v3.14.0 + + - name: Guard appVersion / version format in every Chart.yaml + run: | + set -euo pipefail + rc=0 + while IFS= read -r chart; do + # vendored upstream subcharts (e.g. clickhouse 25.3) carry their own non-semver versions — not ours to enforce + case "$chart" in helmfile/charts/*) continue ;; esac + app_version=$(yq -r '.appVersion // ""' "$chart") + version=$(yq -r '.version // ""' "$chart") + # version must be plain semver X.Y.Z — every chart uses that today + # (0.1.0 / 0.1.72). Widen here if we ever ship a pre-release suffix. + if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error file=$chart::invalid chart version '$version' (expected X.Y.Z)"; rc=1 + fi + # appVersion (when present) must be the release BUILD TAG that + # publish-chart stamps: YYYY.MM.DD.HH.MM-. This is the only + # form our charts use; the check exists to reject the "---" + # placeholder (#1303) and anything else that isn't a real build tag. + if [[ -n "$app_version" ]] && \ + ! [[ "$app_version" =~ ^[0-9]{4}\.[0-9]{2}\.[0-9]{2}\.[0-9]{2}\.[0-9]{2}-[0-9a-f]{7}$ ]]; then + echo "::error file=$chart::invalid appVersion '$app_version' (expected build tag YYYY.MM.DD.HH.MM-; the '---' bug class)"; rc=1 + fi + done < <(git ls-files '*/Chart.yaml' 'Chart.yaml') + exit $rc + + - name: Forbid :latest image tags in chart values + run: | + set -euo pipefail + rc=0 + while IFS= read -r f; do + # Strip comments first so commented examples don't trip the guard. + clean=$(sed -E 's/[[:space:]]#.*$//; s/^[[:space:]]*#.*$//' "$f") + # explicit :latest on an image reference (frontend blank-page defect class) + if grep -nE '^[[:space:]]*image:[[:space:]]*.*:latest("?[[:space:]]*)$' <<<"$clean"; then + echo "::error file=$f::':latest' image reference (frontend blank-page defect class)"; rc=1 + fi + # NOTE: an empty `tag: ""` in a base values.yaml is the repo's + # deliberate convention — the operator / release-train MUST supply the + # tag at deploy (CI renders supply it via ci/ci-values.yaml). So an + # empty tag is "must override", not a floating ref, and is NOT flagged + # here; the umbrella fail-fasts if the operator forgets to set it. + done < <(git ls-files 'charts/**/values.yaml' 'src/**/helm/values.yaml') + exit $rc + + - name: helm lint (strict) — umbrella and all local subcharts + run: | + set -euo pipefail + helm dependency update charts/insight + helm lint --strict charts/insight + # helmfile/ was removed (#1431); only the umbrella + service subcharts + # remain. `|| continue` so an unmatched glob's false test can't become + # the loop's (and the step's) exit status under `set -e`. + for c in src/backend/services/*/helm src/frontend/helm; do + [ -f "$c/Chart.yaml" ] || continue + helm lint --strict "$c" + done + + - name: helm template — full render must succeed with CI values + run: | + set -euo pipefail + # The umbrella fail-fasts without required inputs (infra image tags, + # DB names, tenant) — by design, not a bug, so a bare render can't + # succeed. ci/ci-values.yaml supplies the minimal set a real deploy + # would, giving the render + kubeconform gate valid manifests to + # check. These are render-only placeholders, not deploy defaults. + helm template insight charts/insight \ + --namespace insight \ + -f charts/insight/ci/ci-values.yaml \ + > /tmp/rendered.yaml + echo "rendered $(grep -c '^kind:' /tmp/rendered.yaml) objects" + + - name: kubeconform — rendered manifests must be valid Kubernetes + run: | + set -euo pipefail + # Verified download: fetch to disk, check SHA256, then extract & run. + curl -sSL -o kubeconform.tgz \ + "https://github.com/yannh/kubeconform/releases/download/${KUBECONFORM_VERSION}/kubeconform-linux-amd64.tar.gz" + echo "${KUBECONFORM_SHA256} kubeconform.tgz" | sha256sum -c - + tar xzf kubeconform.tgz kubeconform + ./kubeconform -strict -ignore-missing-schemas -summary /tmp/rendered.yaml diff --git a/charts/insight/ci/ci-values.yaml b/charts/insight/ci/ci-values.yaml new file mode 100644 index 000000000..fbe0d89e7 --- /dev/null +++ b/charts/insight/ci/ci-values.yaml @@ -0,0 +1,59 @@ +# Minimal values that let `helm template` render the umbrella in CI. +# +# The chart intentionally leaves infra image tags, DB names and the tenant +# UNSET (each marked "MUST be set") and fail-fasts without them — that is +# correct hygiene, not a bug, so a bare `helm template` cannot succeed. This +# file supplies the smallest set a real deploy would, purely so the render / +# kubeconform gate has valid manifests to check. These are NOT deploy defaults. +global: + tenantDefaultId: "11111111-1111-1111-1111-111111111111" + +ingestion: + reconcile: + tenantId: "ci-validate" + +clickhouse: + # Render-only placeholder: the chart leaves host UNSET ("MUST be set") and + # fail-fasts, so supply an in-cluster FQDN purely so the umbrella renders. + host: "clickhouse.insight.svc.cluster.local" + database: "insight" + username: "default" + image: + tag: "25.3" + +mariadb: + # Render-only placeholder (host is UNSET / "MUST be set" in the chart). + host: "mariadb.insight.svc.cluster.local" + database: "identity" + username: "identity" + # The bitnami subchart creates the user from auth.*; the umbrella validator + # requires these match the umbrella's mariadb.username / mariadb.database. + auth: + username: "identity" + database: "identity" + +# Render-only placeholders for the remaining infra hosts the chart leaves UNSET. +redis: + host: "redis.insight.svc.cluster.local" + +redpanda: + brokers: "redpanda.insight.svc.cluster.local:9093" + +# Render without requiring real OIDC wiring (the validator demands all four +# OIDC fields when auth is on); CI only needs the manifests to render + validate. +apiGateway: + authDisabled: true + image: + tag: "0.0.0-ci" + +analyticsApi: + image: + tag: "0.0.0-ci" + +identity: + image: + tag: "0.0.0-ci" + +frontend: + image: + tag: "0.0.0-ci"