Skip to main content
View Markdown ↗

Copy this page

Select and copy the Markdown below, then paste it into your LLM.

On this page

GitHub Actions: from build to verified release

This tutorial walks through a full attested CI pipeline using aflock-ai/cilock-action, six steps (lint, SAST, test, build, SBOM, docker-build) each producing signed in-toto attestations via OIDC. The pattern below follows TestifySec's reference implementation at github.com/testifysec/dropbox-clone.

What you'll build

A pipeline where every step is wrapped by cilock-action and produces a signed attestation. All signing is keyless (Fulcio + GitHub OIDC), all attestations are timestamped by the platform's TSA (<platform-url>/api/v1/timestamp), and all evidence is uploaded to Archivista using OIDC for auth, no static API keys.

Prerequisites

  • A GitHub repo (this tutorial assumes a Go project, but any language works)

  • Permission to add workflow files

  • The evidence destination your administrator approved. Every step below signs with the platform's Fulcio and uploads its evidence to the platform's Archivista, both derived from platform-url. Before you run it, get from your administrator:

    • the exact https:// platform origin: the hosted platform, or your private or self-hosted deployment;
    • the tenant, and the product this repository's evidence belongs to, already set up there;
    • confirmation that this repository is allowed to sign and upload to it.

    Store the origin as the repository variable CILOCK_PLATFORM_URL. The workflow refuses to run without it. If you omit platform-url entirely, cilock-action does not stop: it defaults to the hosted platform, https://platform.testifysec.com, and uploads there (enable-archivista defaults to true). Name the destination on purpose instead of relying on that default.

Step 1: Set the right permissions

Cilock-action needs id-token: write to request the OIDC token used by Fulcio (signing) and Archivista (upload). It needs contents: read for checkout. Nothing else.

permissions:
  id-token: write
  contents: read

This is the same minimum set TestifySec uses in cilock-action-oidc.yaml.

Step 2: The six-step attested pipeline

name: cilock-action OIDC attestations

on:
  workflow_dispatch:
  push:
    branches: [main]

permissions:
  id-token: write
  contents: read

jobs:
  attest:
    name: Attested CI Pipeline
    runs-on: ubuntu-latest
    steps:
      # 0. Declare the destination before anything is uploaded. Fails the job
      #    when the repository variable is unset or not an https origin, and
      #    prints where this run's evidence will go.
      - name: Check the evidence destination
        env:
          PLATFORM_URL: ${{ vars.CILOCK_PLATFORM_URL }}
        run: |
          case "$PLATFORM_URL" in
            https://*) ;;
            *) echo "::error::Set the CILOCK_PLATFORM_URL repository variable to your administrator-approved https platform origin"; exit 1 ;;
          esac
          echo "Evidence for this run is signed and uploaded to: $PLATFORM_URL"

      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

      - name: Set up Go
        uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
        with:
          # syft v1.54.1 needs Go 1.26.8 or later to install.
          go-version: "1.26"

      # Pinned tool versions, installed through the Go module proxy and
      # checksum database rather than a script fetched from a moving branch.
      - name: Install syft
        run: go install github.com/anchore/syft/cmd/[email protected]

      - name: Install gosec
        run: go install github.com/securego/gosec/v2/cmd/[email protected]

      # 1. Lint + secret scan
      - name: lint + secrets
        uses: aflock-ai/cilock-action@d39bb9b17e392520f9e7eef17cf355c027a5c283 # v1.0.4
        with:
          step: lint
          command: echo "lint passed"
          attestations: environment git github secretscan
          platform-url: ${{ vars.CILOCK_PLATFORM_URL }}

      # 2. SAST: gosec writes SARIF, captured as a product. -no-fail keeps
      #    cilock's command-run attestor green when gosec reports findings;
      #    the policy gate is the Rego over the captured SARIF, not the
      #    tool's exit code.
      - name: sast
        uses: aflock-ai/cilock-action@d39bb9b17e392520f9e7eef17cf355c027a5c283 # v1.0.4
        with:
          step: sast
          command: gosec -no-fail -fmt=sarif -out=gosec-results.sarif ./...
          attestations: environment git github sarif
          platform-url: ${{ vars.CILOCK_PLATFORM_URL }}
          product-include-glob: "*.sarif"

      # 3. Tests
      - name: test
        uses: aflock-ai/cilock-action@d39bb9b17e392520f9e7eef17cf355c027a5c283 # v1.0.4
        with:
          step: test
          command: go test -count=1 ./...
          attestations: environment git github
          platform-url: ${{ vars.CILOCK_PLATFORM_URL }}

      # 4. Build the binary — cilock observes the compiler's exact argv
      #    via command-run; bin/myapp lands in product/v0.3 as a Merkle
      #    leaf. This is the artifact the SBOM step (next) will scan.
      - name: build
        uses: aflock-ai/cilock-action@d39bb9b17e392520f9e7eef17cf355c027a5c283 # v1.0.4
        env:
          CGO_ENABLED: "0"
        with:
          step: build
          command: go build -o bin/myapp ./cmd/myapp
          attestations: environment git github
          platform-url: ${{ vars.CILOCK_PLATFORM_URL }}
          product-include-glob: "bin/*"

      # 5. SBOM the build's output — separate cilock step so syft's argv
      #    is its own command-run, the build artifact is recorded as
      #    material/v0.3 (because cilock hashes the working tree before
      #    syft runs), and the SBOM lands in product/v0.3. A release-gate
      #    Rego policy can then check that the SBOM's targeted file digest
      #    matches the build step's product.
      - name: sbom
        uses: aflock-ai/cilock-action@d39bb9b17e392520f9e7eef17cf355c027a5c283 # v1.0.4
        with:
          step: sbom
          command: syft bin/myapp -o cyclonedx-json=bin/bom.cdx.json
          attestations: environment git github sbom
          platform-url: ${{ vars.CILOCK_PLATFORM_URL }}
          product-include-glob: "bin/*"

      # 6. Container build
      - name: docker-build
        uses: aflock-ai/cilock-action@d39bb9b17e392520f9e7eef17cf355c027a5c283 # v1.0.4
        with:
          step: docker-build
          command: docker buildx build --metadata-file docker-metadata.json -t myapp:test --load .
          attestations: environment git github docker
          platform-url: ${{ vars.CILOCK_PLATFORM_URL }}
          product-include-glob: "docker-metadata.json"

