diff --git a/.github/configs/helm-install-test-allow-failure.txt b/.github/configs/helm-install-test-allow-failure.txt new file mode 100644 index 000000000..ae390c14d --- /dev/null +++ b/.github/configs/helm-install-test-allow-failure.txt @@ -0,0 +1,10 @@ +# Charts with a KNOWN install/upgrade failure in a bare, credential-less kind +# cluster. The install-test gate reports these as a warning instead of failing, +# so the gate can protect the other charts and all future changes. Remove a chart +# from this list once its issue is fixed — a clean chart must stay clean. +# +# Format: # reason +# +tracer # PodDisruptionBudget sets BOTH minAvailable and maxUnavailable — the API server rejects it ("cannot be both set"). Real chart bug: the PDB template/values must emit only one. +reporter # References external KEDA custom resources (keda.sh/v1alpha1: ScaledObject / ScaledJob / TriggerAuthentication) whose CRDs are not present in a bare kind cluster. Needs the KEDA CRDs pre-installed (a future enhancement) or the chart guarded behind a toggle. +otel-collector-lerian # DaemonSet renders a DUPLICATE env var GOMEMLIMIT — server-side apply rejects it ("duplicate entries for key GOMEMLIMIT"). Real chart bug: drop the duplicate (likely set in both the chart values and the opentelemetry-collector subchart). diff --git a/.github/scripts/install-test.sh b/.github/scripts/install-test.sh new file mode 100755 index 000000000..b5e3c5354 --- /dev/null +++ b/.github/scripts/install-test.sh @@ -0,0 +1,113 @@ +#!/usr/bin/env bash +# install-test.sh — install + upgrade a chart into the current kube context. +# Core value over `helm template`: the API server validates every manifest +# server-side (rejects invalid/immutable/admission-failing objects), and the +# upgrade path catches immutable-field breaks. Does NOT wait for app pods to +# become Ready (images are private; readiness is a separate, creds-gated concern). +# +# Usage: install-test.sh [values-file] +set -uo pipefail + +CHART_DIR="$1" +CHART="$(basename "$CHART_DIR")" +# Values: explicit arg wins; else the render-gate's vetted sample values, if any. +VALUES_FILE="${2:-}" +[[ -z "$VALUES_FILE" && -f ".github/configs/helm-render-values/${CHART}.yaml" ]] \ + && VALUES_FILE=".github/configs/helm-render-values/${CHART}.yaml" +REL="$CHART" +NS="it-${CHART}" # release namespace (helm -n); also created +TARGETS="" # every distinct namespace the chart's manifests reference (created too) +TIMEOUT="${IT_TIMEOUT:-180s}" +VARGS=() +[[ -n "$VALUES_FILE" && -f "$VALUES_FILE" ]] && { VARGS=(-f "$VALUES_FILE"); echo " values: $VALUES_FILE"; } +# --no-hooks: hook Jobs (migrations/bootstrap) pull private images and need real +# backing services, so they never complete in a credential-less CI cluster. We +# validate the steady-state manifests server-side; hook Jobs are out of scope here. +HOOKS=(--no-hooks) + +# Library charts are not installable. +if grep -qiE '^type:[[:space:]]*library([[:space:]]|$)' "$CHART_DIR/Chart.yaml"; then + echo "::notice::$CHART is a library chart — skipping install test." + exit 0 +fi + +fail() { echo "::error::[$CHART] $1"; kubectl get events -n "$NS" --sort-by=.lastTimestamp 2>/dev/null | tail -15; cleanup; exit 1; } +cleanup() { + helm uninstall "$REL" -n "$NS" >/dev/null 2>&1 || true + helm uninstall "${REL}-base" -n "$NS" >/dev/null 2>&1 || true + for x in "$NS" $TARGETS; do kubectl delete ns "$x" --wait=false >/dev/null 2>&1 || true; done +} +# (Re)create the release namespace + every namespace the chart pins, then install. +do_install() { # + for x in "$NS" $TARGETS; do kubectl create ns "$x" >/dev/null 2>&1 || true; done + helm install "$1" "$2" ${VARGS[@]+"${VARGS[@]}"} "${HOOKS[@]}" -n "$NS" --timeout "$TIMEOUT" 2>&1 +} +deployed() { [[ "$(helm status "$1" -n "$NS" -o json 2>/dev/null | tr -d ' \n' | grep -o '"status":"[a-z]*"' | head -1)" == '"status":"deployed"' ]]; } + +echo "===== [$CHART] dependency build =====" +# `helm dependency build` needs every HTTP dependency repo registered first +# (oci:// and file:// deps don't). Mirror what the render-gate's Go tool does: +# register each `repository: https://…` from Chart.yaml before building. +depn=0 +while IFS= read -r repo_url; do + [[ -z "$repo_url" ]] && continue + helm repo add "dep${depn}" "$repo_url" >/dev/null 2>&1 || true + depn=$((depn + 1)) +done < <(grep -E 'repository:[[:space:]]*"?https?://' "$CHART_DIR/Chart.yaml" | grep -Eo 'https?://[^"[:space:]]+' | sort -u) +[[ "$depn" -gt 0 ]] && helm repo update >/dev/null 2>&1 +# Build with one retry (dependency fetches are network-flaky) and surface the real +# error on final failure instead of a bare "build failed". +db_out="$(helm dependency build "$CHART_DIR" 2>&1)" \ + || db_out="$(helm dependency update "$CHART_DIR" 2>&1)" \ + || { echo "$db_out" | tail -8 | sed 's/^/ /'; fail "helm dependency build failed"; } + +# Lerian charts pin their own namespaces (namespaceOverride / global.namespace), so +# resources land there regardless of `-n` — and some charts even span MORE than one +# (most in the release ns, a few in a fixed one). Create EVERY namespace the render +# references instead of fighting it; the release itself lives in $NS. +TARGETS="$(helm template "$REL" "$CHART_DIR" ${VARGS[@]+"${VARGS[@]}"} "${HOOKS[@]}" -n "$NS" 2>/dev/null \ + | awk '/^ namespace:/{gsub(/"/,"",$2); print $2}' | awk 'NF' | sort -u | grep -vxF "$NS" | tr '\n' ' ')" +echo " namespaces: $NS${TARGETS:+ + $TARGETS}" + +cleanup # idempotent: clear any stale release/namespace from a prior aborted run + +# ---- 1. Fresh install of the PR chart (server-side manifest validation) ---- +echo "===== [$CHART] install (PR) =====" +do_install "$REL" "$CHART_DIR" || fail "helm install failed (invalid manifest / admission / hook)" +deployed "$REL" || fail "release not in deployed state" +n=0; for x in "$NS" $TARGETS; do n=$((n + $(kubectl get all -n "$x" --no-headers 2>/dev/null | wc -l))); done +echo " created $n objects" +[[ "$n" -gt 0 ]] || fail "install produced no objects" + +# ---- 2. Upgrade the PR chart in place (upgrade code path) ---- +# Same values on purpose: a no-change upgrade still re-renders and re-applies +# (new revision, STATUS deployed). Forcing a value change is unsafe — a strict +# root-closed schema (e.g. br-sfn) rejects an injected podAnnotations key. +echo "===== [$CHART] upgrade (PR -> PR) =====" +helm upgrade "$REL" "$CHART_DIR" ${VARGS[@]+"${VARGS[@]}"} "${HOOKS[@]}" -n "$NS" --timeout "$TIMEOUT" >/dev/null 2>&1 \ + && deployed "$REL" || fail "in-place upgrade failed" + +# Free the PR release before the baseline so only ONE release is ever installed at +# a time — two full installs of a subchart-heavy chart exhaust a single-node kind +# cluster's memory. The baseline reuses the same (chart-pinned) namespace. +cleanup + +# ---- 3. Real upgrade path: base (origin/main) -> PR, when the chart exists on main ---- +if git cat-file -e "origin/main:charts/${CHART}/Chart.yaml" 2>/dev/null; then + echo "===== [$CHART] upgrade (origin/main -> PR) =====" + BASE_DIR="$(mktemp -d)/$CHART"; mkdir -p "$BASE_DIR" + git archive "origin/main" "charts/${CHART}" | tar -x --strip-components=2 -C "$BASE_DIR" 2>/dev/null + helm dependency build "$BASE_DIR" >/dev/null 2>&1 || echo " (base dep build failed — skipping baseline)" + if do_install "${REL}-base" "$BASE_DIR" >/dev/null 2>&1 && deployed "${REL}-base"; then + helm upgrade "${REL}-base" "$CHART_DIR" ${VARGS[@]+"${VARGS[@]}"} "${HOOKS[@]}" -n "$NS" --timeout "$TIMEOUT" >/dev/null 2>&1 \ + && deployed "${REL}-base" || fail "upgrade from origin/main failed (immutable-field break?)" + echo " origin/main -> PR upgrade OK" + else + echo " (baseline install failed — likely unrelated to this PR; skipping)" + fi +else + echo " (new chart — not on origin/main; skipping baseline upgrade)" +fi + +echo "===== [$CHART] OK =====" +cleanup diff --git a/.github/workflows/helm-install-test.yml b/.github/workflows/helm-install-test.yml new file mode 100644 index 000000000..33ea8135c --- /dev/null +++ b/.github/workflows/helm-install-test.yml @@ -0,0 +1,116 @@ +name: Helm Install Test + +# Installs each changed chart into a throwaway kind cluster and exercises the +# upgrade path. The value over the render-gate (which only runs `helm template`) +# is that the real API server VALIDATES every manifest server-side — it rejects +# invalid field values, bad API versions, and immutable-field changes on upgrade +# that `helm template` renders happily. Hook Jobs (migrations/bootstrap) are +# skipped (--no-hooks in the script): they pull private images and need real +# backing services, so they cannot complete in a credential-less CI cluster. +# App pods are NOT waited on for the same reason — steady-state manifest +# validation + the upgrade path are what this gate proves. + +on: + pull_request: + paths: + - "charts/**" + - ".github/configs/helm-render-values/**" + - ".github/scripts/install-test.sh" + - ".github/workflows/helm-install-test.yml" + workflow_dispatch: {} + +concurrency: + group: helm-install-test-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + install-test: + name: Install + upgrade (kind) + runs-on: blacksmith-4vcpu-ubuntu-2404 + timeout-minutes: 30 # a full --all sweep (workflow/lib change) installs every app chart sequentially + steps: + - name: Checkout code + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + fetch-depth: 0 # full history so the script can `git archive origin/main` for the baseline upgrade + + - name: Fetch base branch + run: git fetch --no-tags --depth=1 origin main + + - name: Setup Helm + uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4 + + - name: Create kind cluster + uses: helm/kind-action@a1b0e391336a6ee6713a0583f8c6240d70863de3 # v1.10.0 + with: + cluster_name: chart-install-test + + - name: Determine chart scope + id: scope + shell: bash + env: + EVENT_NAME: ${{ github.event_name }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.sha }} + run: | + set -euo pipefail + all_app_charts() { + for d in charts/*/; do + c="$(basename "$d")" + grep -qiE '^type:[[:space:]]*library([[:space:]]|$)' "charts/$c/Chart.yaml" 2>/dev/null && continue + printf '%s ' "$c" + done + } + if [[ "$EVENT_NAME" != "pull_request" ]]; then + echo "charts=$(all_app_charts)" >> "$GITHUB_OUTPUT"; exit 0 + fi + changed="$(git diff --name-only "$BASE_SHA" "$HEAD_SHA")" + # An infra change (script/workflow/shared library/fixtures) can affect every + # chart, so test them all; otherwise only the charts whose files changed. + if grep -Eq '^(\.github/scripts/install-test\.sh|\.github/workflows/helm-install-test\.yml|charts/lerian-common/)' <<< "$changed"; then + echo "charts=$(all_app_charts)" >> "$GITHUB_OUTPUT"; exit 0 + fi + scope="" + for c in $(awk -F/ '/^charts\/[^/]+\// {print $2}' <<< "$changed" | sort -u); do + [[ -d "charts/$c" ]] || continue # skip deleted charts + grep -qiE '^type:[[:space:]]*library([[:space:]]|$)' "charts/$c/Chart.yaml" && continue + scope="${scope:+$scope }$c" + done + echo "charts=$scope" >> "$GITHUB_OUTPUT" + + - name: Install + upgrade each chart + shell: bash + env: + CHARTS: ${{ steps.scope.outputs.charts }} + run: | + set -uo pipefail + if [[ -z "${CHARTS// }" ]]; then + echo "No installable charts in scope — nothing to test."; exit 0 + fi + # Charts with a KNOWN pre-existing install/upgrade defect (documented in the + # allow-failure file) are reported but don't fail the gate — same baseline + # pattern the chart-standard check uses. A PR that touches such a chart should + # remove it from the file once fixed. New/changed clean charts must still pass. + ALLOW="$(grep -vE '^\s*(#|$)' .github/configs/helm-install-test-allow-failure.txt 2>/dev/null | awk '{print $1}' || true)" + echo "Charts in scope: $CHARTS" + fails=""; allowed_fails="" + for c in $CHARTS; do + echo "::group::install-test $c" + if bash .github/scripts/install-test.sh "charts/$c"; then + echo "::endgroup::" + elif grep -qxF "$c" <<< "$ALLOW"; then + echo "::endgroup::"; echo "::warning::[$c] install test failed but is allow-listed (known pre-existing defect)" + allowed_fails="${allowed_fails:+$allowed_fails }$c" + else + echo "::endgroup::"; fails="${fails:+$fails }$c" + fi + done + [[ -n "$allowed_fails" ]] && echo "Allow-listed failures (not blocking):$allowed_fails" + if [[ -n "$fails" ]]; then + echo "::error::Install/upgrade test failed for:$fails" + exit 1 + fi + echo "All charts in scope installed and upgraded cleanly (allow-listed excepted)."