From 37d00cc6fe0638eeac8a08d887c07c462100a804 Mon Sep 17 00:00:00 2001 From: Zeke Date: Fri, 24 Jul 2026 08:06:53 -0700 Subject: [PATCH 1/2] feat(release): publish the Helm chart to GHCR OCI + keyless cosign sign (P2) Adds .github/workflows/chart-release.yml: on a `v*` tag (or manual dispatch) it packages the chart, pushes it to GHCR as an OCI artifact (oci://ghcr.io//charts/ironcache:), and keyless-signs the by-DIGEST ref with cosign (Sigstore/Fulcio via GitHub OIDC -- no long-lived keys, matching the repo's existing id-token: write signing posture). On a pull request it PACKAGES + lints only (validates the release path without publishing), so the packaging + the cosign action are exercised in PR CI. Consumers can now: helm install ic oci://ghcr.io/elares/charts/ironcache --version 0.3.0 cosign verify ghcr.io/elares/charts/ironcache:0.3.0 \ --certificate-identity-regexp 'https://github.com/ELares/IronCache/.*' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com Updates the chart README install section (OCI + cosign-verify path alongside the git-checkout path; drops the stale "not published to any registry" note). Actions are pinned per the release convention (checkout + docker/login-action by SHA reused from image.yml/release.yml; setup-helm + cosign-installer by tag). Verified locally: the chart packages cleanly to a 35KB tgz with the schema / k3s overlay / dashboards / tests all included (version 0.3.0, appVersion 0.1.0). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01FfFZ8gkkNhDBASuntB72HR --- .github/workflows/chart-release.yml | 78 +++++++++++++++++++++++++++++ deploy/helm/ironcache/README.md | 25 ++++++--- 2 files changed, 97 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/chart-release.yml diff --git a/.github/workflows/chart-release.yml b/.github/workflows/chart-release.yml new file mode 100644 index 0000000..83a7e4d --- /dev/null +++ b/.github/workflows/chart-release.yml @@ -0,0 +1,78 @@ +# SPDX-License-Identifier: MIT OR Apache-2.0 +# +# Publish the Helm chart to GHCR as an OCI artifact and keyless-sign it with cosign +# (Sigstore/Fulcio via GitHub OIDC -- no long-lived keys). The chart VERSION comes from +# Chart.yaml (independent of the app version), so consumers install a specific, immutable +# chart release: +# +# helm install ic oci://ghcr.io/elares/charts/ironcache --version +# cosign verify ghcr.io/elares/charts/ironcache: \ +# --certificate-identity-regexp 'https://github.com/ELares/IronCache/.*' \ +# --certificate-oidc-issuer https://token.actions.githubusercontent.com +# +# Runs on a `v*` release tag (publishes the current Chart.yaml version) and on manual dispatch. +# On a pull request it PACKAGES + lints only (validates the release path without publishing). +name: chart-release + +on: + push: + tags: + - "v*" + workflow_dispatch: + pull_request: + paths: + - "deploy/helm/ironcache/**" + - ".github/workflows/chart-release.yml" + +permissions: + contents: read + +jobs: + publish: + name: package + push + sign the chart (GHCR OCI) + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + packages: write # push the chart to ghcr.io//charts + id-token: write # keyless cosign signing via OIDC + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: azure/setup-helm@v4 + with: + version: "v3.15.4" + - name: Install cosign + uses: sigstore/cosign-installer@v3.7.0 + - name: helm lint + package + id: pkg + run: | + set -euo pipefail + helm lint deploy/helm/ironcache + helm package deploy/helm/ironcache -d "${RUNNER_TEMP}/chart" + tgz="$(ls "${RUNNER_TEMP}/chart"/ironcache-*.tgz)" + echo "tgz=${tgz}" >> "$GITHUB_OUTPUT" + echo "Packaged: ${tgz}" + # Everything below PUBLISHES -- skip it on pull_request (validate the path only). + - name: Log in to GHCR + if: github.event_name != 'pull_request' + uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: helm push + cosign sign (keyless) + if: github.event_name != 'pull_request' + env: + OWNER: ${{ github.repository_owner }} + run: | + set -euo pipefail + owner_lc="$(echo "$OWNER" | tr '[:upper:]' '[:lower:]')" + repo="ghcr.io/${owner_lc}/charts" + # helm push reads the docker config credentials set by docker/login-action above. + helm push "${{ steps.pkg.outputs.tgz }}" "oci://${repo}" 2>&1 | tee "${RUNNER_TEMP}/push.log" + # helm push prints `Digest: sha256:...`; sign the immutable by-digest ref so the + # signature binds the exact bytes, not a mutable tag. + digest="$(grep -oE 'sha256:[a-f0-9]{64}' "${RUNNER_TEMP}/push.log" | head -1)" + [ -n "$digest" ] || { echo "ERROR: could not parse the pushed digest"; exit 1; } + echo "Signing ${repo}/ironcache@${digest}" + cosign sign --yes "${repo}/ironcache@${digest}" diff --git a/deploy/helm/ironcache/README.md b/deploy/helm/ironcache/README.md index f569a70..81f4728 100644 --- a/deploy/helm/ironcache/README.md +++ b/deploy/helm/ironcache/README.md @@ -24,10 +24,23 @@ chart-local summary. - Optionally (`console.enabled=true`) a separate STATELESS console Deployment with its own Service, PDB, and an egress-allowlisting NetworkPolicy. -## Install (from a git checkout) +## Install -The chart is NOT published to any Helm registry; installing from a repo checkout -is the supported path: +### From the GHCR OCI registry (published + cosign-signed) + +Each release is packaged and pushed to GHCR as an OCI artifact and keyless-signed +with cosign (Sigstore/Fulcio via GitHub OIDC): + +```sh +helm install ironcache oci://ghcr.io/elares/charts/ironcache --version 0.3.0 + +# (optional) verify the signature before installing: +cosign verify ghcr.io/elares/charts/ironcache:0.3.0 \ + --certificate-identity-regexp 'https://github.com/ELares/IronCache/.*' \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com +``` + +### From a git checkout ```sh git clone https://github.com/ELares/IronCache @@ -35,9 +48,9 @@ cd IronCache helm install ironcache deploy/helm/ironcache ``` -Upgrades are `helm upgrade ironcache deploy/helm/ironcache` from a newer -checkout. `NOTES.txt` prints the endpoints and loud warnings (auto-generated -cluster secret, auth off, cleartext bus without `clusterTls`) after install. +Upgrades are `helm upgrade ironcache ` with a newer `--version` (OCI) or +checkout. `NOTES.txt` prints the endpoints and loud warnings (auto-generated cluster +secret, auth off, cleartext bus without `clusterTls`) after install. ## Key values From 36518f8ac324d993d1a37e0aa1fd6b1f3db4f27b Mon Sep 17 00:00:00 2001 From: Zeke Date: Fri, 24 Jul 2026 08:12:35 -0700 Subject: [PATCH 2/2] fix(release): refuse to clobber an already-published chart version (review SHOULD-FIX) Adversarial review (verdict SHIP) flagged one real supply-chain footgun: GHCR OCI tags are mutable and the chart version is decoupled from the app tag, so a `v*` release tag shipped WITHOUT a Chart.yaml version bump would silently OVERWRITE (and re-sign) an already-published, possibly-already-verified chart version. Added a pre-push guard: `helm show chart oci://.../ ironcache --version ` -- if the version already exists, fail with a "bump Chart.yaml" message, so a signed chart version is immutable in practice. (Reviewer verified everything else correct: digest parse, by-digest keyless cosign sign, minimal permissions, PR-skip gating, OCI naming + cosign-verify OIDC identity all match.) Verified: the chart-version parse yields 0.3.0 (not apiVersion/appVersion); YAML valid. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01FfFZ8gkkNhDBASuntB72HR --- .github/workflows/chart-release.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/chart-release.yml b/.github/workflows/chart-release.yml index 83a7e4d..97575c5 100644 --- a/.github/workflows/chart-release.yml +++ b/.github/workflows/chart-release.yml @@ -50,8 +50,10 @@ jobs: helm lint deploy/helm/ironcache helm package deploy/helm/ironcache -d "${RUNNER_TEMP}/chart" tgz="$(ls "${RUNNER_TEMP}/chart"/ironcache-*.tgz)" + version="$(helm show chart deploy/helm/ironcache | awk -F': *' '/^version:/{print $2; exit}')" echo "tgz=${tgz}" >> "$GITHUB_OUTPUT" - echo "Packaged: ${tgz}" + echo "version=${version}" >> "$GITHUB_OUTPUT" + echo "Packaged: ${tgz} (chart version ${version})" # Everything below PUBLISHES -- skip it on pull_request (validate the path only). - name: Log in to GHCR if: github.event_name != 'pull_request' @@ -68,6 +70,15 @@ jobs: set -euo pipefail owner_lc="$(echo "$OWNER" | tr '[:upper:]' '[:lower:]')" repo="ghcr.io/${owner_lc}/charts" + version="${{ steps.pkg.outputs.version }}" + # REFUSE TO CLOBBER: GHCR OCI tags are mutable, so re-pushing an existing chart version + # would silently overwrite an already-published (and possibly already-signed) artifact -- + # a supply-chain footgun. Since the chart version is decoupled from the app tag, guard it: + # fail unless Chart.yaml was bumped to a not-yet-published version. + if helm show chart "oci://${repo}/ironcache" --version "${version}" >/dev/null 2>&1; then + echo "ERROR: ${repo}/ironcache:${version} is already published -- bump Chart.yaml version before releasing the chart" + exit 1 + fi # helm push reads the docker config credentials set by docker/login-action above. helm push "${{ steps.pkg.outputs.tgz }}" "oci://${repo}" 2>&1 | tee "${RUNNER_TEMP}/push.log" # helm push prints `Digest: sha256:...`; sign the immutable by-digest ref so the