CI/CD and GitOps

Automate Fission function deployment from CI/CD with declarative fission spec apply, a GitHub Actions pipeline, and registry-native OCI image delivery.

This guide shows how to deploy Fission functions from a CI/CD pipeline instead of by hand. It builds on two pieces that work together:

  • Declarative YAML specs — your functions, environments, and triggers as version-controlled YAML, reconciled onto the cluster with one idempotent command. This is the CI/CD control plane.
  • OCI image packages — ship function code as a digest-pinned container image your pipeline already knows how to build, push, sign, and scan. This makes the deployed artifact registry-native.

Use specs alone to deploy from source, or add OCI delivery for an immutable, promote-by-digest GitOps flow — and because an OCI package carries no archive to upload, you can apply it with the Fission CLI, plain kubectl, or Argo CD / Flux.

The foundation: fission spec apply

A CI/CD pipeline for Fission is, at its core, a job that runs fission spec apply against your cluster. Because apply is idempotent — it reconciles the cluster to match the committed specs (creating, updating, and pruning) — the same job is safe to run on every push.

fission spec apply --wait   # block until builds finish; exits non-zero on failure

Keep the specs/ directory in version control next to your code, and generate resources with --spec (for example fission function create --spec ...) so every change is reviewable in a pull request. See YAML Specs for the full spec workflow.

Deploy from GitHub Actions

A minimal pipeline installs the Fission CLI, points it at your cluster with a KUBECONFIG secret, and applies the specs on every push to main:

name: Deploy to Fission
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install the Fission CLI
        run: |
          curl -Lo fission https://github.com/fission/fission/releases/download/v1.26.0/fission-v1.26.0-linux-amd64
          chmod +x fission && sudo mv fission /usr/local/bin/

      - name: Configure cluster access
        run: |
          mkdir -p ~/.kube
          echo "${{ secrets.KUBECONFIG }}" | base64 -d > ~/.kube/config

      - name: Apply specs
        run: fission spec apply --wait

Store a base64-encoded kubeconfig — ideally for a dedicated, least-privilege ServiceAccount — as the KUBECONFIG repository secret. The same shape works on any CI system: install the CLI, provide cluster credentials, run fission spec apply.

This supersedes the older .github/main.workflow approach from the 2019 Fission GitHub Action post — GitHub has since replaced that syntax with the YAML workflows shown here.

Registry-native CI/CD with OCI packages

The pipeline above uploads your source to the cluster and builds it there. With OCI image packages, your pipeline instead builds a small code image, pushes it to a registry it already uses, and the package references that image by digest — no separate upload step, and a deploy that is immutable and verifiable.

flowchart TB
  ci["CI pipeline"]:::user -->|"<b>1.</b> build + push code image"| reg["OCI Registry"]:::store
  ci -->|"<b>2.</b> deploy by digest"| fission["Fission"]:::fission
  fission -->|"<b>3.</b> roll function"| pod["Function Pod"]:::pod
  reg -->|"<b>4.</b> pull image at cold start"| pod
  classDef user fill:#ffffff,stroke:#94a3b8,color:#1f2a43
  classDef fission fill:#e8f0fe,stroke:#2d70de,color:#1f2a43
  classDef pod fill:#e6f7f1,stroke:#11a37f,color:#1f2a43,stroke-dasharray:5 3
  classDef store fill:#fff7e0,stroke:#dba514,color:#1f2a43,stroke-dasharray:5 3

A code image is just your function files on top of scratch:

# Dockerfile
FROM scratch
COPY hello.js /

The workflow builds and pushes that image, then deploys the package pinned to the pushed digest:

name: Build and deploy (OCI)
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4

      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push the code image
        id: push
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository }}/hello-code:${{ github.sha }}

      - name: Install the Fission CLI
        run: |
          curl -Lo fission https://github.com/fission/fission/releases/download/v1.26.0/fission-v1.26.0-linux-amd64
          chmod +x fission && sudo mv fission /usr/local/bin/

      - name: Configure cluster access
        run: |
          mkdir -p ~/.kube
          echo "${{ secrets.KUBECONFIG }}" | base64 -d > ~/.kube/config

      - name: Deploy the pinned image
        run: |
          fission package update --name hello \
            --oci "ghcr.io/${{ github.repository }}/hello-code@${{ steps.push.outputs.digest }}"
          fission fn update --name hello --pkg hello

