Skip to content

Commit 1d064dc

Browse files
committed
feat(helm): publish the chart to GHCR as a signed OCI artifact
The chart has only ever existed inside the repo, so self-hosters whose security process requires an officially published artifact could not use it at all — a chart pulled from a git checkout is not something they can pin, mirror, or admit. Publishes helm/sim to oci://ghcr.io/simstudioai/charts/sim on push to main, signed with Sigstore keyless signing and carrying a SLSA build provenance attestation, both stored in the registry so they survive a mirror into an internal registry. The publish job lives in helm.yml rather than its own publish-*.yml so it can gate on the existing jobs: nothing ships unless the chart linted, unit-tested, rendered clean under kubeconform, and installed on kind. Publishing is idempotent — a version already in the registry is skipped rather than overwritten, since a published chart version is immutable. Also pushes helm/artifacthub-repo.yml to the reserved artifacthub.io tag for the Artifact Hub listing, and switches the chart README and the Kubernetes docs page off the clone-and-install path.
1 parent b385131 commit 1d064dc

5 files changed

Lines changed: 257 additions & 13 deletions

File tree

.github/workflows/helm.yml

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ on:
55
branches: [main, staging, dev]
66
paths:
77
- 'helm/sim/**'
8+
# Repository-level Artifact Hub metadata, republished by the publish job.
9+
- 'helm/artifacthub-repo.yml'
810
- '.github/workflows/helm.yml'
911
# The image inventory is generated from the chart and checked here, so a
1012
# change to its generator has to run this workflow too.
@@ -14,6 +16,8 @@ on:
1416
branches: [main, staging, dev]
1517
paths:
1618
- 'helm/sim/**'
19+
# Repository-level Artifact Hub metadata, republished by the publish job.
20+
- 'helm/artifacthub-repo.yml'
1721
- '.github/workflows/helm.yml'
1822
# The image inventory is generated from the chart and checked here, so a
1923
# change to its generator has to run this workflow too.
@@ -181,3 +185,148 @@ jobs:
181185
182186
- name: Run helm test
183187
run: helm test sim --namespace sim --timeout 5m
188+
189+
# Publishes the chart to GHCR as an OCI artifact. Self-hosters cannot admit a
190+
# chart pulled from a git checkout — they need an immutable, versioned artifact
191+
# they can pin by digest and mirror into an internal registry — so shipping the
192+
# chart in-repo only is the same as not shipping it.
193+
#
194+
# Lives here rather than in a `publish-*.yml` of its own so it can gate on the
195+
# jobs above: nothing is published unless the chart linted, unit-tested,
196+
# rendered clean under kubeconform, and actually installed on a kind cluster.
197+
# A separate workflow would race those instead of waiting for them.
198+
publish:
199+
name: Publish chart to GHCR
200+
needs: [chart, install]
201+
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && github.repository == 'simstudioai/sim'
202+
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-2vcpu-ubuntu-2404' || 'ubuntu-latest' }}
203+
timeout-minutes: 15
204+
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
210+
steps:
211+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
212+
with:
213+
persist-credentials: false
214+
215+
- name: Set up Helm
216+
uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4
217+
with:
218+
version: v3.16.4
219+
220+
# oras also reads ~/.docker/config.json, so this one login covers both the
221+
# chart push and the Artifact Hub metadata push below.
222+
- name: Login to GHCR
223+
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
224+
with:
225+
registry: ghcr.io
226+
username: ${{ github.repository_owner }}
227+
password: ${{ secrets.GITHUB_TOKEN }}
228+
229+
- name: Package chart
230+
id: package
231+
run: |
232+
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}')
235+
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"
240+
241+
# Chart versions are immutable once published: whoever pinned a version
242+
# must keep resolving the same bytes forever. The PR gate above
243+
# already forces a version bump on every chart change, so a version that
244+
# is already in the registry means this commit changed something outside
245+
# `helm/sim/` — republishing would either fail or silently move a tag.
246+
- name: Skip if this version is already published
247+
id: exists
248+
run: |
249+
set -euo pipefail
250+
if helm show chart "oci://${{ steps.package.outputs.repository }}" \
251+
--version "${{ steps.package.outputs.version }}" > /dev/null 2>&1; then
252+
echo "already=true" >> "$GITHUB_OUTPUT"
253+
echo "::notice::${{ steps.package.outputs.name }} ${{ steps.package.outputs.version }} is already published; skipping."
254+
else
255+
echo "already=false" >> "$GITHUB_OUTPUT"
256+
fi
257+
258+
# `helm push` takes the namespace only — it derives the repository
259+
# basename from the chart's name and the tag from its version, so the
260+
# result is ghcr.io/<owner>/charts/sim:<version>.
261+
- name: Push chart
262+
id: push
263+
if: steps.exists.outputs.already == 'false'
264+
run: |
265+
set -euo pipefail
266+
output=$(helm push "${{ steps.package.outputs.path }}" \
267+
"oci://ghcr.io/${GITHUB_REPOSITORY_OWNER}/charts" 2>&1)
268+
echo "$output"
269+
digest=$(echo "$output" | grep -oE 'sha256:[a-f0-9]{64}' | head -1 || true)
270+
if [ -z "$digest" ]; then
271+
echo "::error::helm push did not report a digest; refusing to sign an unidentified artifact"
272+
exit 1
273+
fi
274+
echo "digest=${digest}" >> "$GITHUB_OUTPUT"
275+
276+
- name: Install Cosign
277+
if: steps.exists.outputs.already == 'false'
278+
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
279+
280+
# Signed by digest, never by tag: a tag is a mutable pointer, so signing
281+
# one would attest to whatever it happens to reference later. The verify
282+
# is not ceremony — it fails the run if the signature we just wrote cannot
283+
# be read back with the identity we expect, which is the whole point of
284+
# publishing a signature at all.
285+
- name: Sign and verify chart
286+
if: steps.exists.outputs.already == 'false'
287+
run: |
288+
set -euo pipefail
289+
ref="${{ steps.package.outputs.repository }}@${{ steps.push.outputs.digest }}"
290+
cosign sign --yes "$ref"
291+
cosign verify "$ref" \
292+
--certificate-identity-regexp "^https://github.com/${GITHUB_REPOSITORY}/" \
293+
--certificate-oidc-issuer https://token.actions.githubusercontent.com
294+
295+
# Stored alongside the chart so a mirrored registry carries the
296+
# attestation with it, rather than only being retrievable from GitHub.
297+
- name: Attest build provenance
298+
if: steps.exists.outputs.already == 'false'
299+
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
300+
with:
301+
subject-name: ${{ steps.package.outputs.repository }}
302+
subject-digest: ${{ steps.push.outputs.digest }}
303+
push-to-registry: true
304+
305+
- name: Set up ORAS
306+
uses: oras-project/setup-oras@1d808f7d7f6995cc68b7bf507bfe5c5446e1dc9d # v2.0.1
307+
308+
# Artifact Hub reads repository metadata from the reserved `artifacthub.io`
309+
# tag on the chart's own OCI repository. Pushed on every run, including
310+
# version-skip runs, so an edit to the metadata file alone still lands.
311+
- name: Publish Artifact Hub metadata
312+
run: |
313+
set -euo pipefail
314+
oras push "${{ steps.package.outputs.repository }}:artifacthub.io" \
315+
--config /dev/null:application/vnd.cncf.artifacthub.config.v1+yaml \
316+
helm/artifacthub-repo.yml:application/vnd.cncf.artifacthub.repository-metadata.layer.v1.yaml
317+
318+
- name: Summary
319+
run: |
320+
{
321+
if [ "${{ steps.exists.outputs.already }}" = "true" ]; then
322+
echo "### Chart ${{ steps.package.outputs.version }} was already published — nothing to do"
323+
else
324+
echo "### Published chart ${{ steps.package.outputs.version }}"
325+
echo
326+
echo "Digest: \`${{ steps.push.outputs.digest }}\`"
327+
fi
328+
echo
329+
echo '```bash'
330+
echo "helm install sim oci://${{ steps.package.outputs.repository }} --version ${{ steps.package.outputs.version }}"
331+
echo '```'
332+
} >> "$GITHUB_STEP_SUMMARY"

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

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ import { FAQ } from '@/components/ui/faq'
2121

