Even Better GitHub Actions For Your Spin Apps

By Thorsten Hans
October 06, 2026

The gh plugin for Spin CLI has been around for quite some time now, and I hope it has served you well so far. In this short post, I’ll guide you through the most recent changes I added to spin gh create-action.

What is the spin gh Plugin

The main purpose of the spin gh plugin is to generate full-fledged GitHub Actions for your Spin applications.

The plugin does not rely on conventions, because repository structures differ dramatically. Instead, it discovers all Spin applications recursively underneath its working directory. Inspecting each application allows the plugin to identify which tools must be installed to successfully compile all applications.

Deployment to Akamai Functions has been possible for quite a while now. With its recent 0.2.1 release, it can also push OCI artifacts for all your Spin applications to OCI-compliant registries.

On top of that, you can opt in to OCI artifact signing and Software Bill of Materials (SBOM) generation.

We tried to come up with reasonable defaults to ensure generated GitHub Actions work for the majority of scenarios. However, if the generated GitHub Action does not meet your expectations or regulatory compliance, you can always “eject” the default template and change it according to your requirements. As a result, the spin gh plugin is a fully customizable GitHub Actions generator for Spin applications.

Installing the Latest Version of the gh Plugin

Obviously, you must have the Spin CLI and the latest version of the gh plugin installed on your system. Although it is not a strict requirement, I highly encourage you to install the latest stable release of Spin (4.2.1 at the time of writing this post).

To upgrade (or install) the latest version of the gh plugin for Spin, you can use the following commands:

# Update the plugin index
spin plugins update

# Upgrade (or Install) the gh plugin
spin plugins upgrade gh --yes

# Check installed version of Spin plugins
# For gh this should report 0.2.1
spin plugins list --installed
gh 0.2.1 [installed]

Scaffolding Demo Apps

For demonstration purposes, we’ll create a monorepo with two independent Spin applications. app-one will be a Rust-based application, while app-two will use TypeScript.

Let’s create a new folder and scaffold both demo applications:

# Create a new top-level folder
mkdir the-new-spin-gh
cd the-new-spin-gh

# Scaffolding the 1st Spin app
spin new -t http-rust --no-vcs -a app-one

# Scaffolding the 2nd Spin app
spin new -t http-ts --no-vcs -a app-two 

As this is all about GitHub Actions, let’s create a new GitHub repository (usually, I prefix these kinds of repositories with tmp-- or delete-me--). If you are coding along, you can choose a more realistic or meaningful name:

# Init the Git repository
git init

# Create a new GitHub repository
gh repo create tmp--the-new-spin-gh \
  -d "Exploring latest features of spin gh" \
  --public \
  --source .

Let’s add, commit, and push our two demo applications:

# Create a simple README
echo '# Exploring latest features of spin gh' >> README.md

# Use git to do git things
git add .
git commit -sm 'chore: Add two sample applications'
git push -u origin main

The Basics

You can use the spin gh create-action command to generate a new GitHub Actions workflow file. The command has a massive number of flags allowing you to customize almost everything, from events that should trigger your workflow to tool versions, all the way down to Spin plugins that must be installed on the runner.

Just give it a try. By appending the dry-run flag (spin gh create-action --dry-run), you’ll get a preview rendered to stdout instead of writing it to disk.

You can generate a pretty standard CI workflow for our demo applications using the following command:

spin gh create-action \
  --ci main \
  --name "Demo Apps CI"

By default, this will create a new GitHub Actions workflow file at .github/workflows/ci.yaml. Without opting in to any shiny new features, we end up with the following pipeline:

name: "CI"
on:
  push:
    branches:
      - "main"
env:
  RUST_VERSION: "1.98.1"
  RUST_TARGET: "wasm32-wasip2"
  NODE_VERSION: "26"
