feat(helm): publish the chart to GHCR as a signed OCI artifact - #7563
Conversation
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryPublishes the Sim Helm chart through OCI and classic Helm repositories after existing chart validation succeeds.
Confidence Score: 5/5The PR appears safe to merge because no unresolved or newly introduced actionable failures remain. All previous findings are resolved, no code changed after the previous review, and the full-PR rules check found no concrete violations.
|
| Filename | Overview |
|---|---|
| .github/workflows/helm.yml | Adds gated OCI signing, provenance, Artifact Hub metadata publishing, and classic Helm repository release jobs. |
| helm/artifacthub-repo.yml | Adds repository-level Artifact Hub ownership metadata outside the packaged chart. |
| helm/sim/Chart.yaml | Bumps the chart and application versions and adds the Artifact Hub category. |
| helm/sim/README.md | Documents pinned OCI and HTTP installation, mirroring, and Sigstore verification. |
| apps/docs/content/docs/platform/self-hosting/kubernetes.mdx | Replaces checkout-based deployment guidance with pinned published-chart installation and verification. |
| helm/sim/images.yaml | Aligns the mirrored application image inventory with appVersion v0.8.24. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
Push[Push to main] --> Chart[Lint, test, and validate chart]
Push --> Install[Install and test on kind]
Chart --> OCI[OCI publish job]
Install --> OCI
Chart --> HTTP[HTTP repository publish job]
Install --> HTTP
OCI --> Package[Package chart]
Package --> Version{Version already published?}
Version -->|No| GHCR[Push chart to GHCR]
GHCR --> Sign[Sign and verify by digest]
Sign --> Attest[Attach build provenance]
Version -->|Yes| Skip[Preserve immutable version]
Attest --> Metadata[Publish Artifact Hub metadata]
Skip --> Metadata
HTTP --> Pages{gh-pages exists?}
Pages -->|Yes| Release[Publish chart release and index]
Pages -->|No| Warn[Skip with warning]
Reviews (12): Last reviewed commit: "fix(helm): fail the appVersion check onl..." | Re-trigger Greptile
…nstalls The publish guard treated every non-zero `helm show chart` exit as proof the version was unpublished, so a transient 5xx, an expired token, or a DNS blip would have enabled a push that moves an already-published version tag. Runs where the version already exists are routine — the path filter also fires on package.json and workflow edits — so this was reachable. Now only an explicit `: not found` counts as absent; anything else stops the job. Verified against the pinned Helm v3.16.4 that an absent version and an absent repository both report `: not found`, so a first publish still proceeds, while denied/unauthorized/dial-tcp failures abort instead. Also pins the two documented install paths that were still reproducible only by accident: the cloud-specific command paired a pinned chart with values fetched from the moving main branch, and the README quick start omitted --version while the same README warns against exactly that.
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
…sign 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.
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
…atches Dry-ran the publish sequence against a local OCI registry, which surfaced one divergence from Artifact Hub's documented command: passing a path-qualified argument to oras records the layer's title annotation as `helm/artifacthub-repo.yml` rather than the bare `artifacthub-repo.yml` the docs produce. Artifact Hub selects the layer by media type, so this was probably harmless, but there is no reason to diverge from the documented form. Running the step from helm/ reproduces it exactly. Everything else in the sequence behaved as intended against a real registry: helm push derives charts/sim:1.9.1 from the chart name and version, the digest grep extracts it, the metadata lands with both documented media types under the artifacthub.io tag, the chart and metadata tags coexist without disturbing each other, and helm pull round-trips to an identical sha256. Also confirmed the overwrite hazard behind the fail-closed guard is real rather than theoretical: re-pushing modified content under an existing version moved the tag, so a pinned consumer would have silently received different bytes.
The chart carried `category: developer-tools`, which is a different convention entirely -- Artifact Hub reads `artifacthub.io/category` and accepts only one value from a fixed list that has no developer-tools entry. The listing this PR sets up would therefore have published uncategorised. Adds `artifacthub.io/category: ai-machine-learning`, the closest fit in that list, and keeps the bare annotation for whatever else consumes it. Bumps to 1.9.2 per the chart's own gate and moves the documented versions with it. Deliberately not adding `artifacthub.io/images`: Artifact Hub already extracts images from a dry-run render, and a hand-listed copy would be a second source of truth next to the generated images.yaml.
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
I claimed earlier that OCI-only was defensible in 2026. That was wrong, and checking rather than reasoning settled it: Bitnami, cert-manager, ingress-nginx, prometheus-community, Grafana, Argo and external-secrets all still serve a live index.yaml. Dual-publish is the actual convention, and external-secrets -- the workflow this one is modelled on -- does both. Adds a chart-releaser job that maintains index.yaml on a pages branch and attaches each packaged chart to a GitHub release. It is a separate job from the OCI publish because it needs contents: write to cut that release, and that permission has no business sitting in the job that holds the signing identity. Three details worth stating: - mark_as_latest is false. A chart release must never take the "Latest" badge from the application release it packages. - Releases are named helm-chart-<version> so they stay distinguishable from the vX.Y.Z app releases in the same list. - skip_existing mirrors the OCI job: re-running on a published version is a no-op rather than an overwrite. The job no-ops with a warning until a gh-pages branch exists, because creating that branch and enabling Pages are one-time manual steps no workflow can perform for itself, and main's CI must not fail on their absence. No documentation yet. index.yaml is only reachable once the branch, Pages, and DNS are in place, and pointing users at a hostname that does not resolve is the exact failure this whole PR exists to remove.
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
Held these back until the hostname actually resolved, because shipping an install command that points at a dead domain is the exact failure this work exists to remove. It resolves now: DNS is in place, GitHub Pages serves the branch, and Let's Encrypt has issued the certificate. Verified end to end rather than assumed: helm repo add sim https://charts.sim.ai helm repo update # Successfully got an update Presents OCI as the preferred path in both places and the repository as the compatibility path, and says plainly that the signature and provenance are attached to the OCI artifact only -- `helm repo add` has no equivalent, and leaving that implicit would let someone believe they had verified something they had not. Bumps to 1.9.3 per the chart's own gate, since this touches helm/sim/.
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
The Terraform page still told readers the chart "is not published to a Helm repository or an OCI registry, so there is no repository to point at", and had them vendor the repo as a submodule or git clone. That is now false, and it is the same claim that makes a self-hoster conclude Sim is not packaged for them, so it is the most important line in the docs to correct. It now points repository at the OCI registry with a pinned version, notes the classic repository as the alternative for tooling that does not speak OCI, and replaces the "because the chart is local, version does nothing" paragraph -- pinning the chart version is now both possible and the thing that stops an unplanned apply pulling new migrations. Syntax checked against the provider docs; the page's existing set_sensitive list form is already v3. Also switches the two upgrade commands and the env render check off ./helm/sim, which no longer exists for anyone following the install path. Deliberately unchanged, because these are not install references: - The chart README links on the Kubernetes page. The README lives in the repo; charts.sim.ai serves index.yaml, not documentation. - The cosign --certificate-identity-regexp. That is a Sigstore OIDC identity that happens to look like a URL, and rewriting it would break verification. - The releases page and issue tracker links, which point where they should. - The git clone in the quick start, which is the Docker Compose path and genuinely does need the repo.
Swept the remaining checkout-era references and audited the Helm docs by checking every values path they mention against values.yaml rather than reading them. That found a real error: the chart has no `copilot.env`. The map is `copilot.server.env`, so anyone following the env-validation note would have set a key that silently does nothing. Fixed. It also flagged `app.envDefaults.FREE_TABLES_LIMIT` and `app.secrets.existingSecret.keys`, which are NOT bugs -- both appear only in upgrade notes describing keys older releases shipped and this one removed. Left exactly as they are; "fixing" them would delete the history an upgrader needs. The chart README was the bigger problem. It led with registry install after the earlier commits, then used `./helm/sim` in eleven subsequent commands -- upgrade, dry-run, examples, parameters, secrets, troubleshooting. Someone following the documented install had no such path. They now use the OCI reference with a pinned version, with one note establishing that convention so the checkout form does not have to be repeated everywhere. Two related corrections while in there: the example values files are not part of the packaged chart, so the examples section now fetches one at a release tag instead of assuming it locally; and values.schema.json cannot be `cat`ed without a checkout, so it links to the repo or `helm pull --untar`. Left pointing at GitHub on purpose: the chart README deep links (that is where the README lives, and an extra hop through the repository root would be worse), the releases page, the issue tracker, the cosign identity regexp, and the Docker Compose quick start's git clone. Bumps to 1.9.4 per the chart's own gate.
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
appVersion sat at v0.8.18 while the app shipped through v0.8.24 -- six releases. Because the image tags default to Chart.AppVersion, a default `helm install` deployed the older Sim, and regenerating images.yaml here moved eleven lines, so the mirror inventory an air-gapped operator works from named the wrong images too. Publishing is what makes this serious. Cloning main got you whatever was there; a published chart version is immutable, so every stale appVersion would be frozen and installable forever. It was bumped by hand and nothing checked it, which is why it drifted. The publish job now refuses to publish when appVersion does not match the latest GitHub release, comparing against the release API rather than a hardcoded value so the check cannot go stale itself. Confirmed it fires on exactly the drift that existed (v0.8.18 vs v0.8.24) and passes now that it is fixed. Bumps appVersion to v0.8.24, regenerates images.yaml, and takes the chart to 1.9.5.
…smatch The check compared appVersion to /releases/latest for equality, which races the release it is meant to protect. Version tags are cut by the main-branch merge commit that releases them, so on that commit appVersion legitimately names a release that does not exist yet while ci.yml is still building it. Equality would have rejected the bump and blocked the very publish it was for. helm/sim/ci/kind-values.yaml already documents this circularity, and records it as the reason appVersion went unbumped from chart 1.2.0 to 1.6.3. Lagging is the failure; being ahead is a normal transient. The comparison is `sort -V`, so it orders versions rather than strings -- v0.9.9 against v0.10.0 is exactly where a string compare silently inverts. Verified all five cases: behind fails, equal passes, ahead passes with a notice, and both multi-digit orderings resolve correctly.
Summary
helm/simtooci://ghcr.io/simstudioai/charts/simon push tomain. The chart has only ever existed inside the repo, which means self-hosters whose security process requires an officially published artifact can't use it — a chart pulled from a git checkout isn't something they can pin, mirror, or admit.helm/artifacthub-repo.ymlto the reservedartifacthub.iotag so the chart can be listed on Artifact Hub and the repo ownership claimed.Chart.yamlalready carried theartifacthub.io/*annotations.Design notes
helm.yml, not a newpublish-*.yml. It gates onneeds: [chart, install], so nothing publishes unless the chart linted, passed its 108 unit tests, rendered clean under kubeconform, and actually installed on a kind cluster. A separate workflow would race those instead of waiting for them. This follows the release-job-in-the-chart-workflow pattern used by external-secrets and others.Chart.yamlversion bump on every chart change, so a skip means the commit changed something outsidehelm/sim/.cosign verifystep immediately after signing isn't ceremony: it fails the run if the signature can't be read back with the expected identity.helm install --verifyexpects the GPG.provformat and won't apply; the docs say so and give thecosign verifyequivalent instead.simstudioai/simso a fork'smainnever attempts to publish into a namespace it doesn't own.Operational note before this can serve traffic
The first push creates a private GHCR package. Someone with org admin needs to flip
simstudioai/charts/simto public once, orhelm installwill 401 for everyone. Nothing in the workflow can do this itself.Optional follow-up: register the repo at artifacthub.io and fill in
repositoryIDinhelm/artifacthub-repo.ymlfor the Verified Publisher badge. The listing works without it.Type of Change
Testing
Validated locally against the chart:
helm lintclean; both CI value sets (default-values.yaml,full-values.yaml) renderhelm packageproduces a clean.tgz— confirmedartifacthub-repo.yml,examples/,tests/, and.claude/are all excludedawkextraction of chart name/version matches exactly once each against realhelm show chartoutput (themaintainersentry is indented, so it doesn't collide)--valuesover HTTPS, which is what lets the cloud-specific docs work without a checkout now that the example files aren't in the packaged chartcheck:cron-parityandimages:check(the existing helm CI gates) passbun run lint,check:audits(45 audits),docs-manifest:check, and the block-registry audit all passThe publish path itself can only be exercised on a
mainpush — it's gated onpush+refs/heads/main, so this PR runs everything except the publish job.Checklist