2222
## Installation
2323

24-
```bash
25-
# Clone repo
26-
git clone https://github.com/simstudioai/sim.git && cd sim
24+
The chart is published to GitHub Container Registry as an OCI artifact at
25+
`oci://ghcr.io/simstudioai/charts/sim`. Install it directly — no clone, and no
26+
`helm repo add`.
2727

28+
```bash
2829
# Generate secrets
2930
BETTER_AUTH_SECRET=$(openssl rand -hex 32)
3031
ENCRYPTION_KEY=$(openssl rand -hex 32)
@@ -34,7 +35,8 @@ CRON_SECRET=$(openssl rand -hex 32)
3435
POSTGRES_PASSWORD=$(openssl rand -hex 24)
3536

3637
# Install
37-
helm install sim ./helm/sim \
38+
helm install sim oci://ghcr.io/simstudioai/charts/sim \
39+
--version 1.9.1 \
3840
--set app.env.BETTER_AUTH_SECRET="$BETTER_AUTH_SECRET" \
3941
--set app.env.ENCRYPTION_KEY="$ENCRYPTION_KEY" \
4042
--set app.env.INTERNAL_API_SECRET="$INTERNAL_API_SECRET" \
@@ -56,13 +58,28 @@ helm install sim ./helm/sim \
5658
This installs the chart's default image tag. For production, **pin `app`, `realtime`, and `migrations` to the same explicit release tag** — see [Upgrades](/platform/self-hosting/upgrades).
5759
</Callout>
5860

61+
## Verifying the chart
62+
63+
Every published version is signed with Sigstore keyless signing and carries a SLSA build-provenance attestation. Both live in the registry alongside the chart, so they survive a mirror into an internal registry.
64+
65+
```bash
66+
cosign verify oci://ghcr.io/simstudioai/charts/sim:1.9.1 \
67+
--certificate-identity-regexp '^https://github.com/simstudioai/sim/' \
68+
--certificate-oidc-issuer https://token.actions.githubusercontent.com
69+
70+
gh attestation verify oci://ghcr.io/simstudioai/charts/sim:1.9.1 --repo simstudioai/sim
71+
```
72+
73+
Signing is Sigstore-only — there is no GPG `.prov` file, so `helm install --verify` does not apply.
74+
5975
## Cloud-Specific Values
6076

6177
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.
6278

6379
```bash
64-
helm upgrade --install sim ./helm/sim \
65-
--values ./helm/sim/examples/values-aws.yaml \
80+
helm upgrade --install sim oci://ghcr.io/simstudioai/charts/sim \
81+
--version 1.9.1 \
82+
--values https://raw.githubusercontent.com/simstudioai/sim/main/helm/sim/examples/values-aws.yaml \
6683
--set app.env.BETTER_AUTH_SECRET="$BETTER_AUTH_SECRET" \
6784
--set app.env.ENCRYPTION_KEY="$ENCRYPTION_KEY" \
6885
--set app.env.INTERNAL_API_SECRET="$INTERNAL_API_SECRET" \
@@ -80,7 +97,7 @@ helm upgrade --install sim ./helm/sim \
8097