docker/build-push-action exposes the pushed image’s digest as steps.push.outputs.digest, so the function rolls to the exact bytes the pipeline built. For a fully declarative flow, write that digest into the package spec’s deployment.oci.digest field and fission spec apply instead — the spec stays the single source of truth.

Delivering code as an image gives the pipeline:

  • No upload-and-build round trip — the registry your CI already pushes to is the delivery mechanism; cold starts pull cached layers.
  • Supply-chain tooling for free — sign the code image with cosign, scan it, and apply the same registry policies you use for runtime images.
  • Reproducible deploys — a digest reference is immutable, so what you tested is exactly what runs.
Prefer the cluster to publish images for you? Set packageRegistry.enabled (see OCI image packages) and every on-cluster build publishes a digest-pinned image automatically — your pipeline keeps running fission spec apply and gets registry-native delivery without building images itself.

Apply with kubectl — no Fission CLI required

fission spec apply exists mainly to do one thing the Kubernetes API cannot: package your local source, upload it to the cluster’s storage service, and rewrite the spec’s archive:// URLs into real ones. An OCI package has no archive to upload — its code already lives in a registry — so the Package and Function are ordinary Kubernetes custom resources. That means you can manage them with kubectl apply (or Argo CD / Flux) directly, with no Fission CLI in the pipeline at all:

# fission-app.yaml
apiVersion: fission.io/v1
kind: Environment
metadata:
  name: nodejs
spec:
  version: 3
  runtime:
    image: ghcr.io/fission/node-env
---
apiVersion: fission.io/v1
kind: Package
metadata:
  name: hello
spec:
  environment:
    name: nodejs
  deployment:
    type: oci
    oci:
      image: ghcr.io/myorg/hello-code
      digest: sha256:<digest>          # pin to the image your pipeline pushed
---
apiVersion: fission.io/v1
kind: Function
metadata:
  name: hello
spec:
  environment:
    name: nodejs
  package:
    packageref:
      name: hello
    # functionName: <entrypoint>       # optional, e.g. "hello.main"
---
apiVersion: fission.io/v1
kind: HTTPTrigger
metadata:
  name: hello
spec:
  functionref:
    type: name
    name: hello
  methods: [GET]
  relativeurl: /hello

The deploy step is then just kubectl:

kubectl apply -f fission-app.yaml
kubectl wait --for=condition=Ready function/hello

This works because v1.26.0 validates these resources with API-server CEL and ships server-side-apply markers, so kubectl apply --server-side, --dry-run=server, Argo CD, and Flux reconcile Fission resources like any other manifest, and kubectl wait --for=condition=Ready blocks until the function is live. Two prerequisites: install the CRDs once (kubectl create -k "github.com/fission/fission/crds/v1?ref=v1.26.0"), and keep the environment, package, and function in the same namespace.

This is OCI-only. A source or pre-built-archive package still needs fission spec apply (or fission package create) to upload the archive — kubectl cannot perform that upload.

Promote across environments by digest

Because an OCI package is pinned to a digest, promotion is just moving that digest between environments — no rebuild, so staging and production run byte-for-byte what you tested. Build and verify once in a lower environment, then apply the same digest to the next:

fission package update --name hello --oci "ghcr.io/myorg/hello-code@sha256:<digest>"   # in staging
fission package update --name hello --oci "ghcr.io/myorg/hello-code@sha256:<digest>"   # then prod

In a GitOps setup, this is a pull request that bumps the deployment.oci.digest in the next environment’s spec directory, applied by that environment’s pipeline.

Continuous deployment

For environments where you want the cluster to track a spec directory continuously rather than on each pipeline run, fission spec apply --watch re-applies on local changes. In CI/CD, prefer the explicit per-push fission spec apply --wait shown above so each deploy is gated on a green build.