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
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:
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
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
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:
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.0instead of@latestfor 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 stablecode=/exit=/summary=lines, and acollector=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
Related
- Run collectors โ Configure and run collectors locally
- Push packs to remotes โ Configure remotes
- CLI Reference: collect โ All flags