jobs:
  spin:
    runs-on: "ubuntu-latest"
    name: Build Spin App
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@v4
      - name: Install Rust
        uses: dtolnay/rust-toolchain@stable
        with:
          toolchain: "${{ env.RUST_VERSION }}"
          targets: "${{ env.RUST_TARGET }}"
      - name: Install Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "${{ env.NODE_VERSION }}"
      - name: Install Spin
        uses: fermyon/actions/spin/setup@v1
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
      - name: Build app-one
        run: spin build
        working-directory: ./app-one
      - name: Build app-two
        run: spin build
        working-directory: ./app-two

The plugin figured out that Rust and Node.js are required. It installed the latest Spin CLI and compiled each Spin application by running spin build in the correct working directory. Neat, but building is only half the story.

Publishing Spin Apps as OCI Artifacts

The first new feature is behind the --push-oci-artifacts flag. It adds all the steps necessary to publish your Spin app(s) to an OCI registry as part of your workflow:

spin gh create-action --ci main \
  --name "Demo Apps CI" \
  --push-oci-artifacts \
  --overwrite

In contrast to most other flags, --push-oci-artifacts can’t figure out everything on its own. Where should your apps be published? Which tag(s) do you want to use? Does the registry require authentication? Those are decisions only you can make. That’s why the plugin presents an interactive Terminal User Interface (TUI) and walks you through the configuration—one app at a time. Here’s what you’ll be asked for every discovered Spin app:

  • Artifact name - The full repository path of your artifact, without a tag (for example, ghcr.io/thorstenhans/tmp-app-one). This one is required.
  • Tags - Which tag(s) should be published on every run? You can pick from latest, the commit SHA (one immutable tag per commit), both, or provide your own custom tag(s). If you go for custom tags, the plugin asks for a comma-separated list next (for example, v1.0.0, staging).
  • Authentication - Does the target registry require authentication? If you say yes, the plugin asks for the login server and the username to use. It won’t ask for the corresponding password or token; instead, it will inject a GitHub repository secret.
spin gh create-action - Interactive TUI

There’s one convenient shortcut baked in: whenever your artifact name points to the GitHub Container Registry (prefixed with ghcr.io), the plugin detects it automatically and authenticates using the built-in GITHUB_TOKEN. No extra questions, no extra secrets. That’s exactly the path I picked for both demo apps.

With those answers in place, the plugin adds a packages: write permission and appends the publish steps for both apps:

name: "CI"
# ...
jobs:
  spin:
    # ...
    permissions:
      contents: read
      packages: write
    steps:
      # ...
      - name: Authenticate against OCI registry for app-one
        run: |
          spin registry login -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} ghcr.io
      - name: Push OCI Artifact for app-one
        working-directory: ./app-one
        run: |
          spin registry push ghcr.io/thorstenhans/tmp-app-one:latest
      - name: Authenticate against OCI registry for app-two
        run: |
          spin registry login -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} ghcr.io
      - name: Push OCI Artifact for app-two
        working-directory: ./app-two
        run: |
          spin registry push ghcr.io/thorstenhans/tmp-app-two:latest

That’s it—every push to main now builds and publishes both apps to GitHub Container Registry.

Keyless OCI Artifact Signing with cosign

Publishing artifacts is great, but how do your consumers know an artifact really came from your pipeline and wasn’t tampered with? That’s what the --sign flag is for. It uses cosign to keylessly sign your OCI artifacts, leveraging the OIDC identity of your GitHub Actions workflow—no long-lived signing keys to manage or leak.

spin gh create-action --push-oci-artifacts --sign --overwrite

Note that --sign builds on top of OCI publishing. If you forget --push-oci-artifacts, the plugin will terminate and yell at you.

The interactive TUI is exactly the same as before. On top of the publish steps, the plugin now grants the workflow the id-token: write permission (required for keyless signing), installs cosign, additionally authenticates against the OCI-compliant registry using docker, and adds a signing step per artifact:


