CI/CD and GitOps
Overview
Cosmonic Control workloads are distributed as OCI artifacts, so a CI/CD pipeline follows a familiar pattern: build .wasm binaries, push them to an OCI registry, and update Kubernetes manifests to reference the new image.
The wasmCloud project provides a set of official GitHub Actions that handle building components, publishing to OCI registries, and generating supply-chain attestations. For teams using GitOps, Argo CD or Flux can close the loop by reconciling Kubernetes state with a Git repository.
GitHub Actions
wasmCloud provides four GitHub Actions for CI pipelines. Because Cosmonic Control is built on wasmCloud, these actions work directly with Cosmonic Control deployments.
| Action | Description |
|---|---|
wasmCloud/setup-wash-action | Installs the wash CLI on the runner |
wasmcloud/actions/setup-wash-cargo-auditable | Configures cargo-auditable to embed SBOM data in Rust builds |
wasmcloud/actions/wash-build | Builds a Wasm component, outputs the path to the built artifact |
wasmcloud/actions/wash-oci-publish | Publishes a component to an OCI registry with optional attestation and SBOM |
setup-wash-action
The setup-wash-action installs wash, adds it to PATH, caches the binary, and installs the wasm32-wasip2 Rust target.
- uses: wasmCloud/setup-wash-action@main
with:
wash-version: "wash-v2.0.0-rc.7" # version to install (default: wash-v2.0.0-rc.7)setup-wash-cargo-auditable
The setup-wash-cargo-auditable action installs cargo-auditable and cargo-audit, then configures .wash/config.yaml so that wash build uses cargo auditable build under the hood. This embeds dependency metadata in the compiled binary for later SBOM extraction.
A Cargo project (Cargo.toml) must already exist in the working directory before calling this action, as it reads the package name to determine the component output path.
- uses: wasmcloud/actions/setup-wash-cargo-auditable@main
with:
working-directory: "." # directory containing the project (default: .)wash-build
The wash-build action runs wash build --output json and exposes the path to the built component as a step output.
- id: build
uses: wasmcloud/actions/wash-build@main
with:
working-directory: "." # directory containing the project (default: .)Output: steps.build.outputs.component_path — path to the built .wasm file.
wash-oci-publish
The wash-oci-publish action pushes the built component to an OCI registry. When attestation is enabled, the action generates build provenance and an SBOM (converted from CycloneDX to SPDX format).
- uses: wasmcloud/actions/wash-oci-publish@main
with:
component_path: ${{ steps.build.outputs.component_path }} # required
registry: ghcr.io # default: ghcr.io
attestation: "true" # default: false
image_tags: "latest,v1.0.0,${{ github.sha }}" # default: branch nameWhen attestation is enabled, the workflow needs the following permissions. See Supply chain security for details.
permissions:
contents: write
packages: write
attestations: write
id-token: writeExample: Build and publish pipeline
The following GitHub Actions workflow builds a Wasm component, publishes it to GitHub Container Registry, and generates supply-chain attestations:
name: Build and Publish Component
on:
push:
tags:
- "v*"
permissions:
contents: write
packages: write
attestations: write
id-token: write
jobs:
build-and-publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup wash CLI
uses: wasmCloud/setup-wash-action@main
- name: Setup cargo-auditable
uses: wasmcloud/actions/setup-wash-cargo-auditable@main
- name: Build component
id: build
uses: wasmcloud/actions/wash-build@main
- name: Publish component
uses: wasmcloud/actions/wash-oci-publish@main
with:
component_path: ${{ steps.build.outputs.component_path }}
registry: ghcr.io
attestation: "true"
image_tags: "latest,${{ github.ref_name }}"This pipeline triggers on version tags (e.g. v1.0.0). The published image will be tagged with both latest and the Git tag.
GitOps with Argo CD
For production deployments, a GitOps workflow keeps Kubernetes state in sync with a Git repository. Argo CD pairs well with Cosmonic Control's Kubernetes-native resource model.
Two-application pattern
A common pattern uses two Argo CD Applications:
- Infrastructure Application — deploys the Cosmonic Control platform (operator, NATS, Envoy, HostGroups) from Helm charts.
- Workloads Application — deploys
HTTPTriggerandWorkloadDeploymentmanifests from a dedicated Git repository.
This separation lets platform and application teams operate independently, each managing their own deployment cadence.
Example Argo CD Applications
Infrastructure Application — installs Cosmonic Control via Helm:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: cosmonic-control
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: '1'
spec:
project: default
source:
chart: cosmonic-control
repoURL: ghcr.io/cosmonic
targetRevision: 0.3.0
helm:
valuesObject:
envoy:
service:
type: LoadBalancer
destination:
name: in-cluster
namespace: cosmonic-system
syncPolicy:
automated: {}
syncOptions:
- CreateNamespace=true
retry:
limit: -1
backoff:
duration: 30s
factor: 2
maxDuration: 5mHostGroup Application — deploys a HostGroup alongside the platform:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: hostgroup
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: '2'
spec:
project: default
source:
chart: cosmonic-control-hostgroup
repoURL: ghcr.io/cosmonic
targetRevision: 0.3.0
destination:
name: in-cluster
namespace: cosmonic-system
syncPolicy:
automated: {}
retry:
limit: -1
backoff:
duration: 30s
factor: 2
maxDuration: 5mWorkloads Application — syncs HTTPTrigger manifests from a Git repository:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: wasm-workloads
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/<org>/wasm-workloads.git
targetRevision: main
path: manifests
destination:
server: https://kubernetes.default.svc
namespace: default
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- ServerSideApply=trueAutomating manifest updates
After the CI pipeline publishes a new component image, a second workflow job can update the HTTPTrigger manifest in the workloads repository and open a pull request:
update-manifests:
needs: build-and-publish
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
repository: <org>/wasm-workloads
token: ${{ secrets.WORKLOADS_REPO_TOKEN }}
- name: Update image tag
run: |
sed -i "s|image: ghcr.io/<org>/my-component:.*|image: ghcr.io/<org>/my-component:${{ github.ref_name }}|" \
manifests/my-component.yaml
- name: Create pull request
uses: peter-evans/create-pull-request@v7
with:
title: "Update my-component to ${{ github.ref_name }}"
commit-message: "chore: update my-component to ${{ github.ref_name }}"
branch: "update-my-component-${{ github.ref_name }}"Once merged, Argo CD detects the change and rolls out the new version automatically.
This pattern works with any GitOps tool that watches a Git repository for changes, including Flux.
Walkthrough: hello-world end-to-end
The following walkthrough demonstrates the full GitOps loop — from a GitHub release to a live update on your cluster — using the control-demos repository.
Requirements
kubectl- Helm v3.8.0+
- GitHub account
Set up a local cluster
Install k3d (requires Docker) and create a cluster with port 80 mapped for Envoy ingress:
k3d cluster create cosmonic \
--port "80:80@loadbalancer" \
--k3s-arg "--disable=traefik@server:0"Fork the demo repository
Navigate to the control-demos repository on GitHub and fork it. Clone your fork and navigate to the Argo CD integration directory:
git clone https://github.com/<your-github-namespace>/control-demos.git
cd control-demos/integrations/argo-cdDeploy Argo CD
Install Argo CD using the community Helm chart (authentication disabled for this demo):
helm install argocd oci://ghcr.io/argoproj/argo-helm/argo-cd \
--set-string configs.params."server\.disable\.auth"=true \
--version 8.1.3 \
--create-namespace -n argocdPort-forward to access the Argo CD dashboard:
kubectl port-forward service/argo-cd-argocd-server -n argocd 3000:443Open localhost:3000 to see the dashboard.
Deploy Cosmonic Control
Apply the infrastructure Applications from integrations/argo-cd/:
kubectl apply -f control-proj.yaml
kubectl apply -f hostgroup-proj.yamlThe Applications will appear on the Argo CD dashboard. Wait for them to finish syncing.

