A chart in ci/excluded-charts.txt got an install-test job that ran the script, printed SKIP and exited 0. GitHub knows only the exit code, so the check list showed "Install-test satisfactory OK" for a chart whose image was never pulled - indistinguishable from one that really came up in the cluster (#231). detect-changed-charts now removes those charts before the matrix is built, so no job exists for them and nothing claims they passed. An absence says nothing by itself, so the detect job writes every excluded chart of the PR with its documented justification into the job summary. A "Skipped" would only say "not run"; the reason is the question a review actually asks. The list is not parsed in YAML. The workflow calls ci/run-install-test.sh --list-excluded, the one parser, and compares whole strings with jq, so a chart name is never read as a pattern. The script's own exclusion branch now goes through the same excluded_reason() helper instead of a grep that used the chart name as a regex; it stays for local runs, where the CI filter does not apply. Not viable, and recorded at the matrix as a comment: deciding per chart in an "if" on the install-test job, which would give a visible Skipped entry instead of an absent one. GitHub rejects it - jobs.<id>.if is evaluated before the matrix is expanded, so the matrix context does not exist there ("context \"matrix\" is not allowed here. available contexts are \"github\", \"inputs\", \"needs\", \"vars\"."). Run 33650724109 on the closed PR #233 ended in failure with zero jobs created.
308 lines
16 KiB
YAML
308 lines
16 KiB
YAML
name: Validate Helm Charts
|
|
on:
|
|
pull_request:
|
|
|
|
jobs:
|
|
detect-changed-charts:
|
|
name: Detect changed charts
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
charts: ${{ steps.detect.outputs.charts }}
|
|
any-chart-changed: ${{ steps.detect.outputs.any-chart-changed }}
|
|
scan-charts: ${{ steps.detect.outputs.scan-charts }}
|
|
any-scan: ${{ steps.detect.outputs.any-scan }}
|
|
install-charts: ${{ steps.detect.outputs.install-charts }}
|
|
any-install: ${{ steps.detect.outputs.any-install }}
|
|
steps:
|
|
- name: "Checkout Repository"
|
|
uses: actions/checkout@v7
|
|
with:
|
|
fetch-depth: 0
|
|
- name: "Detect changed charts"
|
|
id: detect
|
|
run: |
|
|
CHANGED_FILES=$(git diff --name-only "origin/${{ github.base_ref }}...HEAD")
|
|
CHARTS="[]"
|
|
CHANGED_DIRS=$(echo "$CHANGED_FILES" | cut -d/ -f1 | sort -u)
|
|
for DIR in $CHANGED_DIRS; do
|
|
if [ -f "$DIR/Chart.yaml" ]; then
|
|
CHARTS=$(echo "$CHARTS" | jq -c --arg dir "$DIR" '. + [$dir]')
|
|
fi
|
|
done
|
|
echo "charts=$CHARTS" >> "$GITHUB_OUTPUT"
|
|
if [ "$CHARTS" = "[]" ]; then
|
|
echo "any-chart-changed=false" >> "$GITHUB_OUTPUT"
|
|
echo "No chart directories changed - nothing to build."
|
|
else
|
|
echo "any-chart-changed=true" >> "$GITHUB_OUTPUT"
|
|
echo "Changed charts: $CHARTS"
|
|
fi
|
|
|
|
# The Trivy scan normally follows the changed charts. But a PR that
|
|
# changes the scanner itself touches no chart at all, and would leave
|
|
# the change untested. In that case scan a fixed sample instead, one
|
|
# chart per interesting case: a documented deviation (samba), a fully
|
|
# hardened one (cyberchef), and one with two workloads
|
|
# (matrix-synapse).
|
|
SCAN_CHARTS="$CHARTS"
|
|
if [ "$CHARTS" = "[]" ] && echo "$CHANGED_FILES" | grep -qE '^(ci/(run-trivy-config\.sh|trivy/)|\.github/workflows/validate-charts\.yml)'; then
|
|
SCAN_CHARTS='["samba","cyberchef","matrix-synapse"]'
|
|
echo "Scanner changed without a chart change - scanning the sample: $SCAN_CHARTS"
|
|
fi
|
|
echo "scan-charts=$SCAN_CHARTS" >> "$GITHUB_OUTPUT"
|
|
if [ "$SCAN_CHARTS" = "[]" ]; then
|
|
echo "any-scan=false" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "any-scan=true" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
# --- The install-test matrix: changed charts minus the excluded ones
|
|
# (issue #231). A chart in ci/excluded-charts.txt used to get a job
|
|
# that ran the script, printed SKIP and exited 0 - and GitHub knows
|
|
# only the exit code, so the check list showed a green tick for a
|
|
# chart nothing was ever attempted on. Dropping it here means no job
|
|
# exists for it and nothing claims it passed.
|
|
#
|
|
# The list is NOT parsed here. ci/run-install-test.sh --list-excluded
|
|
# emits "<chart><TAB><reason>", and that script is the one parser;
|
|
# re-implementing the format in YAML would be a second chance to
|
|
# select different charts, silently. The comparison below is jq's
|
|
# "!=" on whole strings, so a chart name is never read as a pattern.
|
|
INSTALL_CHARTS="$CHARTS"
|
|
SUMMARY_ROWS=""
|
|
while IFS=$'\t' read -r EX_NAME EX_REASON; do
|
|
[ -n "$EX_NAME" ] || continue
|
|
if [ "$(echo "$INSTALL_CHARTS" | jq -r --arg c "$EX_NAME" 'index($c) != null')" = "true" ]; then
|
|
# A "|" in the justification would break out of the table cell.
|
|
SUMMARY_ROWS="${SUMMARY_ROWS}| \`$EX_NAME\` | ${EX_REASON//|/\\|} |"$'\n'
|
|
echo "Excluded from the install-test: $EX_NAME - $EX_REASON"
|
|
fi
|
|
INSTALL_CHARTS="$(echo "$INSTALL_CHARTS" | jq -c --arg c "$EX_NAME" 'map(select(. != $c))')"
|
|
done <<< "$(ci/run-install-test.sh --list-excluded)"
|
|
echo "install-charts=$INSTALL_CHARTS" >> "$GITHUB_OUTPUT"
|
|
if [ "$INSTALL_CHARTS" = "[]" ]; then
|
|
echo "any-install=false" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "any-install=true" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
# An absent job says "not run" but never why, and an absence is easy
|
|
# to miss entirely. So every excluded chart of this PR is named here
|
|
# WITH its documented justification - which is the question a review
|
|
# actually asks. This summary is the visible half of the change; the
|
|
# dropped matrix entry is only the half that stops the lie.
|
|
if [ -n "$SUMMARY_ROWS" ]; then
|
|
{
|
|
echo "### Charts excluded from the install-test"
|
|
echo
|
|
echo "These charts are changed by this PR but listed in \`ci/excluded-charts.txt\`, so they get **no** install-test job. There is no green tick for them, because nothing was attempted (issue #231)."
|
|
echo
|
|
echo "| Chart | Documented reason |"
|
|
echo "| --- | --- |"
|
|
printf '%s' "$SUMMARY_ROWS"
|
|
echo
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
fi
|
|
echo "Install-test matrix: $INSTALL_CHARTS"
|
|
|
|
build-chart:
|
|
name: "Build ${{ matrix.chart }}"
|
|
needs: detect-changed-charts
|
|
if: needs.detect-changed-charts.outputs.any-chart-changed == 'true'
|
|
runs-on: ubuntu-latest
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
chart: ${{ fromJSON(needs.detect-changed-charts.outputs.charts) }}
|
|
steps:
|
|
- name: "Checkout Repository"
|
|
uses: actions/checkout@v7
|
|
with:
|
|
fetch-depth: 0
|
|
- name: "Install Helm"
|
|
uses: azure/setup-helm@v5
|
|
# Both steps below refuse to run while a chart declares a dependency that
|
|
# is not in its charts/ directory - lint with "[WARNING] chart directory
|
|
# is missing these dependencies", package with "found in Chart.yaml, but
|
|
# missing in charts/ directory", both exiting 1. Charts without a
|
|
# dependencies: block are skipped without a word, so this costs nothing
|
|
# for the rest of the repo. See ci/helm-dependencies.sh for why it is
|
|
# "update" and not "build".
|
|
- name: "Fetch chart dependencies"
|
|
run: ci/helm-dependencies.sh "${{ matrix.chart }}"
|
|
- name: "Lint Helm Chart"
|
|
run: helm lint --strict "${{ matrix.chart }}"
|
|
- name: "Package Helm Chart"
|
|
run: helm package "${{ matrix.chart }}"
|
|
- name: "Check Chart Version was bumped"
|
|
# template-chart is a scaffold and never published, its version is meaningless
|
|
if: matrix.chart != 'template-chart'
|
|
run: |
|
|
BASE_VERSION=$(git show "origin/${{ github.base_ref }}:${{ matrix.chart }}/Chart.yaml" 2>/dev/null | grep '^version:' | awk '{print $2}' || true)
|
|
NEW_VERSION=$(grep '^version:' "${{ matrix.chart }}/Chart.yaml" | awk '{print $2}')
|
|
if [ -z "$BASE_VERSION" ]; then
|
|
echo "New chart (does not exist on ${{ github.base_ref }}), version: $NEW_VERSION"
|
|
elif [ "$BASE_VERSION" = "$NEW_VERSION" ]; then
|
|
echo "::error file=${{ matrix.chart }}/Chart.yaml::Chart '${{ matrix.chart }}' was modified but its version was not bumped (still $BASE_VERSION). Bump 'version:' in Chart.yaml, otherwise the publish on main will collide with the already published version."
|
|
exit 1
|
|
else
|
|
echo "Version bump OK: $BASE_VERSION -> $NEW_VERSION"
|
|
fi
|
|
|
|
quantity-render:
|
|
name: "Resource quantities"
|
|
runs-on: ubuntu-latest
|
|
# The one job in this workflow that deliberately ignores the changed-chart
|
|
# matrix and runs over *every* chart (issue #157). The bug it guards
|
|
# against - resource quantities formatted with printf "%v", where float64
|
|
# flips to exponential notation above ~1e6 - lives in the resources helper,
|
|
# and that helper is copied into every chart's templates/_helpers.tpl,
|
|
# because charts here have no dependencies. So it comes back by copy: a new
|
|
# chart started from an older one, or from template-chart before the fix.
|
|
# In that PR the chart is new and its copy of the bug is in no diff anyone
|
|
# reads. Running over everything also means there is no list to maintain -
|
|
# adding a chart enrolls it. Two renders per chart, no cluster, a few
|
|
# seconds in total; it depends on no other job and finishes long before
|
|
# build-chart, so it adds no wall-clock time to the pipeline.
|
|
steps:
|
|
- name: "Checkout Repository"
|
|
uses: actions/checkout@v7
|
|
- name: "Install Helm"
|
|
uses: azure/setup-helm@v5
|
|
- name: "Render every chart with bare integer quantities"
|
|
run: ci/run-quantity-render-test.sh
|
|
|
|
trivy-config:
|
|
name: "Trivy config ${{ matrix.chart }}"
|
|
needs: detect-changed-charts
|
|
if: needs.detect-changed-charts.outputs.any-scan == 'true'
|
|
runs-on: ubuntu-latest
|
|
# Reporting only (issue #84): the scan renders each changed chart and lists
|
|
# what `trivy config` finds, but it never fails the job - see
|
|
# TRIVY_CONFIG_MODE below. Deliberately not a gate yet: the repo has several
|
|
# documented deviations, and a check that goes red on those every time is one
|
|
# people learn to scroll past. Depends only on detect-changed-charts, so it
|
|
# runs beside build-chart and adds no wall-clock time to the pipeline.
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
chart: ${{ fromJSON(needs.detect-changed-charts.outputs.scan-charts) }}
|
|
env:
|
|
# renovate: datasource=github-releases depName=aquasecurity/trivy
|
|
TRIVY_VERSION: v0.74.0
|
|
# "report" always exits 0. Switch to "gate" to make a finding that no
|
|
# documented exception covers fail the job - the intended step once the
|
|
# seccomp gap (#84) is closed.
|
|
TRIVY_CONFIG_MODE: report
|
|
steps:
|
|
- name: "Checkout Repository"
|
|
uses: actions/checkout@v7
|
|
- name: "Install Helm"
|
|
uses: azure/setup-helm@v5
|
|
- name: "Install Trivy"
|
|
uses: aquasecurity/setup-trivy@v0.3.1
|
|
with:
|
|
version: ${{ env.TRIVY_VERSION }}
|
|
cache: true
|
|
# Findings land in the job summary, not only in the log: exceptions with
|
|
# their reasons come from ci/trivy/trivyignore.yaml, the trusted-registry
|
|
# list from ci/trivy/data/ksv0125.yaml.
|
|
- name: "Scan rendered manifests"
|
|
run: ci/run-trivy-config.sh "${{ matrix.chart }}"
|
|
|
|
install-test:
|
|
name: "Install-test ${{ matrix.chart }}"
|
|
needs: [detect-changed-charts, build-chart]
|
|
if: needs.detect-changed-charts.outputs.any-install == 'true'
|
|
runs-on: ubuntu-latest
|
|
# Blocking since Renovate automerges patch/minor updates: a chart that no
|
|
# longer starts must stop the PR before it reaches main and publishes. This
|
|
# ends the stabilization phase of issue #22 - the job reported failures
|
|
# without blocking while it proved itself across several chart PRs.
|
|
timeout-minutes: 30
|
|
strategy:
|
|
fail-fast: false
|
|
# "install-charts", not "charts": the changed charts minus the ones in
|
|
# ci/excluded-charts.txt, filtered in detect-changed-charts. Excluded
|
|
# charts are named with their reason in that job's summary (issue #231).
|
|
#
|
|
# DO NOT try to move that decision into an "if" on this job, e.g.
|
|
# if: ... && !contains(fromJSON(needs.detect-changed-charts.outputs.excluded-charts), matrix.chart)
|
|
# to get a visible "Skipped" per chart instead of an absent entry.
|
|
# GitHub rejects it - jobs.<id>.if is evaluated BEFORE the matrix is
|
|
# expanded, so the matrix context does not exist there:
|
|
# context "matrix" is not allowed here.
|
|
# available contexts are "github", "inputs", "needs", "vars".
|
|
# Measured on run 33650724109 (PR #233, closed): the whole workflow ended
|
|
# in failure with zero jobs created. The per-chart value has to be gone
|
|
# from the matrix by the time this job is reached.
|
|
matrix:
|
|
chart: ${{ fromJSON(needs.detect-changed-charts.outputs.install-charts) }}
|
|
env:
|
|
K3D_VERSION: v5.9.0
|
|
K3S_IMAGE: docker.io/rancher/k3s:v1.35.5-k3s1
|
|
steps:
|
|
- name: "Checkout Repository"
|
|
uses: actions/checkout@v7
|
|
- name: "Install Helm"
|
|
uses: azure/setup-helm@v5
|
|
# freesailarr is the one chart whose install-test needs credentials:
|
|
# without a real Wireguard key gluetun never completes a handshake, and
|
|
# behind its kill switch not a single app container becomes Ready. The key
|
|
# comes from 1Password Connect like the registry credentials in
|
|
# publish-helm-chart.yml, and it is a CI-ONLY Mullvad key: Mullvad ties an
|
|
# address to a key and sends the tunnel to the endpoint it saw last, so
|
|
# reusing the production key would let every PR run pull the tunnel out
|
|
# from under the running stack - with the symptom in the cluster and the
|
|
# cause in a pull request. Loaded only for this chart; no other chart run
|
|
# touches the VPN key.
|
|
- name: "Configure OP-Connect"
|
|
if: matrix.chart == 'freesailarr'
|
|
uses: 1password/load-secrets-action/configure@v5
|
|
with:
|
|
connect-host: https://op-connect.jk-dev.de
|
|
- name: "Load freesailarr VPN credentials"
|
|
id: load-freesailarr-vpn
|
|
if: matrix.chart == 'freesailarr'
|
|
uses: 1password/load-secrets-action@v5
|
|
env:
|
|
OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN }}
|
|
FREESAILARR_WG_KEY: op://GitHub-Actions/freesailarr-mullvad-test-config/credential
|
|
FREESAILARR_WG_ADDRESSES: op://GitHub-Actions/freesailarr-mullvad-test-config/ip-address
|
|
# The same trap publish-helm-chart.yml documents at length: v5 of
|
|
# load-secrets-action does NOT export the loaded values as environment
|
|
# variables for later steps, only its step outputs carry them, and the
|
|
# loading step logs "Populating variable" and stays green either way. So
|
|
# the values are mapped into each step's own env block via
|
|
# steps.<id>.outputs.* and read as quoted shell variables. The ":?" guards
|
|
# run before the cluster is even created and name the op:// field that is
|
|
# missing - an empty key would otherwise reach gluetun and look like a
|
|
# broken chart instead of a missing secret. They print the variable name,
|
|
# never its value.
|
|
- name: "Verify the VPN credentials arrived"
|
|
if: matrix.chart == 'freesailarr'
|
|
env:
|
|
FREESAILARR_WG_KEY: ${{ steps.load-freesailarr-vpn.outputs.FREESAILARR_WG_KEY }}
|
|
FREESAILARR_WG_ADDRESSES: ${{ steps.load-freesailarr-vpn.outputs.FREESAILARR_WG_ADDRESSES }}
|
|
run: |-
|
|
: "${FREESAILARR_WG_KEY:?empty - check op://GitHub-Actions/freesailarr-mullvad-test-config/credential}"
|
|
: "${FREESAILARR_WG_ADDRESSES:?empty - check op://GitHub-Actions/freesailarr-mullvad-test-config/ip-address}"
|
|
echo "Both VPN values arrived non-empty."
|
|
- name: "Install k3d"
|
|
run: curl -sSfL https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | TAG="$K3D_VERSION" bash
|
|
- name: "Create k3d cluster"
|
|
run: k3d cluster create install-test --image "$K3S_IMAGE" --wait --timeout 180s
|
|
# Every chart reaching this step is one the exclusion filter let through,
|
|
# so the script's SKIP branch is never taken here - it exists for local
|
|
# runs. The variables below are empty for every chart but the one that
|
|
# declares them in ci/<chart>/settings.env (REQUIRED_ENV); the script
|
|
# fails loudly if a declared one is missing, it never skips over it.
|
|
- name: "Install chart and wait for readiness"
|
|
env:
|
|
FREESAILARR_WG_KEY: ${{ steps.load-freesailarr-vpn.outputs.FREESAILARR_WG_KEY }}
|
|
FREESAILARR_WG_ADDRESSES: ${{ steps.load-freesailarr-vpn.outputs.FREESAILARR_WG_ADDRESSES }}
|
|
run: ci/run-install-test.sh "${{ matrix.chart }}"
|
|
- name: "Delete k3d cluster"
|
|
if: always()
|
|
run: k3d cluster delete install-test || true
|