Guide

Automate in CI/CD

Generate and sign packs automatically in GitHub Actions, GitLab CI, or any CI system.

GitHub Actions

Build and sign a pack

.github/workflows/evidence.yml
name: Generate Evidence Pack

on:
  schedule:
    - cron: '0 0 * * 0'  # Weekly
  workflow_dispatch:

permissions:
  id-token: write      # Required for Sigstore
  contents: read

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

      - name: Install epack
        run: go install -tags components github.com/locktivity/epack/cmd/epack@v0.6.0

      - name: Build pack
        run: epack build evidence.pack ./artifacts/* --stream ${{ github.repository }}

      - name: Sign pack
        run: epack sign evidence.pack

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: evidence-pack
          path: evidence.pack

Run collectors

Use epack collect to run configured collectors:

.github/workflows/evidence.yml
name: Collect Evidence

on:
  schedule:
    - cron: '0 0 * * 0'
  workflow_dispatch:

permissions:
  id-token: write
  contents: read

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

      - name: Install epack (with collectors)
        run: go install -tags components github.com/locktivity/epack/cmd/epack@v0.6.0

      - name: Install locked collectors and tools
        run: epack sync --locked

      - name: Collect and build
        run: epack collect --frozen
        env:
          GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}

      - name: Sign pack
        run: epack sign packs/*.pack

      - uses: actions/upload-artifact@v4
        with:
          name: evidence-pack
          path: packs/*.pack
Why sync --locked then collect --frozen? sync --locked installs exactly the versions the committed lockfile pins, with full signature and digest verification, and fails on any drift instead of repairing it. collect --frozen then refuses to run anything the lockfile doesn't pin. Together they prevent supply chain attacks from modified or substituted collector binaries.

GitLab CI

.gitlab-ci.yml
evidence-pack:
  image: golang:1.21
  id_tokens:
    SIGSTORE_ID_TOKEN:
      aud: sigstore
  script:
    - go install -tags components github.com/locktivity/epack/cmd/epack@v0.6.0
    - epack build evidence.pack ./artifacts/* --stream $CI_PROJECT_PATH
    - EPACK_OIDC_TOKEN=$SIGSTORE_ID_TOKEN epack sign evidence.pack
  artifacts:
    paths:
      - evidence.pack
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
    - if: '$CI_PIPELINE_SOURCE == "web"'

Runners you manage

epack run runs a configuration from start to finish: it installs the locked components, collects, runs tools, signs, and pushes. On a runner that has no identity of its own, such as Jenkins, a cron host, or self-managed GitLab, the run signs in and signs its pack with a key.

On your machine, make a key for the runner. The remote may ask you to approve it in your browser:

epack key create --for northwind-production --out ./epack-signing-key.pem

Store the private key file where the runner keeps secrets. Then, in the configuration's folder on the runner, check the setup once and schedule the run:

export EPACK_SIGNING_KEY=/path/to/epack-signing-key.pem
export EPACK_PIPELINE_ID=<configuration-id>

# Sign in, verify the lock, secrets, credentials, and publishers; collect nothing
epack run --check

# The scheduled run
epack run --yes --key "$EPACK_SIGNING_KEY"

A folder made with epack remote clone names its configuration already, so EPACK_PIPELINE_ID is only needed when the folder came another way. When a platform issues its own identity, epack signs in with that first: a GitHub Actions OIDC token, then a GitLab ID token, then the key.

A job can also run a configuration by name with no folder. With EPACK_PIPELINE_ID set and the job's GitLab ID token or the runner's key, the run signs in for that configuration at the credential broker, fetches its current revision, and runs it. The remote's adapter is the one installed for the user, so the job must trust its publisher:

export EPACK_SIGNING_KEY=/path/to/epack-signing-key.pem
export EPACK_PIPELINE_ID=<configuration-id>
export EPACK_TRUSTED_PUBLISHERS=locktivity

# Fetch the current revision and run it
epack run northwind-production --yes --key "$EPACK_SIGNING_KEY"

When the runner lacks something the current revision needs, such as a variable, the run lists what is missing and where to add it, and runs the newest earlier revision that works without it. With none, the run stops before installing anything and reports the list to the remote.

Push to a remote

After signing, push to a configured remote:

# Add to your workflow after signing
- name: Push to remote
  run: |
    epack push myremote packs/*.pack -y \
      --label automated \
      --label "build-${{ github.run_number }}"

Verify packs in CI

Verify vendor packs as part of your supply chain checks:

.github/workflows/verify-vendor.yml
name: Verify Vendor

on:
  workflow_dispatch:
    inputs:
      pack_url:
        description: 'URL to vendor pack'
        required: true

jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - name: Download pack
        run: curl -L "${{ inputs.pack_url }}" -o vendor.pack

      - name: Install epack
        run: go install -tags components github.com/locktivity/epack/cmd/epack@v0.6.0

      - name: Verify signature
        run: |
          epack verify vendor.pack \
            --issuer "https://accounts.google.com" \
            --subject "security@vendor.com"

Environment variables

Variable Purpose
EPACK_OIDC_TOKEN OIDC token for keyless signing in CI systems that do not provide GitHub Actions ambient OIDC
EPACK_SIGNING_KEY Private key file a runner without its own identity signs in with. epack run --key sets it
EPACK_PIPELINE_ID The configuration a run belongs to, for a run that signs in with a key, or a job that runs a configuration by name with no folder
EPACK_CHECK=1 Make epack run a check that collects nothing, like --check
EPACK_TRUSTED_PUBLISHERS Comma-separated GitHub owners a fetched configuration may run components from, like --trust-publisher
GITHUB_TOKEN For the GitHub collector (manual token or brokered short-lived installation token)
NO_COLOR=1 Disable colored output

Tips

  • Pin versions: Use @v0.6.0 instead of @latest for reproducibility
  • Install with sync --locked: On clean runners, install exactly the reviewed lockfile and fail on any drift, including a pin that no longer satisfies its configured range
  • Run with --frozen: Always use in CI to fail if the lockfile is stale
  • Set EPACK_ERROR_FILE: Failed commands append stable code=/exit=/summary= lines, and a collector= line for each failed collector, that your workflow can read without scraping logs
  • Upload as artifacts: Store packs for traceability
  • Schedule regularly: Weekly or monthly keeps evidence current