Skip to content

Commit 163c917

Browse files
committed
chore(helm): harden the chart workflow against zizmor and document cosign v3
Ran the two standard workflow linters over helm.yml. actionlint reported one shellcheck style issue; zizmor reported 22 findings, including two High. Both High findings predate this PR: the version gate expanded github.base_ref straight into a run block. The three Medium findings were checkouts leaving the token in .git/config. Fixed all of them here rather than leaving known findings in a file this PR already rewrites -- the gate only reads history and fetches a public branch, so it never needed the credential. The 16 informational findings were mine: every ${{ }} in a run block is template expansion before the shell sees it, so the documented fix is to pass values through env and reference them as shell variables. Done for the whole publish job, which also removes the shellcheck nit. zizmor now reports zero findings on the file in pedantic mode, with no suppressions, and actionlint is clean. Separately, cosign v3 writes the Sigstore protobuf bundle format by default and cosign v2 cannot read it, so the verification instructions now state the required version. The in-workflow verify uses the same binary it signs with, so it could never have caught this. Re-verified the existence guard's four branches after the env refactor, and confirmed it also fails closed when the registry login has not happened.
1 parent 41b71eb commit 163c917

3 files changed

Lines changed: 59 additions & 28 deletions

File tree

.github/workflows/helm.yml

Lines changed: 53 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ jobs:
3838
timeout-minutes: 15
3939
steps:
4040
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
41+
with:
42+
persist-credentials: false
4143

4244
- name: Set up Helm
4345
uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4
@@ -129,11 +131,16 @@ jobs:
129131
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
130132
with:
131133
fetch-depth: 0
134+
# The version gate only reads history and fetches a public branch, so
135+
# it never needs the token left behind in .git/config.
136+
persist-credentials: false
132137
- name: Require a Chart.yaml version bump when chart content changes
138+
env:
139+
BASE_REF: ${{ github.base_ref }}
133140
run: |
134141
set -euo pipefail
135-
base="origin/${{ github.base_ref }}"
136-
git fetch origin "${{ github.base_ref }}"
142+
base="origin/${BASE_REF}"
143+
git fetch origin "${BASE_REF}"
137144
merge_base=$(git merge-base "$base" HEAD)
138145
changed=$(git diff --name-only "$merge_base" HEAD)
139146
if echo "$changed" | grep -q '^helm/sim/'; then
@@ -155,6 +162,8 @@ jobs:
155162
timeout-minutes: 25
156163
steps:
157164
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
165+
with:
166+
persist-credentials: false
158167