Deploy the hello-world workload
Edit hello-proj.yaml to point to your fork:
+ repoURL: https://github.com/<your-github-namespace>/control-demos.git
- repoURL: https://github.com/cosmonic-labs/control-demos.gitThis Application targets an HTTPTrigger manifest in your fork:
apiVersion: control.cosmonic.io/v1alpha1
kind: HTTPTrigger
metadata:
name: hello-world
namespace: default
spec:
replicas: 1
ingress:
host: 'hello.localhost.cosmonic.sh'
paths:
- path: /
pathType: Prefix
template:
spec:
components:
- name: http
image: ghcr.io/cosmonic-labs/control-demos/hello-world:0.1.2Apply it:
kubectl apply -f hello-proj.yamlThe hello-world Application should appear healthy and synced in the dashboard shortly.

Trigger a release
Now create a new release in your fork to trigger the CI pipeline. Navigate to https://github.com/<your-github-namespace>/control-demos/releases/new, create a tag 1.1.0, and publish the release.
The release triggers a GitHub Workflow that:
- Builds the Wasm component using the
setup-wash-action - Pushes the component to GHCR as an OCI artifact under your namespace
- Updates the
imagefield in theHTTPTriggermanifest to reference1.1.0in your registry - Opens a pull request with the manifest change
The manifest update step uses the same sed + create-pull-request pattern shown in Automating manifest updates:
- name: Update image tag in Kubernetes manifest
working-directory: ./hello-world
run: |
DEPLOYMENT_FILE="manifests/component.yaml"
NEW_IMAGE="ghcr.io/${{ env.GHCR_REPO_NAMESPACE }}/components/hello-world:${{ github.ref_name }}"
sed -i "s|image:.*|image: $NEW_IMAGE|" "$DEPLOYMENT_FILE"
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "Update image tag in manifest to ${{ github.ref_name }}"
title: "Update image tag in manifest to ${{ github.ref_name }}"Once the workflow completes, merge the pull request. Argo CD will detect the manifest change and roll out the new version automatically.

Test the deployment
curl http://hello.localhost.cosmonic.shHello from Cosmonic Control and Argo CD!Clean up
kubectl delete -f control-proj.yaml
kubectl delete -f hostgroup-proj.yaml
kubectl delete -f hello-proj.yaml
helm uninstall argocd -n argocd
k3d cluster delete cosmonicSupply chain security
The wasmCloud GitHub Actions support a full supply-chain security pipeline using cargo-auditable, CycloneDX, and GitHub's built-in attestation actions.
The attestation flow works as follows:
setup-wash-cargo-auditableconfigurescargo-auditablevia.wash/config.yamlso that dependency metadata is embedded in the compiled binary duringwash build.wash-oci-publish(withattestation: "true") extracts the embedded metadata and generates attestations:- Extracts a CycloneDX SBOM from the binary using
auditable2cdx - Converts the SBOM to SPDX format using
cyclonedx-cli - Generates an SBOM attestation via
actions/attest-sbom - Generates build provenance via
actions/attest-build-provenance
- Extracts a CycloneDX SBOM from the binary using
For attestation to work, the workflow must include the following permissions:
permissions:
contents: write # required for attestation uploads
packages: write # required for OCI registry push
attestations: write # required for attestation creation
id-token: write # required for OIDC token (provenance signing)Without all four permissions, the attestation steps will fail. If you don't need attestation, omit these permissions and set attestation: "false" (the default).
Consumers can verify a published artifact using the GitHub CLI:
gh attestation verify oci://ghcr.io/<org>/<component>:<tag> --repo <org>/<repo>