8198
Every one of those overrides is required. The cloud values files hardcode a placeholder domain in all six places, and overriding only `NEXT_PUBLIC_APP_URL` leaves sign-in pointed at the placeholder, realtime rejecting every socket upgrade, and the Ingress serving the wrong host.
8299

83-
Swap the `--values` file for your cloud: `values-aws.yaml` (EKS), `values-azure.yaml` (AKS), or `values-gcp.yaml` (GKE). Everything else is identical.
100+
Swap the `--values` file for your cloud: `values-aws.yaml` (EKS), `values-azure.yaml` (AKS), or `values-gcp.yaml` (GKE). Everything else is identical. Helm reads `--values` over HTTPS, so this works without a checkout; the example files are not part of the packaged chart. Download the file and edit it locally if you'd rather not fetch it at install time.
84101

85102
## Key Configuration
86103

@@ -181,8 +198,8 @@ kubectl port-forward deployment/sim-app 3000:3000 -n simstudio
181198
# View logs
182199
kubectl logs -l app.kubernetes.io/component=app -n simstudio --tail=100
183200
184-
# Upgrade
185-
helm upgrade sim ./helm/sim --namespace simstudio
201+
# Upgrade (always pin the target chart version)
202+
helm upgrade sim oci://ghcr.io/simstudioai/charts/sim --version 1.9.1 --namespace simstudio
186203
187204
# Uninstall
188205
helm uninstall sim --namespace simstudio

helm/artifacthub-repo.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Artifact Hub repository metadata for the Sim chart.
2+
#
3+
# Lives outside `helm/sim/` on purpose: it describes the *repository* rather
4+
# than the chart, so it must not be packaged into the `.tgz`, and a change to it
5+
# must not trip the chart's mandatory Chart.yaml version bump.
6+
#
7+
# The publish job pushes this file to the OCI registry under the reserved
8+
# `artifacthub.io` tag, which is where Artifact Hub looks for it:
9+
# ghcr.io/simstudioai/charts/sim:artifacthub.io
10+
#
11+
# `owners` is what backs an ownership claim — Artifact Hub matches the email of
12+
# the requesting account against this list, and processes claims immediately
13+
# rather than waiting for the next repository scan.
14+
owners:
15+
- name: Sim Team
16+
email: help@sim.ai
17+
18+
# Set this to the repository's Artifact Hub ID once the repository has been
19+
# registered at https://artifacthub.io/control-panel/repositories. It is what
20+
# turns on the "Verified Publisher" badge; until then the listing still works,
21+
# just unverified.
22+
# repositoryID: ""