Every uses: is pinned to a full commit SHA, with the release tag in a comment, and each tool is installed at a fixed version. A tag or @latest can be moved after you review the workflow; a commit SHA cannot.

Why each step uses the attestor mix it does

StepExtra attestorWhy
lintsecretscanCheap to run on a no-op command, catches credentials accidentally echoed during real lint output.
sastsarifThe output is a SARIF file; the SARIF attestor parses it into structured findings inside the attestation.
test(none)Test runs primarily need command-run + git + CI context.
build(none)command-run records the compiler's argv, and bin/myapp lands in the product attestation.
sbomsbomA separate step, so syft's argv is its own command-run and the build output is recorded as a material before syft reads it.
docker-builddockerThe docker attestor parses the buildx metadata file and records image digests.

environment, git, and github are passed to every step, this gives you the source commit, runner identity, and CI context on every attestation, so verification policy can match identity claims per step.

What gets produced

Each step produces a signed DSSE envelope containing an in-toto Collection. With enable-archivista: true (the cilock-action default), each envelope is also pushed to the Archivista derived from platform-url (the origin the preflight step printed) using a fresh OIDC token. The attestation_file and git_oid action outputs let downstream steps reference the evidence:

- name: docker-build
  id: docker
  uses: aflock-ai/cilock-action@d39bb9b17e392520f9e7eef17cf355c027a5c283 # v1.0.4
  with:
    step: docker-build
    # ...

- name: Print evidence GitOID
  env:
    GIT_OID: ${{ steps.docker.outputs.git_oid }}
  run: echo "Evidence stored at $GIT_OID"

Adding a verification gate

To make this enforce policy (not just observe), add a separate job that runs cilock verify against a signed policy after all attested steps complete. See Verify in a release gate for the gate pattern.

Going further

  • Need the raw CLI? TestifySec's test-staging-cilock.yaml shows a similar pipeline using cilock run directly instead of the action, useful for understanding what the action does under the hood. It is a development recipe: it uploads to a TestifySec staging environment. Read it for the commands; do not copy its destination into a production workflow.
  • Two-pipeline architecture. The repo also splits CI (ci.yaml, PR-triggered, contents: read) from CD (deploy.yaml, push-triggered, full AWS credentials via OIDC federation). This is the "two pipelines, two trust boundaries" pattern; CI/lock is the proof that the boundary holds.
  • Defending against real attacks. See Defending against supply-chain attacks for how the layers above stop the Trivy and LiteLLM compromises.

Reference generated from the product documentation. Match commands and support details to your installed release.