Skip to main content

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.

ActionDescription
wasmCloud/setup-wash-actionInstalls the wash CLI on the runner
wasmcloud/actions/setup-wash-cargo-auditableConfigures cargo-auditable to embed SBOM data in Rust builds
wasmcloud/actions/wash-buildBuilds a Wasm component, outputs the path to the built artifact
wasmcloud/actions/wash-oci-publishPublishes 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.

note

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 name
note

When attestation is enabled, the workflow needs the following permissions. See Supply chain security for details.

permissions:
  contents: write
  packages: write
  attestations: write
  id-token: write

Example: 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:

  1. Infrastructure Application — deploys the Cosmonic Control platform (operator, NATS, Envoy, HostGroups) from Helm charts.
  2. Workloads Application — deploys HTTPTrigger and WorkloadDeployment manifests 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: 5m

HostGroup 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: 5m

Workloads 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=true

Automating 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.

note

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

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-cd

Deploy 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 argocd

Port-forward to access the Argo CD dashboard:

kubectl port-forward service/argo-cd-argocd-server -n argocd 3000:443

Open 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.yaml

The Applications will appear on the Argo CD dashboard. Wait for them to finish syncing.

healthy apps

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.git

This 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.2

Apply it:

kubectl apply -f hello-proj.yaml

The hello-world Application should appear healthy and synced in the dashboard shortly.

Hello world synced

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:

  1. Builds the Wasm component using the setup-wash-action
  2. Pushes the component to GHCR as an OCI artifact under your namespace
  3. Updates the image field in the HTTPTrigger manifest to reference 1.1.0 in your registry
  4. 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.

Successful sync

Test the deployment

curl http://hello.localhost.cosmonic.sh
Hello 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 cosmonic

Supply 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:

  1. setup-wash-cargo-auditable configures cargo-auditable via .wash/config.yaml so that dependency metadata is embedded in the compiled binary during wash build.
  2. wash-oci-publish (with attestation: "true") extracts the embedded metadata and generates attestations:

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)
warning

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>