name: "CI"
# ...
jobs:
  spin:
    # ...
    permissions:
      contents: read
      packages: write
      id-token: write
    steps:
      - name: Install Cosign
        uses: sigstore/cosign-installer@v3
        # ...
      - name: Authenticate against OCI registry for app-one
        run: |
          spin registry login -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} ghcr.io
          docker login -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} ghcr.io
      - name: Push OCI Artifact for app-one
        # ...
      - name: Sign OCI Artifact for app-one
        working-directory: ./app-one
        run: |
          cosign sign --yes ghcr.io/thorstenhans/tmp-app-one:latest
      - name: Authenticate against OCI registry for app-two
        run: |
          spin registry login -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} ghcr.io
          docker login -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} ghcr.io
      - name: Push OCI Artifact for app-two
        # ...
      - name: Sign OCI Artifact for app-two
        working-directory: ./app-two
        run: |
          cosign sign --yes ghcr.io/thorstenhans/tmp-app-two:latest

SBOM Generation and Vulnerability Scanning with trivy and oras

The last new feature is the --sbom flag. It brings a complete software supply chain story to your workflow. It generates a Software Bill of Materials (SBOM), scans it for critical vulnerabilities, and attaches the SBOM right next to your OCI artifact so consumers can inspect it later.

To achieve this, two new tools are added to the GitHub Actions workflow file: trivy generates the SBOM (in SPDX JSON format) and scans it, and oras attaches the SBOM to the published artifact.

Just like --sign, the --sbom flag requires --push-oci-artifacts.

spin gh create-action --push-oci-artifacts --sign --sbom --overwrite

Again, the interactive UI is unchanged. Under the hood, the plugin installs the required tools, generates and scans the SBOM before pushing, and attaches it after:

name: "CI"
# ...
jobs:
  spin:
    # ...
    steps:
      - name: Install trivy
        uses: aquasecurity/setup-trivy@e07451d2e059ed86c2870430ea286b3a9e0bf241
      - name: Install ORAS
        uses: oras-project/setup-oras@v1
        # ...
      - name: Generate SBOM for app-one
        working-directory: ./app-one
        run: |
          trivy fs --format spdx-json -o sbom.spdx.json .
      - name: Scan app-one SBOM for Critical Vulnerabilities
        working-directory: ./app-one
        run: |
          trivy sbom --exit-code 1 --severity HIGH,CRITICAL sbom.spdx.json
      - name: Authenticate against OCI registry for app-one
        # ...
      - name: Push OCI Artifact for app-one
        # ...
      - name: Sign OCI Artifact for app-one
        # ...
      - name: Attach app-one SBOM via ORAS
        working-directory: ./app-one
        run: |
          oras attach --artifact-type application/spdx+json ghcr.io/thorstenhans/tmp-app-one:latest sbom.spdx.json

      - name: Generate SBOM for app-two
        working-directory: ./app-two
        run: |
          trivy fs --format spdx-json -o sbom.spdx.json .
      - name: Scan app-two SBOM for Critical Vulnerabilities
        working-directory: ./app-two
        run: |
          trivy sbom --exit-code 1 --severity HIGH,CRITICAL sbom.spdx.json
      - name: Authenticate against OCI registry for app-two
        # ...
      - name: Push OCI Artifact for app-two
        # ...
      - name: Sign OCI Artifact for app-two
        # ...
      - name: Attach app-two SBOM via ORAS
        working-directory: ./app-two
        run: |
          oras attach --artifact-type application/spdx+json ghcr.io/thorstenhans/tmp-app-two:latest sbom.spdx.json

Notice that the vulnerability scan uses --exit-code 1 --severity HIGH,CRITICAL. In other words, if trivy finds a HIGH or CRITICAL vulnerability, the workflow fails and nothing gets published. Exactly what you want as a safety net.

Deployment to Akamai Functions

Although this feature already landed in an earlier version of the gh plugin, it’s worth mentioning here as well. By appending the --deploy-to-akamai-functions flag, you can instruct the spin gh create-action command to scaffold all necessary steps for deploying your apps straight to an Akamai Functions account.

Conclusion

With --push-oci-artifacts, --sign, and --sbom, the gh plugin for Spin CLI now covers the full journey from source code to a published, signed, and documented OCI artifact—without having to hand-craft a single line of YAML. The interactive UI keeps the publishing configuration approachable, while the generated workflow follows supply chain best practices out of the box.

Happy shipping!