diff --git a/.github/workflows/chart-release.yml b/.github/workflows/chart-release.yml new file mode 100644 index 0000000..97575c5 --- /dev/null +++ b/.github/workflows/chart-release.yml @@ -0,0 +1,89 @@ +# 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)" + version="$(helm show chart deploy/helm/ironcache | awk -F': *' '/^version:/{print $2; exit}')" + echo "tgz=${tgz}" >> "$GITHUB_OUTPUT" + 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' + 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" + 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 + # 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