159168
- name: Set up Helm
160169
uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4
@@ -202,11 +211,10 @@ jobs:
202211
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-2vcpu-ubuntu-2404' || 'ubuntu-latest' }}
203212
timeout-minutes: 15
204213
permissions:
205-
contents: read
206-
packages: write
207-
# Sigstore signs against the runner's OIDC identity; no key material is stored.
208-
id-token: write
209-
attestations: write
214+
contents: read # Read the chart source.
215+
packages: write # Push the chart, its signature, and its attestations to GHCR.
216+
id-token: write # Sigstore signs against the runner's OIDC identity; no key material is stored.
217+
attestations: write # Let actions/attest-build-provenance record the SLSA provenance.
210218
steps:
211219
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
212220
with:
@@ -230,13 +238,16 @@ jobs:
230238
id: package
231239
run: |
232240
set -euo pipefail
233-
name=$(helm show chart helm/sim | awk '/^name:/ {print $2}')
234-
version=$(helm show chart helm/sim | awk '/^version:/ {print $2}')
241+
chart=$(helm show chart helm/sim)
242+
name=$(printf '%s\n' "$chart" | awk '/^name:/ {print $2}')
243+
version=$(printf '%s\n' "$chart" | awk '/^version:/ {print $2}')
235244
helm package helm/sim --destination dist
236-
echo "name=${name}" >> "$GITHUB_OUTPUT"
237-
echo "version=${version}" >> "$GITHUB_OUTPUT"
238-
echo "path=dist/${name}-${version}.tgz" >> "$GITHUB_OUTPUT"
239-
echo "repository=ghcr.io/${GITHUB_REPOSITORY_OWNER}/charts/${name}" >> "$GITHUB_OUTPUT"
245+
{
246+
echo "name=${name}"
247+
echo "version=${version}"
248+
echo "path=dist/${name}-${version}.tgz"
249+
echo "repository=ghcr.io/${GITHUB_REPOSITORY_OWNER}/charts/${name}"
250+
} >> "$GITHUB_OUTPUT"
240251
241252
# Chart versions are immutable once published: whoever pinned a version
242253
# must keep resolving the same bytes forever. The PR gate above already
@@ -256,17 +267,20 @@ jobs:
256267
# do not match and stop the job instead.
257268
- name: Skip if this version is already published
258269
id: exists
270+
env:
271+
REPOSITORY: ${{ steps.package.outputs.repository }}
272+
NAME: ${{ steps.package.outputs.name }}
273+
VERSION: ${{ steps.package.outputs.version }}
259274
run: |
260275
set -euo pipefail
261-
if err=$(helm show chart "oci://${{ steps.package.outputs.repository }}" \
262-
--version "${{ steps.package.outputs.version }}" 2>&1 >/dev/null); then
276+
if err=$(helm show chart "oci://${REPOSITORY}" --version "${VERSION}" 2>&1 >/dev/null); then
263277
echo "already=true" >> "$GITHUB_OUTPUT"
264-
echo "::notice::${{ steps.package.outputs.name }} ${{ steps.package.outputs.version }} is already published; skipping."
278+
echo "::notice::${NAME} ${VERSION} is already published; skipping."
265279
elif printf '%s\n' "$err" | grep -q ': not found'; then
266280
echo "already=false" >> "$GITHUB_OUTPUT"
267281
else
268282
printf '%s\n' "$err"
269-
echo "::error::Could not determine whether ${{ steps.package.outputs.name }} ${{ steps.package.outputs.version }} is already published. Refusing to push, because an unchecked push can overwrite a published version."
283+
echo "::error::Could not determine whether ${NAME} ${VERSION} is already published. Refusing to push, because an unchecked push can overwrite a published version."
270284
exit 1
271285
fi
272286
@@ -276,12 +290,13 @@ jobs:
276290
- name: Push chart
277291
id: push
278292
if: steps.exists.outputs.already == 'false'
293+
env:
294+
CHART_PATH: ${{ steps.package.outputs.path }}
279295
run: |
280296
set -euo pipefail
281-
output=$(helm push "${{ steps.package.outputs.path }}" \
282-
"oci://ghcr.io/${GITHUB_REPOSITORY_OWNER}/charts" 2>&1)
283-
echo "$output"
284-
digest=$(echo "$output" | grep -oE 'sha256:[a-f0-9]{64}' | head -1 || true)
297+
output=$(helm push "${CHART_PATH}" "oci://ghcr.io/${GITHUB_REPOSITORY_OWNER}/charts" 2>&1)
298+
printf '%s\n' "$output"
299+
digest=$(printf '%s\n' "$output" | grep -oE 'sha256:[a-f0-9]{64}' | head -1 || true)
285300
if [ -z "$digest" ]; then
286301
echo "::error::helm push did not report a digest; refusing to sign an unidentified artifact"
287302
exit 1
@@ -299,9 +314,12 @@ jobs:
299314
# publishing a signature at all.
300315
- name: Sign and verify chart
301316
if: steps.exists.outputs.already == 'false'
317+
env:
318+
REPOSITORY: ${{ steps.package.outputs.repository }}
319+
DIGEST: ${{ steps.push.outputs.digest }}
302320
run: |
303321
set -euo pipefail
304-
ref="${{ steps.package.outputs.repository }}@${{ steps.push.outputs.digest }}"
322+
ref="${REPOSITORY}@${DIGEST}"
305323
cosign sign --yes "$ref"
306324
cosign verify "$ref" \
307325
--certificate-identity-regexp "^https://github.com/${GITHUB_REPOSITORY}/" \
@@ -324,24 +342,31 @@ jobs:
324342
# tag on the chart's own OCI repository. Pushed on every run, including
325343
# version-skip runs, so an edit to the metadata file alone still lands.
326344
- name: Publish Artifact Hub metadata
345+
env:
346+
REPOSITORY: ${{ steps.package.outputs.repository }}
327347
run: |
328348
set -euo pipefail
329-
oras push "${{ steps.package.outputs.repository }}:artifacthub.io" \
349+
oras push "${REPOSITORY}:artifacthub.io" \
330350
--config /dev/null:application/vnd.cncf.artifacthub.config.v1+yaml \
331351
helm/artifacthub-repo.yml:application/vnd.cncf.artifacthub.repository-metadata.layer.v1.yaml
332352
333353
- name: Summary
354+
env:
355+
ALREADY: ${{ steps.exists.outputs.already }}
356+
REPOSITORY: ${{ steps.package.outputs.repository }}
357+
VERSION: ${{ steps.package.outputs.version }}
358+
DIGEST: ${{ steps.push.outputs.digest }}
334359
run: |
335360
{
336-
if [ "${{ steps.exists.outputs.already }}" = "true" ]; then
337-
echo "### Chart ${{ steps.package.outputs.version }} was already published — nothing to do"
361+
if [ "${ALREADY}" = "true" ]; then
362+
echo "### Chart ${VERSION} was already published — nothing to do"
338363
else
339-
echo "### Published chart ${{ steps.package.outputs.version }}"
364+
echo "### Published chart ${VERSION}"
340365
echo
341-
echo "Digest: \`${{ steps.push.outputs.digest }}\`"
366+
echo "Digest: \`${DIGEST}\`"
342367
fi
343368
echo
344369
echo '```bash'
345-
echo "helm install sim oci://${{ steps.package.outputs.repository }} --version ${{ steps.package.outputs.version }}"
370+
echo "helm install sim oci://${REPOSITORY} --version ${VERSION}"
346371
echo '```'
347372
} >> "$GITHUB_STEP_SUMMARY"

