Files
pulse/.github/workflows/publish-helm-chart.yml
T
courtmanr@gmail.com 6ce41ae9d4 Fold the echo-only release joins into their public writers
candidate_qualification and release_readiness only restated other jobs'
results. publish_release_tag, publish_docker, publish_helm_chart and
activate_release now each carry the exact candidate predicate in their own
needs and if, guarded by !cancelled() with the same allowed integration
skip, and activation joins the tag and both registry publications directly.
The commit verdict restates every candidate result in place of the
readiness join.

recover-release-activation.yml accepts both shapes. A source run that has
release_readiness still has to prove only that join, as before. A run
without it has to prove publish_release_tag and at least one, and only
successful, jobs under the publish_docker and publish_helm_chart caller IDs.

Failure diagnostics and the inspection-only Helm chart artifact, which
nothing downloads, are kept for three days instead of 14 and 90.
2026-10-01 21:47:58 +01:00

244 lines
9.5 KiB
YAML

name: Publish Helm Chart
# Triggers:
# - release: published — fires when a release is initially created with
# draft=false (legacy publish path).
# - workflow_call — called from create-release.yml after candidate qualification
# and qualified public tag creation succeed. create-release.yml's publish step creates releases as drafts and
# PATCHes to draft=false later, which does NOT fire the `release: published`
# webhook (GitHub-documented quirk). Without this workflow_call trigger,
# v6 rc.1 → rc.5 published successfully but never produced a Helm chart on
# rcourtman.github.io/Pulse/index.yaml — customers running
# `helm install pulse pulse/pulse --version 6.0.0-rc.5` got chart-not-found.
# - workflow_dispatch — manual backfill / re-publish path.
on:
release:
types: [published]
workflow_call:
inputs:
chart_version:
description: "Chart version (e.g., 6.0.0-rc.5). Required for workflow_call."
required: true
type: string
app_version:
description: "Application version (must equal chart version; defaults to it)."
required: false
type: string
default: ""
outputs:
chart_digest:
description: "Verified OCI manifest digest for the exact-version chart."
value: ${{ jobs.publish.outputs.chart_digest }}
workflow_dispatch:
inputs:
chart_version:
description: "Chart version (required when running manually, use format 4.24.0)"
required: true
app_version:
description: "Application version (must equal chart version; defaults to it)"
required: false
permissions:
contents: read
jobs:
publish:
name: Package and Push Helm Chart
runs-on: ubuntu-24.04
timeout-minutes: 30
outputs:
chart_digest: ${{ steps.proof.outputs.chart_digest }}
permissions:
contents: write # Required for gh release upload
packages: write
id-token: write
attestations: write
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0
fetch-tags: true
- name: Determine chart version
id: versions
env:
INPUT_CHART_VERSION: ${{ inputs.chart_version }}
INPUT_APP_VERSION: ${{ inputs.app_version }}
RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
run: |
# inputs.chart_version is set by both workflow_dispatch and
# workflow_call (from create-release.yml). Fall back to the
# release-event tag only when running from a `release: published`
# webhook with no inputs.
if [ -n "${INPUT_CHART_VERSION}" ]; then
CHART_VERSION="${INPUT_CHART_VERSION}"
APP_VERSION="${INPUT_APP_VERSION}"
if [ -z "$APP_VERSION" ]; then
APP_VERSION="$CHART_VERSION"
fi
RELEASE_TAG="v${CHART_VERSION}"
else
RELEASE_TAG="${RELEASE_TAG_NAME}"
if [ -z "$RELEASE_TAG" ]; then
echo "::error::No chart_version input supplied and release event has no tag_name"
exit 1
fi
CHART_VERSION="${RELEASE_TAG#v}"
APP_VERSION="$CHART_VERSION"
fi
# Pulse's server and agent image defaults use Chart.AppVersion.
# Source provenance alone cannot detect a mismatched image override.
if [ "$APP_VERSION" != "$CHART_VERSION" ]; then
echo "::error::Release chart app_version must equal chart_version."
exit 1
fi
IS_PRERELEASE="false"
if [[ "$APP_VERSION" =~ -rc\.[0-9]+$ ]] || [[ "$APP_VERSION" =~ -alpha\.[0-9]+$ ]] || [[ "$APP_VERSION" =~ -beta\.[0-9]+$ ]]; then
IS_PRERELEASE="true"
fi
python3 scripts/write_github_output.py chart_version "$CHART_VERSION"
python3 scripts/write_github_output.py app_version "$APP_VERSION"
python3 scripts/write_github_output.py release_tag "$RELEASE_TAG"
echo "is_prerelease=$IS_PRERELEASE" >> "$GITHUB_OUTPUT"
- name: Set up Helm
uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1
with:
version: v3.15.2
- name: Validate release line policy
id: release_line
env:
RELEASE_TAG: ${{ steps.versions.outputs.release_tag }}
run: |
set -euo pipefail
python3 scripts/release_control/validate_artifact_release_line.py \
--tag "${RELEASE_TAG}" \
--purpose "Helm publish" \
--github-output "$GITHUB_OUTPUT"
- name: Check out validated release tag
env:
RELEASE_TAG: ${{ steps.versions.outputs.release_tag }}
run: |
set -euo pipefail
release_sha="$(git rev-parse "refs/tags/${RELEASE_TAG}^{commit}")"
if [ "${release_sha}" != "${GITHUB_SHA}" ]; then
echo "::error::Helm provenance source ${GITHUB_SHA} does not match ${RELEASE_TAG} at ${release_sha}. Dispatch manual backfills from the exact tag ref."
exit 1
fi
git checkout --detach "refs/tags/${RELEASE_TAG}"
- name: Align chart metadata links
env:
WORKFLOW_OUTPUT_1: ${{ steps.versions.outputs.chart_version }}
run: |
python3 scripts/sync_chart_release_metadata.py \
--chart deploy/helm/pulse/Chart.yaml \
--version "${WORKFLOW_OUTPUT_1}" \
--repo "${{ github.repository }}"
- name: Helm lint (strict)
run: helm lint deploy/helm/pulse --strict
- name: Package chart
env:
WORKFLOW_OUTPUT_1: ${{ steps.versions.outputs.chart_version }}
WORKFLOW_OUTPUT_2: ${{ steps.versions.outputs.app_version }}
run: |
mkdir -p dist
helm package deploy/helm/pulse \
--version "${WORKFLOW_OUTPUT_1}" \
--app-version "${WORKFLOW_OUTPUT_2}" \
--destination dist
- name: Upload packaged chart artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: pulse-chart-${{ steps.versions.outputs.chart_version }}
path: dist/pulse-${{ steps.versions.outputs.chart_version }}.tgz
# Inspection copy only. The chart is published to GHCR and Pages, and
# no job or workflow downloads this artifact.
retention-days: 3
- name: Authenticate with GHCR
env:
GHCR_TOKEN: ${{ github.token }}
GHCR_USERNAME: ${{ github.actor }}
run: |
echo "${GHCR_TOKEN}" | helm registry login ghcr.io --username "${GHCR_USERNAME}" --password-stdin
# Helm and actions/attest use separate credential stores. Keep the
# chart push authenticated through Helm, and authenticate the OCI
# attestation client explicitly before it publishes provenance.
- name: Authenticate OCI attestation client with GHCR
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Push exact-version chart to GHCR
id: push
env:
CHART_PATH: dist/pulse-${{ steps.versions.outputs.chart_version }}.tgz
run: |
set -euo pipefail
output="$(
helm push "${CHART_PATH}" \
oci://ghcr.io/${{ github.repository_owner }}/pulse-chart 2>&1
)"
printf '%s\n' "${output}"
mapfile -t digests < <(
sed -nE 's/^Digest:[[:space:]]*(sha256:[0-9a-f]{64})\r?$/\1/p' \
<<<"${output}" | sort -u
)
if [ "${#digests[@]}" -ne 1 ]; then
echo "::error::Helm push did not report one exact OCI chart digest."
exit 1
fi
echo "chart_digest=${digests[0]}" >> "$GITHUB_OUTPUT"
- name: Attest exact-version OCI chart
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
with:
subject-name: ghcr.io/${{ github.repository_owner }}/pulse-chart/pulse
subject-digest: ${{ steps.push.outputs.chart_digest }}
push-to-registry: true
create-storage-record: false
- name: Verify public GHCR chart identity and provenance
id: proof
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ steps.versions.outputs.release_tag }}
EXPECTED_DIGEST: ${{ steps.push.outputs.chart_digest }}
run: |
set -euo pipefail
helm registry logout ghcr.io || true
proof_output="$(./scripts/verify-release-helm-chart.sh \
"${RELEASE_TAG}" "${GITHUB_SHA}" \
"${GITHUB_REPOSITORY}" "${EXPECTED_DIGEST}")"
if [[ ! "${proof_output}" =~ ^chart_digest=sha256:[0-9a-f]{64}$ ]]; then
echo "::error::Helm verification did not return one exact chart digest."
exit 1
fi
printf '%s\n' "${proof_output}" >> "$GITHUB_OUTPUT"
- name: Attach chart to release
if: github.event_name == 'release'
env:
GITHUB_TOKEN: ${{ github.token }}
WORKFLOW_OUTPUT_1: ${{ steps.versions.outputs.release_tag }}
WORKFLOW_OUTPUT_2: ${{ steps.versions.outputs.chart_version }}
run: |
gh release upload "${WORKFLOW_OUTPUT_1}" \
"dist/pulse-${WORKFLOW_OUTPUT_2}.tgz" \
--clobber