Git, CI/CD & GitOpsLII · Kubernetes CIPackageAndPublish
Render and package as OCI — the artifact handoff to the GitOps controller
What you'll learn
- Run helm package and push the chart to an OCI registry
- Push a Kustomize render to an OCI registry as an immutable artifact
- Sign the packaged artifact with cosign
- Explain why the GitOps controller pulls by digest rather than by tag
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
The package stage is the handoff between CI and the GitOps controller. The input is a validated render — a stream of YAML that has passed kubeconform, Conftest, Trivy, and Kubescape. The output is an immutable OCI artifact identified by a content hash. The GitOps controller pulls that artifact by digest, applies it to the cluster, and verifies a cosign signature before applying. The package stage is what makes the handoff deterministic: the artifact that CI published is the artifact the controller applies, with no re-render and no template re-execution.
Why OCI
OCI (Open Container Initiative) is the same registry format that stores container images. Helm 3 supports OCI natively — the chart is packaged as a .tgz, tagged, and pushed to an OCI-compatible registry (Harbor, AWS ECR, Google Artifact Registry, GHCR, Docker Hub). The registry stores the artifact by content hash and serves it by tag, digest, or both.
The reasons OCI is the right format for the package stage:
- Immutability. The content hash identifies the bytes; the registry cannot serve different bytes for the same hash.
- Provenance. cosign signs the digest; the signature is stored alongside the artifact and verifiable by anyone with the public key.
- Tooling. The same registry clients (
crane,oras,cosign) work for charts, Kustomize output, and container images. The CI pipeline uses one toolchain for all three. - Distribution. The same replication, access-control, and audit mechanisms that govern container images govern the packaged manifests.
flowchart LR
A["Validated render"] --> B["helm package / crane push"]
B --> C["OCI registry"]
C --> D["cosign sign"]
D --> E["Signed artifact\n(digest identity)"]
E --> F["GitOps controller\n(pull by digest)"]
helm package
helm package produces a .tgz from a chart directory:
helm package ./chart
The default output is ./mychart-0.1.0.tgz. The --version and --app-version flags override the chart’s Chart.yaml. The .tgz is a versioned archive; the filename embeds the version. Pushing to an OCI registry is a separate helm push step (or the push is implied with --push if the registry is configured):
helm registry login registry.example.com -u user -p pass
helm package ./chart
helm push mychart-0.1.0.tgz oci://registry.example.com/charts
The oci:// scheme tells Helm to push to an OCI registry. The path after the registry (/charts) is the repository namespace; the tag is the version. The result is oci://registry.example.com/charts/mychart:0.1.0.
After push, the registry stores the artifact at the repository path with the tag and the content-addressed digest. The Helm install side uses the same oci:// URL:
helm install myapp oci://registry.example.com/charts/mychart --version 0.1.0
The --version flag is the version pin. Pinning by version is human-readable; pinning by digest is the contract.
Kustomize as OCI
Kustomize has no native OCI packaging. The pattern is to wrap the YAML output in an OCI artifact using crane (from the go-containerregistry toolchain):
kustomize build overlays/prod > rendered.yaml
crane append -f rendered.yaml -t registry.example.com/manifests/myapp:0.1.0 \
--base-image alpine:3.19 \
--new-tag 0.1.0
The crane append command takes the rendered YAML, adds it as a layer to a base image, and pushes the result as a single OCI artifact. The artifact’s digest is the content hash of the rendered YAML plus the base image layers. Pulling the artifact and extracting the YAML is the reverse:
crane export registry.example.com/manifests/myapp@sha256:${DIGEST} - | tar -x
An alternative is to use ORAS (OCI Registry As Storage) directly. ORAS pushes arbitrary artifacts to an OCI registry without the container-image wrapper:
kustomize build overlays/prod > rendered.yaml
oras push registry.example.com/manifests/myapp:0.1.0 \
--artifact-type application/vnd.kubernetes.config.v1+yaml \
rendered.yaml:application/vnd.kubernetes.config.v1+yaml
The ORAS push uses an OCI artifact type that identifies the content as Kubernetes manifests. The registry stores it like any other artifact; the pull side uses oras pull to retrieve it.
cosign signatures
A signature on the artifact is what makes the handoff trustworthy. cosign (from the sigstore project) signs OCI digests:
cosign sign registry.example.com/manifests/myapp@sha256:${DIGEST}
The signature is stored in the registry alongside the artifact. Verification at pull time:
cosign verify registry.example.com/manifests/myapp@sha256:${DIGEST}
The verify step checks the signature against a public key (key-based verification) or against a Fulcio-issued certificate bound to an OIDC identity (keyless verification). The GitOps controller performs the verify step before applying the manifest; a manifest with an invalid or missing signature is rejected.
The signature is the chain-of-trust extension from the Git commit (Part III-05) to the OCI artifact. A signed commit attests the source change; a signed artifact attests the render. The two together let an auditor trace a production deployment back to a commit, a pull request, a CI run, and an artifact — every link in the chain has a signature.
Pull by digest
The GitOps controller pulls by digest, not by tag:
# Argo CD Application
spec:
source:
repoURL: registry.example.com/manifests/myapp
targetRevision: sha256:${DIGEST}
The targetRevision field accepts a digest. The controller pulls the artifact at that exact digest; the tag is irrelevant because the digest is the content hash. A later push to the same tag with different bytes produces a different digest; the controller’s pinned digest resolves to the original bytes only.
flowchart LR
A["CI: helm package + push"] --> B["OCI registry"]
B --> C["cosign sign"]
C --> D["Signed artifact\n(digest identity)"]
D --> E["GitOps controller\n(targetRevision: sha256:${DIGEST})"]
E --> F["cosign verify"]
F --> G["Apply to cluster"]
The pull-by-digest contract is what makes the artifact identity stable. A tag can move; a digest cannot. A version pin (--version 0.1.0) is pinning by mutable handle; a digest pin is pinning by content hash. The production pattern is digest.
Production discipline
- Always sign the artifact. cosign sign is the chain-of-trust extension from the Git commit to the OCI artifact. An unsigned artifact is a render that anyone could have produced.
- Always pull by digest. The GitOps controller’s
targetRevisionmust be a digest, not a tag. A tag pin is a moving target. - Version the chart, pin the digest. The chart’s
versionfield is the human-readable handle; the artifact’s digest is the contract. Both are required; neither replaces the other. - Re-render on every change. The CI pipeline renders the chart (or the overlay) on every pull request. A re-render produces a new digest; the new digest is the new artifact identity. Skipping the re-render skips the audit surface.
Cross-course references
- Kubernetes for Production Sysadmins - Part X (Manifests) covers the Kustomize and Helm mechanics the package stage assumes.
- GitOps with Argo CD - Part IV (ArgoCD) covers the controller side of the pull-by-digest contract.
- This course, Part LII-01 (Foundations) - the three-stage pipeline (render, validate, package) the package stage completes.
Quiz
Knowledge check · 4 questions
Q1. A team pushes a chart to an OCI registry with `helm push mychart-0.1.0.tgz oci://registry.example.com/charts` and configures the GitOps controller to install with `helm install myapp oci://registry.example.com/charts/mychart --version 0.1.0`. What is the production risk?
Q2. Signing an OCI artifact with cosign proves that the GitOps controller is authorised to apply the artifact to the cluster.
Q3. Name the canonical Helm command that produces a chart `.tgz` and the canonical cosign command that signs the OCI artifact after push.
Q4. Diagnose a supply-chain incident where a Helm chart is pushed without a cosign signature and a malicious version is installed by the GitOps controller, and recommend the package-stage fix.
A team pushes a chart with `helm push` but does not sign with cosign. An attacker who compromises the CI runner's OCI credentials pushes a malicious `mychart:0.1.0` to the same registry repository. The GitOps controller is configured to install `--version 0.1.0` and pulls the new bytes. The malicious chart deploys a workload with a privileged pod that exfiltrates a secret.
Passing score: 75%. Answers are checked in this browser.