diff --git a/.github/workflows/helm.yml b/.github/workflows/helm.yml index ac6298ed8d6..6c2e29f79fe 100644 --- a/.github/workflows/helm.yml +++ b/.github/workflows/helm.yml @@ -5,6 +5,8 @@ on: branches: [main, staging, dev] paths: - 'helm/sim/**' + # Repository-level Artifact Hub metadata, republished by the publish job. + - 'helm/artifacthub-repo.yml' - '.github/workflows/helm.yml' # The image inventory is generated from the chart and checked here, so a # change to its generator has to run this workflow too. @@ -14,6 +16,8 @@ on: branches: [main, staging, dev] paths: - 'helm/sim/**' + # Repository-level Artifact Hub metadata, republished by the publish job. + - 'helm/artifacthub-repo.yml' - '.github/workflows/helm.yml' # The image inventory is generated from the chart and checked here, so a # change to its generator has to run this workflow too. @@ -34,6 +38,8 @@ jobs: timeout-minutes: 15 steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false - name: Set up Helm uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4 @@ -125,11 +131,16 @@ jobs: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: fetch-depth: 0 + # The version gate only reads history and fetches a public branch, so + # it never needs the token left behind in .git/config. + persist-credentials: false - name: Require a Chart.yaml version bump when chart content changes + env: + BASE_REF: ${{ github.base_ref }} run: | set -euo pipefail - base="origin/${{ github.base_ref }}" - git fetch origin "${{ github.base_ref }}" + base="origin/${BASE_REF}" + git fetch origin "${BASE_REF}" merge_base=$(git merge-base "$base" HEAD) changed=$(git diff --name-only "$merge_base" HEAD) if echo "$changed" | grep -q '^helm/sim/'; then @@ -151,6 +162,8 @@ jobs: timeout-minutes: 25 steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false - name: Set up Helm uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4 @@ -181,3 +194,291 @@ jobs: - name: Run helm test run: helm test sim --namespace sim --timeout 5m + + # Publishes the chart to GHCR as an OCI artifact. Self-hosters cannot admit a + # chart pulled from a git checkout — they need an immutable, versioned artifact + # they can pin by digest and mirror into an internal registry — so shipping the + # chart in-repo only is the same as not shipping it. + # + # Lives here rather than in a `publish-*.yml` of its own so it can gate on the + # jobs above: nothing is published unless the chart linted, unit-tested, + # rendered clean under kubeconform, and actually installed on a kind cluster. + # A separate workflow would race those instead of waiting for them. + publish: + name: Publish chart to GHCR + needs: [chart, install] + if: github.event_name == 'push' && github.ref == 'refs/heads/main' && github.repository == 'simstudioai/sim' + runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-2vcpu-ubuntu-2404' || 'ubuntu-latest' }} + timeout-minutes: 15 + permissions: + contents: read # Read the chart source. + packages: write # Push the chart, its signature, and its attestations to GHCR. + id-token: write # Sigstore signs against the runner's OIDC identity; no key material is stored. + attestations: write # Let actions/attest-build-provenance record the SLSA provenance. + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + + - name: Set up Helm + uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4 + with: + version: v3.16.4 + + # oras also reads ~/.docker/config.json, so this one login covers both the + # chart push and the Artifact Hub metadata push below. + - name: Login to GHCR + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Package chart + id: package + run: | + set -euo pipefail + chart=$(helm show chart helm/sim) + name=$(printf '%s\n' "$chart" | awk '/^name:/ {print $2}') + version=$(printf '%s\n' "$chart" | awk '/^version:/ {print $2}') + helm package helm/sim --destination dist + { + echo "name=${name}" + echo "version=${version}" + echo "path=dist/${name}-${version}.tgz" + echo "repository=ghcr.io/${GITHUB_REPOSITORY_OWNER}/charts/${name}" + } >> "$GITHUB_OUTPUT" + + # `appVersion` is what the image tags default to, so a stale one publishes + # a chart that silently installs an old Sim -- and because published chart + # versions are immutable, every stale value is frozen forever. It sat six + # releases behind before this check existed, bumped only by hand. + # + # BEHIND is the failure. AHEAD is normal and must not be blocked: a + # version tag is cut by the main-branch merge commit that releases it + # (detect-version in ci.yml), so appVersion legitimately names a release + # that does not exist yet while that release is still being built. Failing + # on any mismatch would race that workflow and block the very publish the + # bump was for. `helm/sim/ci/kind-values.yaml` documents the same + # circularity, and it is why appVersion went unbumped for so long. + # + # Compares against the latest GitHub release rather than a hardcoded value + # so the check cannot go stale itself. Prereleases and drafts are excluded: + # the `/releases/latest` endpoint already returns neither. + - name: appVersion does not lag the app release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + app_version=$(helm show chart helm/sim | awk '/^appVersion:/ {print $2}' | tr -d '"') + latest=$(gh api "repos/${GITHUB_REPOSITORY}/releases/latest" --jq .tag_name) + if [ -z "$latest" ]; then + echo "::error::Could not resolve the latest release; refusing to publish unverified." + exit 1 + fi + if [ "$app_version" = "$latest" ]; then + echo "appVersion ${app_version} matches the latest release." + exit 0 + fi + oldest=$(printf '%s\n%s\n' "$app_version" "$latest" | sort -V | head -1) + if [ "$oldest" = "$app_version" ]; then + echo "::error::Chart.yaml appVersion is ${app_version} but the latest release is ${latest}. Bump appVersion (and the chart version) so the chart does not publish an install pinned to an older Sim." + exit 1 + fi + echo "::notice::appVersion ${app_version} is ahead of the latest release ${latest}, which is expected while that release is still being cut." + + # Chart versions are immutable once published: whoever pinned a version + # must keep resolving the same bytes forever. The PR gate above already + # forces a version bump on every chart change, so a version that is + # already in the registry means this commit changed something outside + # `helm/sim/`. + # + # The lookup must fail CLOSED. Treating every non-zero exit as "absent" + # would let a transient 5xx, an expired token, or a DNS blip re-push an + # existing version and move a tag consumers have already pinned — and + # same-version runs are routine, since the path filter also fires on + # `package.json` and workflow edits. + # + # Verified against the pinned Helm (v3.16.4): an absent version AND an + # absent repository both report `: not found`, so a first publish + # still proceeds, while `denied`, `unauthorized`, and `dial tcp` failures + # do not match and stop the job instead. + - name: Skip if this version is already published + id: exists + env: + REPOSITORY: ${{ steps.package.outputs.repository }} + NAME: ${{ steps.package.outputs.name }} + VERSION: ${{ steps.package.outputs.version }} + run: | + set -euo pipefail + if err=$(helm show chart "oci://${REPOSITORY}" --version "${VERSION}" 2>&1 >/dev/null); then + echo "already=true" >> "$GITHUB_OUTPUT" + echo "::notice::${NAME} ${VERSION} is already published; skipping." + elif printf '%s\n' "$err" | grep -q ': not found'; then + echo "already=false" >> "$GITHUB_OUTPUT" + else + printf '%s\n' "$err" + echo "::error::Could not determine whether ${NAME} ${VERSION} is already published. Refusing to push, because an unchecked push can overwrite a published version." + exit 1 + fi + + # `helm push` takes the namespace only — it derives the repository + # basename from the chart's name and the tag from its version, so the + # result is ghcr.io//charts/sim:. + - name: Push chart + id: push + if: steps.exists.outputs.already == 'false' + env: + CHART_PATH: ${{ steps.package.outputs.path }} + run: | + set -euo pipefail + output=$(helm push "${CHART_PATH}" "oci://ghcr.io/${GITHUB_REPOSITORY_OWNER}/charts" 2>&1) + printf '%s\n' "$output" + digest=$(printf '%s\n' "$output" | grep -oE 'sha256:[a-f0-9]{64}' | head -1 || true) + if [ -z "$digest" ]; then + echo "::error::helm push did not report a digest; refusing to sign an unidentified artifact" + exit 1 + fi + echo "digest=${digest}" >> "$GITHUB_OUTPUT" + + - name: Install Cosign + if: steps.exists.outputs.already == 'false' + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 + + # Signed by digest, never by tag: a tag is a mutable pointer, so signing + # one would attest to whatever it happens to reference later. The verify + # is not ceremony — it fails the run if the signature we just wrote cannot + # be read back with the identity we expect, which is the whole point of + # publishing a signature at all. + - name: Sign and verify chart + if: steps.exists.outputs.already == 'false' + env: + REPOSITORY: ${{ steps.package.outputs.repository }} + DIGEST: ${{ steps.push.outputs.digest }} + run: | + set -euo pipefail + ref="${REPOSITORY}@${DIGEST}" + cosign sign --yes "$ref" + cosign verify "$ref" \ + --certificate-identity-regexp "^https://github.com/${GITHUB_REPOSITORY}/" \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com + + # Stored alongside the chart so a mirrored registry carries the + # attestation with it, rather than only being retrievable from GitHub. + - name: Attest build provenance + if: steps.exists.outputs.already == 'false' + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-name: ${{ steps.package.outputs.repository }} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true + + - name: Set up ORAS + uses: oras-project/setup-oras@1d808f7d7f6995cc68b7bf507bfe5c5446e1dc9d # v2.0.1 + + # Artifact Hub reads repository metadata from the reserved `artifacthub.io` + # tag on the chart's own OCI repository. Pushed on every run, including + # version-skip runs, so an edit to the metadata file alone still lands. + - name: Publish Artifact Hub metadata + env: + REPOSITORY: ${{ steps.package.outputs.repository }} + # Run from `helm/` so the layer's title annotation is the bare + # `artifacthub-repo.yml`, matching Artifact Hub's documented command. A + # path-qualified argument records `helm/artifacthub-repo.yml` instead. + working-directory: helm + run: | + set -euo pipefail + oras push "${REPOSITORY}:artifacthub.io" \ + --config /dev/null:application/vnd.cncf.artifacthub.config.v1+yaml \ + artifacthub-repo.yml:application/vnd.cncf.artifacthub.repository-metadata.layer.v1.yaml + + - name: Summary + env: + ALREADY: ${{ steps.exists.outputs.already }} + REPOSITORY: ${{ steps.package.outputs.repository }} + VERSION: ${{ steps.package.outputs.version }} + DIGEST: ${{ steps.push.outputs.digest }} + run: | + { + if [ "${ALREADY}" = "true" ]; then + echo "### Chart ${VERSION} was already published — nothing to do" + else + echo "### Published chart ${VERSION}" + echo + echo "Digest: \`${DIGEST}\`" + fi + echo + echo '```bash' + echo "helm install sim oci://${REPOSITORY} --version ${VERSION}" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + + # The classic HTTP repo, published alongside the OCI artifact above. Both is + # what the ecosystem actually does: Bitnami, cert-manager, ingress-nginx, + # prometheus-community, Grafana, Argo and external-secrets all still serve an + # index.yaml, because plenty of clusters, GitOps configs and mirroring tools + # only speak `helm repo add`. OCI is the modern path, not yet the only one. + # + # Separate from the OCI job on purpose: chart-releaser needs `contents: write` + # to cut a release and push the index, and there is no reason to hand that to + # the job holding the signing identity. + publish-http: + name: Publish chart to the Helm repo + needs: [chart, install] + if: github.event_name == 'push' && github.ref == 'refs/heads/main' && github.repository == 'simstudioai/sim' + runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-2vcpu-ubuntu-2404' || 'ubuntu-latest' }} + timeout-minutes: 15 + permissions: + contents: write # Cut the chart release and push index.yaml to the pages branch. + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + # chart-releaser diffs against the previous tag to decide which charts + # changed, so it needs the full history rather than a shallow clone. + fetch-depth: 0 + # chart-releaser authenticates with CR_TOKEN, not the checkout credential. + persist-credentials: false + + # Creating the pages branch and turning on GitHub Pages are one-time + # manual steps that no workflow can do for itself. Skip loudly rather than + # failing main when they have not happened yet -- the OCI publish is + # independent and must not be held hostage to this. + - name: Check the pages branch exists + id: pages + run: | + set -euo pipefail + if git ls-remote --exit-code --heads origin gh-pages >/dev/null 2>&1; then + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "exists=false" >> "$GITHUB_OUTPUT" + echo "::warning::No gh-pages branch, so the HTTP chart repo was not updated. Create it and point GitHub Pages at it to activate this job. The OCI publish is unaffected." + fi + + - name: Configure Git + if: steps.pages.outputs.exists == 'true' + env: + ACTOR: ${{ github.actor }} + run: | + set -euo pipefail + git config user.name "${ACTOR}" + git config user.email "${ACTOR}@users.noreply.github.com" + + # chart-releaser writes index.yaml to the pages branch and attaches the + # .tgz to a GitHub release, which is where index.yaml points -- so the + # packages stay reachable no matter which domain serves the index. + - name: Run chart-releaser + if: steps.pages.outputs.exists == 'true' + uses: helm/chart-releaser-action@cae68fefc6b5f367a0275617c9f83181ba54714f # v1.7.0 + with: + charts_dir: helm + # Re-running on an already-released version must be a no-op, the same + # way the OCI publish above refuses to move a published version. + skip_existing: true + # A chart release must never take the "Latest" badge from the + # application release it packages. + mark_as_latest: false + env: + CR_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Keeps chart releases visually distinct from the vX.Y.Z app releases + # they share the list with. + CR_RELEASE_NAME_TEMPLATE: "helm-chart-{{ .Version }}" diff --git a/apps/docs/content/docs/platform/self-hosting/environment-variables.mdx b/apps/docs/content/docs/platform/self-hosting/environment-variables.mdx index 8dbcf1ba31a..b41d95e48b1 100644 --- a/apps/docs/content/docs/platform/self-hosting/environment-variables.mdx +++ b/apps/docs/content/docs/platform/self-hosting/environment-variables.mdx @@ -311,7 +311,7 @@ Setting the variable to an empty string does **not** remove it: the chart reads Null the variable in every layer that sets it. If it appears in both `app.env` and `app.envDefaults`, nulling only the `app.env` entry lets the `envDefaults` value apply again and the limit stays in force. With External Secrets, also drop the key from `externalSecrets.remoteRefs.app`, which keeps syncing it independently. Confirm what the pod will actually receive before rolling out: ```bash -helm template sim ./helm/sim -f values.yaml | grep -A1 FREE_TABLE # expect no output +helm template sim oci://ghcr.io/simstudioai/charts/sim --version 1.9.5 -f values.yaml | grep -A1 FREE_TABLE # expect no output ``` `null` deletion has no effect under `helm upgrade --reuse-values` — pass your full values with `-f`, or use `--reset-then-reuse-values` (Helm 3.14+). If you deploy with Argo CD, put the `null` in `valueFiles` or the `values` string rather than `valuesObject`, which strips nulls. On Docker Compose, delete the line from your `.env` file. diff --git a/apps/docs/content/docs/platform/self-hosting/kubernetes.mdx b/apps/docs/content/docs/platform/self-hosting/kubernetes.mdx index 29772c1cacc..303a0795366 100644 --- a/apps/docs/content/docs/platform/self-hosting/kubernetes.mdx +++ b/apps/docs/content/docs/platform/self-hosting/kubernetes.mdx @@ -21,10 +21,15 @@ import { FAQ } from '@/components/ui/faq' ## Installation -```bash -# Clone repo -git clone https://github.com/simstudioai/sim.git && cd sim +The chart is published to GitHub Container Registry as an OCI artifact at +`oci://ghcr.io/simstudioai/charts/sim`. Install it directly — no clone required. + +It is also available from a classic Helm repository at `https://charts.sim.ai` +for tooling that expects one; see [Helm repository](#helm-repository) below. +Prefer OCI where you can, since the signature and provenance are attached to the +OCI artifact. +```bash # Generate secrets BETTER_AUTH_SECRET=$(openssl rand -hex 32) ENCRYPTION_KEY=$(openssl rand -hex 32) @@ -34,7 +39,8 @@ CRON_SECRET=$(openssl rand -hex 32) POSTGRES_PASSWORD=$(openssl rand -hex 24) # Install -helm install sim ./helm/sim \ +helm install sim oci://ghcr.io/simstudioai/charts/sim \ + --version 1.9.5 \ --set app.env.BETTER_AUTH_SECRET="$BETTER_AUTH_SECRET" \ --set app.env.ENCRYPTION_KEY="$ENCRYPTION_KEY" \ --set app.env.INTERNAL_API_SECRET="$INTERNAL_API_SECRET" \ @@ -56,13 +62,58 @@ helm install sim ./helm/sim \ 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). +## Helm repository + +For clusters or GitOps configs that consume `helm repo add` rather than OCI: + +```bash +helm repo add sim https://charts.sim.ai +helm repo update + +helm install sim sim/sim --version 1.9.5 --namespace simstudio --create-namespace \ + --set app.env.BETTER_AUTH_SECRET="$BETTER_AUTH_SECRET" \ + --set app.env.ENCRYPTION_KEY="$ENCRYPTION_KEY" \ + --set app.env.INTERNAL_API_SECRET="$INTERNAL_API_SECRET" \ + --set app.env.API_ENCRYPTION_KEY="$API_ENCRYPTION_KEY" \ + --set app.env.CRON_SECRET="$CRON_SECRET" \ + --set postgresql.auth.password="$POSTGRES_PASSWORD" +``` + +It serves the same chart as the OCI registry. The signature and provenance below +apply to the OCI artifact only. + +## Verifying the chart + +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. + +```bash +cosign verify oci://ghcr.io/simstudioai/charts/sim:1.9.5 \ + --certificate-identity-regexp '^https://github.com/simstudioai/sim/' \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com + +gh attestation verify oci://ghcr.io/simstudioai/charts/sim:1.9.5 --repo simstudioai/sim +``` + +Signing is Sigstore-only — there is no GPG `.prov` file, so `helm install --verify` does not apply. + + + 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. + + ## Cloud-Specific Values 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. ```bash -helm upgrade --install sim ./helm/sim \ - --values ./helm/sim/examples/values-aws.yaml \ +# The example values files are not part of the packaged chart, so fetch the one +# you want at a release tag — pinning the chart but reading values off a moving +# branch would still make this command produce different deployments over time. +SIM_RELEASE=v0.8.24 +curl -fsSLO "https://raw.githubusercontent.com/simstudioai/sim/$SIM_RELEASE/helm/sim/examples/values-aws.yaml" + +helm upgrade --install sim oci://ghcr.io/simstudioai/charts/sim \ + --version 1.9.5 \ + --values values-aws.yaml \ --set app.env.BETTER_AUTH_SECRET="$BETTER_AUTH_SECRET" \ --set app.env.ENCRYPTION_KEY="$ENCRYPTION_KEY" \ --set app.env.INTERNAL_API_SECRET="$INTERNAL_API_SECRET" \ @@ -80,7 +131,7 @@ helm upgrade --install sim ./helm/sim \ 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. -Swap the `--values` file for your cloud: `values-aws.yaml` (EKS), `values-azure.yaml` (AKS), or `values-gcp.yaml` (GKE). Everything else is identical. +Swap the `--values` file for your cloud: `values-aws.yaml` (EKS), `values-azure.yaml` (AKS), or `values-gcp.yaml` (GKE). Everything else is identical. Keep the downloaded file in your own config repo — the `--set` overrides above cover the six placeholder domains, but anything else you tune belongs in the file. ## Key Configuration @@ -181,8 +232,8 @@ kubectl port-forward deployment/sim-app 3000:3000 -n simstudio # View logs kubectl logs -l app.kubernetes.io/component=app -n simstudio --tail=100 -# Upgrade -helm upgrade sim ./helm/sim --namespace simstudio +# Upgrade (always pin the target chart version) +helm upgrade sim oci://ghcr.io/simstudioai/charts/sim --version 1.9.5 --namespace simstudio # Uninstall helm uninstall sim --namespace simstudio diff --git a/apps/docs/content/docs/platform/self-hosting/reference-architectures.mdx b/apps/docs/content/docs/platform/self-hosting/reference-architectures.mdx index 0f3242e5995..8df1ba75150 100644 --- a/apps/docs/content/docs/platform/self-hosting/reference-architectures.mdx +++ b/apps/docs/content/docs/platform/self-hosting/reference-architectures.mdx @@ -139,7 +139,7 @@ See [Security](/platform/self-hosting/security) for the full secret inventory, w ## Calling the chart from Terraform -If you already run Terraform, the chart is the resource to wrap — not something to reimplement. It is not published to a Helm repository or an OCI registry, so there is no `repository` to point at: vendor this repo as a submodule, a release tarball, or a `git clone` in your pipeline, and give `chart` the local path. +If you already run Terraform, the chart is the resource to wrap — not something to reimplement. Point `repository` at the OCI registry and pin `version`; there is no need to vendor the repo. ```hcl resource "helm_release" "sim" { @@ -147,8 +147,15 @@ resource "helm_release" "sim" { namespace = "sim" create_namespace = true - # Local path, not a repository. Pin the git ref you vendor from. - chart = "${path.module}/sim/helm/sim" + # Always pin `version`. Without it Terraform resolves the newest published + # chart at apply time, which is how an unplanned apply moves Sim to a new + # release with new migrations. + repository = "oci://ghcr.io/simstudioai/charts" + chart = "sim" + version = "1.9.5" + + # Or the classic repository, if your tooling does not speak OCI: + # repository = "https://charts.sim.ai" # Your own values file. The examples under helm/sim/examples/ carry # placeholder secrets and are starting points, not deployable as-is. @@ -172,6 +179,6 @@ resource "helm_release" "sim" { The example values files ship literal placeholders such as `your-secure-production-auth-secret-here`. That includes `postgresql.auth.password`. The chart only rejects empty values and its own `CHANGE-ME` strings, so a deployment that inherits those placeholders installs cleanly with a publicly known session-signing secret and database password. Override every secret, or use External Secrets and set none of them inline. -Because the chart is local, `version` does nothing — what pins it is the git ref you vendor from, and `helm/sim/Chart.yaml` tells you which chart release that ref carries. Pin that ref, and pin the image tags separately, or an unplanned `terraform apply` can move Sim to a new release with new migrations. See [Upgrades](/platform/self-hosting/upgrades). +Pin `version` above, and pin the image tags separately — the chart version and the application version move independently, so pinning one does not pin the other. See [Upgrades](/platform/self-hosting/upgrades). Once the infrastructure exists, follow [Kubernetes](/platform/self-hosting/kubernetes) for the install itself, then the [pre-launch checklist](/platform/self-hosting/security) and the [verification checklist](/platform/self-hosting/verify). diff --git a/apps/docs/content/docs/platform/self-hosting/upgrades.mdx b/apps/docs/content/docs/platform/self-hosting/upgrades.mdx index 7fdf3f9bfe7..b5fde599612 100644 --- a/apps/docs/content/docs/platform/self-hosting/upgrades.mdx +++ b/apps/docs/content/docs/platform/self-hosting/upgrades.mdx @@ -156,7 +156,8 @@ Migration surprises are usually data-shaped rather than schema-shaped, so a stag ```bash -helm upgrade sim ./helm/sim \ +helm upgrade sim oci://ghcr.io/simstudioai/charts/sim \ + --version 1.9.5 \ --namespace simstudio \ --values my-values.yaml ``` @@ -164,7 +165,7 @@ helm upgrade sim ./helm/sim \ Preview first if the chart version changed: ```bash -helm diff upgrade sim ./helm/sim -n simstudio --values my-values.yaml +helm diff upgrade sim oci://ghcr.io/simstudioai/charts/sim --version 1.9.5 -n simstudio --values my-values.yaml ``` Then watch the rollout: diff --git a/helm/artifacthub-repo.yml b/helm/artifacthub-repo.yml new file mode 100644 index 00000000000..47a578b80a0 --- /dev/null +++ b/helm/artifacthub-repo.yml @@ -0,0 +1,22 @@ +# Artifact Hub repository metadata for the Sim chart. +# +# Lives outside `helm/sim/` on purpose: it describes the *repository* rather +# than the chart, so it must not be packaged into the `.tgz`, and a change to it +# must not trip the chart's mandatory Chart.yaml version bump. +# +# The publish job pushes this file to the OCI registry under the reserved +# `artifacthub.io` tag, which is where Artifact Hub looks for it: +# ghcr.io/simstudioai/charts/sim:artifacthub.io +# +# `owners` is what backs an ownership claim — Artifact Hub matches the email of +# the requesting account against this list, and processes claims immediately +# rather than waiting for the next repository scan. +owners: + - name: Sim Team + email: help@sim.ai + +# Set this to the repository's Artifact Hub ID once the repository has been +# registered at https://artifacthub.io/control-panel/repositories. It is what +# turns on the "Verified Publisher" badge; until then the listing still works, +# just unverified. +# repositoryID: "" diff --git a/helm/sim/Chart.yaml b/helm/sim/Chart.yaml index b3d1464925c..83ddb43abe3 100644 --- a/helm/sim/Chart.yaml +++ b/helm/sim/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: sim description: A Helm chart for Sim - the open-source AI workspace where teams build, deploy, and manage AI agents type: application -version: 1.9.0 -appVersion: "v0.8.18" +version: 1.9.5 +appVersion: "v0.8.24" kubeVersion: ">=1.25.0-0" home: https://sim.ai icon: https://raw.githubusercontent.com/simstudioai/sim/main/apps/sim/public/logo/primary/primary.svg @@ -21,6 +21,9 @@ keywords: - nextjs annotations: category: developer-tools + # Artifact Hub only accepts one value from its fixed list; the bare `category` + # above is a different convention and does not categorise the listing. + artifacthub.io/category: ai-machine-learning artifacthub.io/license: Apache-2.0 artifacthub.io/links: | - name: Homepage diff --git a/helm/sim/README.md b/helm/sim/README.md index 54ded4b28af..5c24b0ee3c5 100644 --- a/helm/sim/README.md +++ b/helm/sim/README.md @@ -2,6 +2,8 @@ Deploy [Sim](https://sim.ai) — the open-source AI workspace where teams build, deploy, and manage AI agents — on Kubernetes. +* **Registry:** `oci://ghcr.io/simstudioai/charts/sim` +* **Helm repository:** `https://charts.sim.ai` * **Chart version:** see `Chart.yaml` * **App version:** tracks the upstream Sim release * **Kubernetes:** 1.25+ @@ -19,8 +21,9 @@ export INTERNAL_API_SECRET=$(openssl rand -hex 32) export CRON_SECRET=$(openssl rand -hex 32) export POSTGRES_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=') -# Install from this repository -helm install sim ./helm/sim \ +# Install from the registry +helm install sim oci://ghcr.io/simstudioai/charts/sim \ + --version 1.9.5 \ --namespace sim --create-namespace \ --set app.env.BETTER_AUTH_SECRET="$BETTER_AUTH_SECRET" \ --set app.env.ENCRYPTION_KEY="$ENCRYPTION_KEY" \ @@ -96,7 +99,65 @@ If you set `app.secrets.existingSecret.enabled=true` and point at a pre-created ## Installing the chart -### From this repository +### From the registry + +The chart is published to GitHub Container Registry as an OCI artifact. This is +the supported install path — no clone, no `helm repo add`, and every version is +immutable once published. + +```bash +# List the published versions +helm show chart oci://ghcr.io/simstudioai/charts/sim --version 1.9.5 + +helm install sim oci://ghcr.io/simstudioai/charts/sim \ + --version 1.9.5 \ + --namespace sim --create-namespace \ + --set app.env.BETTER_AUTH_SECRET="$BETTER_AUTH_SECRET" \ + --set app.env.ENCRYPTION_KEY="$ENCRYPTION_KEY" \ + --set app.env.INTERNAL_API_SECRET="$INTERNAL_API_SECRET" \ + --set app.env.CRON_SECRET="$CRON_SECRET" \ + --set postgresql.auth.password="$POSTGRES_PASSWORD" +``` + +Always pass `--version`. Without it Helm resolves to the newest published +version at install time, which makes the same command produce different +deployments on different days. + +To mirror the chart into an internal registry — the usual requirement for an +air-gapped or internal-only cluster: + +```bash +helm pull oci://ghcr.io/simstudioai/charts/sim --version 1.9.5 +helm push sim-1.9.5.tgz oci://registry.internal.example.com/charts +``` + +The container images the chart references are listed in +[`images.yaml`](./images.yaml); mirror those alongside it. + +### From the Helm repository + +The chart is also published to a classic Helm repository, for clusters and +tooling that consume `helm repo add` rather than OCI. + +```bash +helm repo add sim https://charts.sim.ai +helm repo update + +helm install sim sim/sim \ + --version 1.9.5 \ + --namespace sim --create-namespace \ + --set app.env.BETTER_AUTH_SECRET="$BETTER_AUTH_SECRET" \ + --set app.env.ENCRYPTION_KEY="$ENCRYPTION_KEY" \ + --set app.env.INTERNAL_API_SECRET="$INTERNAL_API_SECRET" \ + --set app.env.CRON_SECRET="$CRON_SECRET" \ + --set postgresql.auth.password="$POSTGRES_PASSWORD" +``` + +Both paths serve the same chart. Prefer OCI where you can: the signature and +provenance described in [Verifying the chart](#verifying-the-chart) are attached +to the OCI artifact, and `helm repo add` has no equivalent. + +### From a checkout ```bash helm install sim ./helm/sim \ @@ -108,20 +169,26 @@ helm install sim ./helm/sim \ --set postgresql.auth.password="$POSTGRES_PASSWORD" ``` +The remaining examples in this README use the OCI reference. If you are working +from a checkout, substitute `./helm/sim` and drop `--version` — the commands are +otherwise identical. + ### With a values file ```bash -helm install sim ./helm/sim \ +helm install sim oci://ghcr.io/simstudioai/charts/sim \ + --version 1.9.5 \ --namespace sim --create-namespace \ --values my-values.yaml ``` -Run `helm template ./helm/sim --values my-values.yaml | less` first to see what will be applied. +Run `helm template oci://ghcr.io/simstudioai/charts/sim --version 1.9.5 --values my-values.yaml | less` first to +see what will be applied. ### Validate the install ```bash -helm install sim ./helm/sim --dry-run --debug \ +helm install sim oci://ghcr.io/simstudioai/charts/sim --version 1.9.5 --dry-run --debug \ --values my-values.yaml \ --set app.env.BETTER_AUTH_SECRET=$(openssl rand -hex 16) \ --set app.env.ENCRYPTION_KEY=$(openssl rand -hex 16) \ @@ -132,10 +199,32 @@ helm install sim ./helm/sim --dry-run --debug \ --- +## Verifying the chart + +Every published version is signed with [Sigstore](https://www.sigstore.dev/) +keyless signing and carries a SLSA build-provenance attestation, both stored in +the registry next to the chart so they survive a mirror. + +```bash +# The signature: proves this chart was signed by a GitHub Actions run in this repo +cosign verify oci://ghcr.io/simstudioai/charts/sim:1.9.5 \ + --certificate-identity-regexp '^https://github.com/simstudioai/sim/' \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com + +# The provenance: proves which workflow, commit, and runner produced it +gh attestation verify oci://ghcr.io/simstudioai/charts/sim:1.9.5 --repo simstudioai/sim +``` + +There is no GPG `.prov` file — signing is Sigstore-only, so there is no +long-lived private key to hold or rotate. `helm install --verify` expects the +GPG provenance format and will not work; use `cosign verify` above. + +> **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. + ## Upgrading ```bash -helm upgrade sim ./helm/sim --namespace sim --values my-values.yaml +helm upgrade sim oci://ghcr.io/simstudioai/charts/sim --version 1.9.5 --namespace sim --values my-values.yaml ``` --- @@ -165,7 +254,7 @@ kubectl delete namespace sim ## Examples -Pre-built values files for common scenarios live in `helm/sim/examples/`. Each file has a header explaining when to use it and any prerequisites. +Pre-built values files for common scenarios live in [`helm/sim/examples/`](https://github.com/simstudioai/sim/tree/main/helm/sim/examples). Each file has a header explaining when to use it and any prerequisites. They are **not** part of the packaged chart, so fetch the one you want at a release tag rather than expecting it locally. | File | When to use | |---|---| @@ -183,9 +272,13 @@ Pre-built values files for common scenarios live in `helm/sim/examples/`. Each f Use one with: ```bash -helm install sim ./helm/sim \ +SIM_RELEASE=v0.8.24 +curl -fsSLO "https://raw.githubusercontent.com/simstudioai/sim/$SIM_RELEASE/helm/sim/examples/values-production.yaml" + +helm install sim oci://ghcr.io/simstudioai/charts/sim \ + --version 1.9.5 \ --namespace sim --create-namespace \ - --values ./helm/sim/examples/values-production.yaml \ + --values values-production.yaml \ --set app.env.BETTER_AUTH_SECRET="$BETTER_AUTH_SECRET" \ --set app.env.ENCRYPTION_KEY="$ENCRYPTION_KEY" \ --set app.env.INTERNAL_API_SECRET="$INTERNAL_API_SECRET" \ @@ -200,12 +293,14 @@ This chart is intentionally configurable. Rather than maintain a hand-curated pa ```bash # Print all values with comments and defaults -helm show values ./helm/sim - -# Print the JSON Schema (used by `helm install` to validate your values) -cat ./helm/sim/values.schema.json +helm show values oci://ghcr.io/simstudioai/charts/sim --version 1.9.5 ``` +The JSON Schema that `helm install` validates your values against ships inside +the chart as `values.schema.json`; read it in the +[repository](https://github.com/simstudioai/sim/blob/main/helm/sim/values.schema.json) +or extract it from a pulled chart with `helm pull --untar`. + `values.yaml` is heavily commented; each top-level section explains what it controls and which sub-keys are required vs optional. For per-cloud examples and idiomatic overrides, see `examples/`. --- @@ -246,7 +341,7 @@ Before installing in production, confirm each of the following: kubernetes.io/metadata.name: ingress-nginx ``` * **Namespace hardening** — label the install namespace with Pod Security Standards `restricted` enforcement (`pod-security.kubernetes.io/enforce=restricted`). All workloads set `runAsNonRoot`, drop all Linux capabilities, disable privilege escalation, and set `seccompProfile: RuntimeDefault` — the four controls the Restricted profile requires. `readOnlyRootFilesystem` is intentionally **not** defaulted anywhere (Postgres/Ollama genuinely need a writable root; the stateless services — `realtime`, `pii`, `copilot` — could tolerate it but aren't pre-wired with a `/tmp` `emptyDir`). If your policy requires it, set `.securityContext.readOnlyRootFilesystem: true` and mount an `emptyDir` at `/tmp` yourself via `extraVolumes`/`extraVolumeMounts`. -* **Env validation** — keys under `app.env`, `realtime.env`, and `copilot.env` are passed through to the application and validated at startup. The JSON Schema intentionally does not enforce `additionalProperties: false` (would break custom user envs), so typos like `OPENA_API_KEY` (instead of `OPENAI_API_KEY`) surface as missing-key errors at runtime, not at `helm install` time. Review your env block carefully. +* **Env validation** — keys under `app.env`, `realtime.env`, and `copilot.server.env` are passed through to the application and validated at startup. The JSON Schema intentionally does not enforce `additionalProperties: false` (would break custom user envs), so typos like `OPENA_API_KEY` (instead of `OPENAI_API_KEY`) surface as missing-key errors at runtime, not at `helm install` time. Review your env block carefully. * **Set public URLs** — `app.env.NEXT_PUBLIC_APP_URL` and `app.env.BETTER_AUTH_URL` must match your public origin (e.g. `https://sim.example.com`). Leaving them as `localhost` breaks sign-in. --- @@ -258,7 +353,7 @@ The chart supports three ways to provide secrets, in increasing order of product ### 1. Inline `--set` (dev / dry-run only) ```bash -helm install sim ./helm/sim --set app.env.BETTER_AUTH_SECRET=... +helm install sim oci://ghcr.io/simstudioai/charts/sim --version 1.9.5 --set app.env.BETTER_AUTH_SECRET=... ``` Discouraged for production — values land in `helm get values` output. @@ -389,7 +484,7 @@ With the chart-managed Secret (the default), nulling a key the application canno The common case is a free-tier cap inherited from a chart release older than the one that stopped presetting them, which shipped `FREE_TABLES_LIMIT: "3"` and `FREE_TABLE_ROWS_LIMIT: "1000"` under `app.envDefaults`. With billing disabled, Sim reads an unset limit as unlimited, so nulling these lifts the cap. Verify before rolling out: ```bash -helm template sim ./helm/sim -f values.yaml | grep -A1 FREE_TABLE # expect no output +helm template sim oci://ghcr.io/simstudioai/charts/sim --version 1.9.5 -f values.yaml | grep -A1 FREE_TABLE # expect no output ``` --- @@ -457,7 +552,7 @@ Without a cluster-reachable `INTERNAL_API_BASE_URL` (it falls back to `NEXT_PUBL You ran `helm install` without setting required secrets. Generate them and pass with `--set`: ```bash -helm install sim ./helm/sim \ +helm install sim oci://ghcr.io/simstudioai/charts/sim --version 1.9.5 \ --set app.env.BETTER_AUTH_SECRET=$(openssl rand -hex 32) \ --set app.env.ENCRYPTION_KEY=$(openssl rand -hex 32) \ --set app.env.INTERNAL_API_SECRET=$(openssl rand -hex 32) \ diff --git a/helm/sim/images.yaml b/helm/sim/images.yaml index 98c53e4074d..1e0efa1396c 100644 --- a/helm/sim/images.yaml +++ b/helm/sim/images.yaml @@ -22,22 +22,22 @@ # render it twice. That override also CHANGES where the chart pulls from, to # `/nvidia/k8s-device-plugin` — mirror the device plugin there # instead of to the `mirror` path listed below, or the pull fails. -appVersion: v0.8.18 +appVersion: v0.8.24 images: - source: busybox:1.36 mirror: busybox:1.36 - source: curlimages/curl:8.5.0 mirror: curlimages/curl:8.5.0 - - source: ghcr.io/simstudioai/copilot:v0.8.18 - mirror: simstudioai/copilot:v0.8.18 - - source: ghcr.io/simstudioai/migrations:v0.8.18 - mirror: simstudioai/migrations:v0.8.18 - - source: ghcr.io/simstudioai/pii:v0.8.18 - mirror: simstudioai/pii:v0.8.18 - - source: ghcr.io/simstudioai/realtime:v0.8.18 - mirror: simstudioai/realtime:v0.8.18 - - source: ghcr.io/simstudioai/simstudio:v0.8.18 - mirror: simstudioai/simstudio:v0.8.18 + - source: ghcr.io/simstudioai/copilot:v0.8.24 + mirror: simstudioai/copilot:v0.8.24 + - source: ghcr.io/simstudioai/migrations:v0.8.24 + mirror: simstudioai/migrations:v0.8.24 + - source: ghcr.io/simstudioai/pii:v0.8.24 + mirror: simstudioai/pii:v0.8.24 + - source: ghcr.io/simstudioai/realtime:v0.8.24 + mirror: simstudioai/realtime:v0.8.24 + - source: ghcr.io/simstudioai/simstudio:v0.8.24 + mirror: simstudioai/simstudio:v0.8.24 - source: nvcr.io/nvidia/k8s-device-plugin:v0.18.2 mirror: nvcr.io/nvidia/k8s-device-plugin:v0.18.2 - source: ollama/ollama:0.23.2