helm/sim/Chart.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ apiVersion: v2
22
name: sim
33
description: A Helm chart for Sim - the open-source AI workspace where teams build, deploy, and manage AI agents
44
type: application
5-
version: 1.9.0
5+
version: 1.9.1
66
appVersion: "v0.8.18"
77
kubeVersion: ">=1.25.0-0"
88
home: https://sim.ai

helm/sim/README.md

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
Deploy [Sim](https://sim.ai) — the open-source AI workspace where teams build, deploy, and manage AI agents — on Kubernetes.
44

5+
* **Registry:** `oci://ghcr.io/simstudioai/charts/sim`
56
* **Chart version:** see `Chart.yaml`
67
* **App version:** tracks the upstream Sim release
78
* **Kubernetes:** 1.25+
@@ -19,8 +20,8 @@ export INTERNAL_API_SECRET=$(openssl rand -hex 32)
1920
export CRON_SECRET=$(openssl rand -hex 32)
2021
export POSTGRES_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=')
2122

22-
# Install from this repository
23-
helm install sim ./helm/sim \
23+
# Install from the registry
24+
helm install sim oci://ghcr.io/simstudioai/charts/sim \
2425
--namespace sim --create-namespace \
2526
--set app.env.BETTER_AUTH_SECRET="$BETTER_AUTH_SECRET" \
2627
--set app.env.ENCRYPTION_KEY="$ENCRYPTION_KEY" \
@@ -96,7 +97,42 @@ If you set `app.secrets.existingSecret.enabled=true` and point at a pre-created
9697

9798
## Installing the chart
9899

99-
### From this repository
100+
### From the registry
101+
102+
The chart is published to GitHub Container Registry as an OCI artifact. This is
103+
the supported install path — no clone, no `helm repo add`, and every version is
104+
immutable once published.
105+
106+
```bash
107+
# List the published versions
108+
helm show chart oci://ghcr.io/simstudioai/charts/sim --version 1.9.1
109+
110+
helm install sim oci://ghcr.io/simstudioai/charts/sim \
111+
--version 1.9.1 \
112+
--namespace sim --create-namespace \
113+
--set app.env.BETTER_AUTH_SECRET="$BETTER_AUTH_SECRET" \
114+
--set app.env.ENCRYPTION_KEY="$ENCRYPTION_KEY" \
115+
--set app.env.INTERNAL_API_SECRET="$INTERNAL_API_SECRET" \
116+
--set app.env.CRON_SECRET="$CRON_SECRET" \
117+
--set postgresql.auth.password="$POSTGRES_PASSWORD"
118+
```
119+
120+
Always pass `--version`. Without it Helm resolves to the newest published
121+
version at install time, which makes the same command produce different
122+
deployments on different days.
123+
124+
To mirror the chart into an internal registry — the usual requirement for an
125+
air-gapped or internal-only cluster:
126+
127+
```bash
128+
helm pull oci://ghcr.io/simstudioai/charts/sim --version 1.9.1
129+
helm push sim-1.9.1.tgz oci://registry.internal.example.com/charts
130+
```
131+
132+
The container images the chart references are listed in
133+
[`images.yaml`](./images.yaml); mirror those alongside it.
134+
135+
### From a checkout
100136

101137
```bash
102138
helm install sim ./helm/sim \
@@ -132,6 +168,26 @@ helm install sim ./helm/sim --dry-run --debug \
132168

133169
---
134170

171+
## Verifying the chart
172+
173+
Every published version is signed with [Sigstore](https://www.sigstore.dev/)
174+
keyless signing and carries a SLSA build-provenance attestation, both stored in
175+
the registry next to the chart so they survive a mirror.
176+
177+
```bash
178+
# The signature: proves this chart was signed by a GitHub Actions run in this repo
179+
cosign verify oci://ghcr.io/simstudioai/charts/sim:1.9.1 \
180+
--certificate-identity-regexp '^https://github.com/simstudioai/sim/' \
181+
--certificate-oidc-issuer https://token.actions.githubusercontent.com
182+
183+
# The provenance: proves which workflow, commit, and runner produced it
184+
gh attestation verify oci://ghcr.io/simstudioai/charts/sim:1.9.1 --repo simstudioai/sim
185+
```
186+
187+
There is no GPG `.prov` file — signing is Sigstore-only, so there is no
188+
long-lived private key to hold or rotate. `helm install --verify` expects the
189+
GPG provenance format and will not work; use `cosign verify` above.
190+
135191
## Upgrading
136192

137193
```bash

0 commit comments

Comments
 (0)