From 1ba7bb06a0f6fa4a94ea6e29c9eb17379fc629e5 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sun, 5 Jul 2026 09:40:20 +0100 Subject: [PATCH] Verify public Helm chart publication --- .github/workflows/README.md | 1 + .github/workflows/publish-helm-chart.yml | 18 +++++---------- .../subsystems/deployment-installability.md | 7 ++++++ .../installtests/build_release_assets_test.go | 23 ++++++++++++++++++- .../release_promotion_policy_test.py | 8 +++++++ 5 files changed, 44 insertions(+), 13 deletions(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index a1ecfcf9f..8b863cfd4 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -141,4 +141,5 @@ Packages the Helm chart and pushes it to the GitHub Container Registry (OCI) whe - Triggered automatically on `release: published`, or manually via workflow dispatch (requires `chart_version` input) - Chart and app versions mirror the Pulse release tag (e.g., `v4.24.0` → `4.24.0`) - Publishes to `oci://ghcr.io//pulse-chart` +- Verifies the pushed OCI chart can be read from GHCR without registry credentials - Requires no additional secrets—uses the built-in `GITHUB_TOKEN` with `packages: write` permission diff --git a/.github/workflows/publish-helm-chart.yml b/.github/workflows/publish-helm-chart.yml index 4c745741d..6a5adca37 100644 --- a/.github/workflows/publish-helm-chart.yml +++ b/.github/workflows/publish-helm-chart.yml @@ -146,19 +146,13 @@ jobs: helm push dist/pulse-${{ steps.versions.outputs.chart_version }}.tgz \ oci://ghcr.io/${{ github.repository_owner }}/pulse-chart - - name: Configure package visibility - env: - GITHUB_TOKEN: ${{ github.token }} + - name: Verify public GHCR chart read run: | - # Connect package to repository and set visibility to public - # This ensures the package inherits public visibility and appears in repo packages - gh api -X PUT /user/packages/container/pulse-chart/versions/latest/restore || true - gh api -X PATCH /user/packages/container/pulse-chart -f visibility=public || true - - # Also try org endpoint if user endpoint fails - gh api -X PATCH /orgs/${{ github.repository_owner }}/packages/container/pulse-chart -f visibility=public || true - - echo "Package visibility configuration attempted. Verify at: https://github.com/${{ github.repository_owner }}?tab=packages" + set -euo pipefail + helm registry logout ghcr.io || true + helm show chart \ + oci://ghcr.io/${{ github.repository_owner }}/pulse-chart/pulse \ + --version "${{ steps.versions.outputs.chart_version }}" - name: Attach chart to release if: github.event_name == 'release' diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index b98352ca6..122ccaffb 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -647,6 +647,13 @@ TLS floor in the dynamic config. `release` and `workflow_dispatch` triggers, and its chart-version resolver must prefer inputs over the release-event tag when inputs are present so all three entry paths converge on the same identity. + After pushing the OCI chart, `publish-helm-chart.yml` must prove the + pushed chart is readable from GHCR without registry credentials by logging + out of `ghcr.io` and running `helm show chart` against the versioned chart + reference. The workflow must not mask package-visibility drift with + best-effort GitHub Packages visibility API calls: invalid or unauthorized + visibility endpoints create false success and noisy release logs, while the + unauthenticated chart read is the customer-facing availability contract. `create-release.yml` must apply the same explicit `workflow_call` to `promote-floating-tags.yml`. Its legacy `workflow_run` chain off `publish-docker.yml` silently stops promoting `latest` / major / minor diff --git a/scripts/installtests/build_release_assets_test.go b/scripts/installtests/build_release_assets_test.go index fcc544634..89493fbae 100644 --- a/scripts/installtests/build_release_assets_test.go +++ b/scripts/installtests/build_release_assets_test.go @@ -1144,7 +1144,8 @@ func TestPromoteFloatingTagsReachableViaWorkflowCall(t *testing.T) { } func TestPublishHelmChartReachableViaWorkflowCall(t *testing.T) { - assertFileContainsAll(t, repoFile(".github", "workflows", "publish-helm-chart.yml"), + workflowPath := repoFile(".github", "workflows", "publish-helm-chart.yml") + assertFileContainsAll(t, workflowPath, `workflow_call:`, `chart_version:`, `description: "Chart version (e.g., 6.0.0-rc.5). Required for workflow_call."`, @@ -1154,7 +1155,27 @@ func TestPublishHelmChartReachableViaWorkflowCall(t *testing.T) { // Chart-version resolver prefers inputs over release-event tag. `if [ -n "${INPUT_CHART_VERSION}" ]; then`, `RELEASE_TAG="${RELEASE_TAG_NAME}"`, + `name: Verify public GHCR chart read`, + `helm registry logout ghcr.io || true`, + `helm show chart`, + `oci://ghcr.io/${{ github.repository_owner }}/pulse-chart/pulse`, + `--version "${{ steps.versions.outputs.chart_version }}"`, ) + + content, err := os.ReadFile(workflowPath) + if err != nil { + t.Fatalf("read publish-helm-chart.yml: %v", err) + } + workflow := string(content) + for _, forbidden := range []string{ + `versions/latest/restore`, + `-f visibility=public`, + `Package visibility configuration attempted`, + } { + if strings.Contains(workflow, forbidden) { + t.Fatalf("publish-helm-chart.yml must verify chart readability instead of masking GHCR visibility API failures; found %q", forbidden) + } + } } func TestCreateReleasePublishesPrivateProRuntime(t *testing.T) { diff --git a/scripts/release_control/release_promotion_policy_test.py b/scripts/release_control/release_promotion_policy_test.py index 6f3cbd7a7..0ff6eb33e 100644 --- a/scripts/release_control/release_promotion_policy_test.py +++ b/scripts/release_control/release_promotion_policy_test.py @@ -807,6 +807,14 @@ class ReleasePromotionPolicyTest(unittest.TestCase): self.assertIn("--chart deploy/helm/pulse/Chart.yaml", helm) self.assertIn("--chart deploy/helm/pulse/Chart.yaml", helm_pages) self.assertIn('git checkout --detach "refs/tags/${RELEASE_TAG}"', helm) + self.assertIn("Verify public GHCR chart read", helm) + self.assertIn("helm registry logout ghcr.io || true", helm) + self.assertIn("helm show chart", helm) + self.assertIn("oci://ghcr.io/${{ github.repository_owner }}/pulse-chart/pulse", helm) + self.assertIn('--version "${{ steps.versions.outputs.chart_version }}"', helm) + self.assertNotIn("versions/latest/restore", helm) + self.assertNotIn("-f visibility=public", helm) + self.assertNotIn("Package visibility configuration attempted", helm) self.assertNotIn("blob/main/docs/KUBERNETES.md", chart) self.assertNotIn("raw.githubusercontent.com/rcourtman/Pulse/main/docs/images/pulse-logo.svg", chart) self.assertIn("blob/{tag}/docs/KUBERNETES.md", chart_sync)