apps/docs/content/docs/platform/self-hosting/kubernetes.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ gh attestation verify oci://ghcr.io/simstudioai/charts/sim:1.9.1 --repo simstudi
7272

7373
Signing is Sigstore-only — there is no GPG `.prov` file, so `helm install --verify` does not apply.
7474

75+
<Callout type="warn">
76+
Verification requires **cosign v3.0 or newer**. Signatures use the Sigstore protobuf bundle format, which cosign v3 writes by default and cosign v2 cannot read. cosign v3.1+ auto-detects both formats.
77+
</Callout>
78+
7579
## Cloud-Specific Values
7680

7781
These are cloud-tuned **alternatives** to the generic install above — pick one path, don't run both. The commands reuse the `$BETTER_AUTH_SECRET`, `$ENCRYPTION_KEY`, `$INTERNAL_API_SECRET`, `$API_ENCRYPTION_KEY`, `$CRON_SECRET`, and `$POSTGRES_PASSWORD` variables generated in [Installation](#installation) above, so run that block's `openssl` lines first in the same shell. They use `helm upgrade --install`, so they work whether or not a release exists yet. Two caveats when converting an existing generic install rather than starting fresh: (1) **reuse the original secret values** — recover them with `helm get values sim -n simstudio` if your shell no longer has them; supplying a newly generated `ENCRYPTION_KEY` makes every previously encrypted value (workspace environment variables, stored provider keys, MCP OAuth credentials) undecryptable. (2) The cloud values rename the bundled PostgreSQL database to `simstudio`, but Postgres only applies that setting on first initialization — add `--set postgresql.auth.database=sim` to keep your existing database. If you'd rather start clean, `helm uninstall sim -n simstudio`, delete its PVCs, and run the cloud command fresh.

helm/sim/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,8 @@ There is no GPG `.prov` file — signing is Sigstore-only, so there is no
189189
long-lived private key to hold or rotate. `helm install --verify` expects the
190190
GPG provenance format and will not work; use `cosign verify` above.
191191

192+
> **Requires cosign v3.0 or newer.** Signatures use the Sigstore protobuf bundle format, which cosign v3 writes by default and cosign v2 cannot read. cosign v3.1+ auto-detects both formats.
193+
192194
## Upgrading
193195

194196
```bash

0 commit comments

Comments
 (0)