Files
pulse/.github/workflows/helm-pages.yml
T
2026-08-31 13:21:25 +01:00

266 lines
11 KiB
YAML

name: Release Helm Chart to GitHub Pages
run-name: Release Helm Chart ${{ inputs.chart_version }}
# Called only from release-convergence.yml while it holds the global customer-
# promotion lease. Chart lint, packaging, OCI publication, and the exact-
# candidate install/upgrade smoke complete before activation. This workflow
# verifies and promotes that immutable source-run artifact after activation.
on:
workflow_call:
inputs:
chart_version:
description: "Exact chart version (e.g., 6.3.0-rc.5)."
required: true
type: string
source_release_run_id:
description: "Exact create-release.yml run containing the qualified chart."
required: true
type: string
target_commitish:
description: "Exact activated release source commit."
required: true
type: string
chart_digest:
description: "OCI chart digest committed by release activation."
required: true
type: string
permissions:
actions: read
contents: write
packages: read
jobs:
release:
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- name: Checkout release verification control
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
persist-credentials: false
- name: Require activated GitHub release and source run
id: identity
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ inputs.chart_version }}
SOURCE_RELEASE_RUN_ID: ${{ inputs.source_release_run_id }}
TARGET_COMMITISH: ${{ inputs.target_commitish }}
CHART_DIGEST: ${{ inputs.chart_digest }}
run: |
set -euo pipefail
if [[ -z "${VERSION}" ]] || \
[[ ! "${SOURCE_RELEASE_RUN_ID}" =~ ^[0-9]+$ ]] || \
[[ ! "${TARGET_COMMITISH}" =~ ^[0-9a-f]{40}$ ]] || \
[[ ! "${CHART_DIGEST}" =~ ^sha256:[0-9a-f]{64}$ ]]; then
echo "::error::Helm Pages requires an exact version, source run ID, commit SHA, and chart digest."
exit 1
fi
release_tag="v${VERSION}"
release_state="$(
gh api "repos/${{ github.repository }}/releases/tags/${release_tag}" \
--jq '[.tag_name, .target_commitish, (.draft | tostring), (.published_at // "")] | @tsv'
)"
actual_tag="$(awk -F '\t' '{print $1}' <<<"${release_state}")"
actual_commit="$(awk -F '\t' '{print $2}' <<<"${release_state}")"
is_draft="$(awk -F '\t' '{print $3}' <<<"${release_state}")"
published_at="$(awk -F '\t' '{print $4}' <<<"${release_state}")"
if [[ "${actual_tag}" != "${release_tag}" ]] || \
[[ "${actual_commit}" != "${TARGET_COMMITISH}" ]] || \
[[ "${is_draft}" != "false" ]] || \
[[ -z "${published_at}" ]]; then
echo "::error::Helm Pages refuses to advertise inactive or mismatched release ${release_tag}."
exit 1
fi
marker="$(mktemp)"
curl -fsSL --retry 4 --retry-delay 2 --retry-all-errors \
-o "${marker}" \
"https://github.com/${{ github.repository }}/releases/download/${release_tag}/release-activation.json"
jq -e \
--arg tag "${release_tag}" \
--arg target_commitish "${TARGET_COMMITISH}" \
--arg source_release_run_id "${SOURCE_RELEASE_RUN_ID}" \
--arg helm_chart_digest "${CHART_DIGEST}" \
'.schema_version == 1 and .tag == $tag and .target_commitish == $target_commitish and .source_release_run_id == $source_release_run_id and .helm_chart_digest == $helm_chart_digest' \
"${marker}" >/dev/null
rm -f "${marker}"
source_run="$(
gh api "repos/${{ github.repository }}/actions/runs/${SOURCE_RELEASE_RUN_ID}" \
--jq '[.path, .event, .head_sha] | @tsv'
)"
run_path="$(awk -F '\t' '{print $1}' <<<"${source_run}")"
run_event="$(awk -F '\t' '{print $2}' <<<"${source_run}")"
run_sha="$(awk -F '\t' '{print $3}' <<<"${source_run}")"
if [[ "${run_path}" != ".github/workflows/create-release.yml" ]] || \
[[ "${run_event}" != "workflow_dispatch" ]] || \
[[ "${run_sha}" != "${TARGET_COMMITISH}" ]]; then
echo "::error::Source release run ${SOURCE_RELEASE_RUN_ID} does not own the activated ${release_tag} chart."
exit 1
fi
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "release_tag=${release_tag}" >> "$GITHUB_OUTPUT"
- name: Download immutable qualified chart
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ steps.identity.outputs.version }}
SOURCE_RELEASE_RUN_ID: ${{ inputs.source_release_run_id }}
run: |
set -euo pipefail
mkdir -p dist
gh run download "${SOURCE_RELEASE_RUN_ID}" \
--repo "${{ github.repository }}" \
--name "pulse-chart-${VERSION}" \
--dir dist
- name: Set up Helm
uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0
with:
version: v3.15.2
- name: Verify immutable chart identity
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ steps.identity.outputs.version }}
CHART_PATH: dist/pulse-${{ steps.identity.outputs.version }}.tgz
RELEASE_TAG: ${{ steps.identity.outputs.release_tag }}
TARGET_COMMITISH: ${{ inputs.target_commitish }}
CHART_DIGEST: ${{ inputs.chart_digest }}
run: |
set -euo pipefail
proof="$(./scripts/verify-release-helm-chart.sh \
"${RELEASE_TAG}" "${TARGET_COMMITISH}" \
"${GITHUB_REPOSITORY}" "${CHART_DIGEST}")"
if [ "${proof}" != "chart_digest=${CHART_DIGEST}" ]; then
echo "::error::Activated Helm OCI provenance did not produce the committed digest."
exit 1
fi
python3 - <<'PY'
import os
import pathlib
import re
import tarfile
chart_path = pathlib.Path(os.environ["CHART_PATH"])
version = os.environ["VERSION"]
if not chart_path.is_file():
raise SystemExit(f"missing immutable qualified chart: {chart_path}")
with tarfile.open(chart_path, "r:gz") as archive:
for member in archive.getmembers():
parts = pathlib.PurePosixPath(member.name).parts
if member.name.startswith("/") or any(part in {"", ".", ".."} for part in parts):
raise SystemExit(f"unsafe chart member: {member.name}")
stream = archive.extractfile(archive.getmember("pulse/Chart.yaml"))
if stream is None:
raise SystemExit("qualified chart has no readable pulse/Chart.yaml")
metadata = stream.read().decode("utf-8")
def value(key: str) -> str:
match = re.search(rf"(?m)^{re.escape(key)}:\s*[\"']?([^\"'\s]+)", metadata)
return match.group(1) if match else ""
if value("name") != "pulse" or value("version") != version or value("appVersion") != version:
raise SystemExit("qualified chart metadata does not match the activated release")
PY
sha256sum "${CHART_PATH}"
helm show chart "${CHART_PATH}"
- name: Check out current Pages index
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
ref: gh-pages
path: gh-pages
fetch-depth: 1
persist-credentials: true # required: authenticated git writes
- name: Publish chart release and merge Pages index
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ steps.identity.outputs.version }}
TARGET_COMMITISH: ${{ inputs.target_commitish }}
run: |
set -euo pipefail
chart="pulse-${VERSION}.tgz"
chart_path="dist/${chart}"
chart_release="helm-chart-${VERSION}"
chart_release_url="https://github.com/${{ github.repository }}/releases/download/${chart_release}"
if gh release view "${chart_release}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
gh release upload "${chart_release}" "${chart_path}" --clobber \
--repo "${GITHUB_REPOSITORY}"
else
gh release create "${chart_release}" "${chart_path}" \
--repo "${GITHUB_REPOSITORY}" \
--target "${TARGET_COMMITISH}" \
--title "Helm chart ${VERSION}" \
--notes "Helm chart for Pulse ${VERSION}." \
--prerelease \
--latest=false
fi
gh release edit "${chart_release}" --prerelease --latest=false \
--repo "${GITHUB_REPOSITORY}"
git -C gh-pages config user.name "$GITHUB_ACTOR"
git -C gh-pages config user.email "$GITHUB_ACTOR@users.noreply.github.com"
index_work="$(mktemp -d)"
cleanup() { rm -rf "${index_work}"; }
trap cleanup EXIT
cp "${chart_path}" "${index_work}/${chart}"
if [[ -f gh-pages/index.yaml ]]; then
cp gh-pages/index.yaml "${index_work}/index.yaml"
helm repo index "${index_work}" \
--url "${chart_release_url}" \
--merge "${index_work}/index.yaml"
else
helm repo index "${index_work}" --url "${chart_release_url}"
fi
cp "${index_work}/index.yaml" gh-pages/index.yaml
if ! grep -q "version: ${VERSION}" gh-pages/index.yaml; then
echo "::error::Helm Pages index is missing version ${VERSION}."
exit 1
fi
git -C gh-pages add index.yaml
if git -C gh-pages diff --cached --quiet; then
echo "Helm Pages index already contains ${VERSION}."
else
git -C gh-pages commit -m "Update Helm chart index for ${VERSION}"
git -C gh-pages push origin HEAD:gh-pages
fi
- name: Verify public Pages chart
env:
VERSION: ${{ steps.identity.outputs.version }}
run: |
set -euo pipefail
public_repo="https://rcourtman.github.io/Pulse"
public_ready=false
for attempt in $(seq 1 30); do
public_index="$(mktemp)"
if curl -fsSL --retry 2 --retry-delay 1 --retry-all-errors \
-o "${public_index}" "${public_repo}/index.yaml" && \
grep -q "version: ${VERSION}" "${public_index}"; then
helm repo remove pulse-public >/dev/null 2>&1 || true
helm repo add pulse-public "${public_repo}" --force-update
helm repo update pulse-public
if helm show chart pulse-public/pulse --version "${VERSION}" >/dev/null; then
public_ready=true
rm -f "${public_index}"
break
fi
fi
rm -f "${public_index}"
echo "Public Helm repository has not exposed ${VERSION} yet (${attempt}/30)."
sleep 2
done
if [[ "${public_ready}" != "true" ]]; then
echo "::error::Public Helm repository did not expose chart ${VERSION}."
exit 1
fi
echo "[OK] Public Helm repository serves pulse ${VERSION}."