Skip to content

GitHub Action

Listed on the GitHub Marketplace; the source is action.yml at the root of the repository.

This repository doubles as a GitHub Action. It downloads the pinned release binary for the runner (verifying the checksum), runs it, and uploads the documents as a workflow artifact; pixi itself is not needed, and neither is pixi install, since the lockfile is the only input:

- uses: actions/checkout@v4
- uses: millsks/pixi-sbom@v1
  with:
    all-environments: "true"
    fetch-licenses: "true"
    pypi-mapping: prefix
    primary-purl: pypi
    deny-license: "GPL-3.0-only AGPL-3.0-only"
    require-license: "true"

@v1 follows the newest 1.x.y release: each release moves the tag once its binaries are published, and the action installs the binary of the release the tag points at. For a build that never changes underneath you, pin an exact release (@v1.0.0) or a commit sha and let Dependabot bump it.

Without pixi: uv, Poetry, PDM, pylock and venvs

Nothing about the action needs pixi, so a project that does not use it needs nothing else on the runner. lockfile, scan, prefix and from-sbom say what to describe; give at most one (two at once fail the step).

- uses: actions/checkout@v4
# A uv project: the lockfile is found by the upward search, or named.
- uses: millsks/pixi-sbom@v1
  with:
    lockfile: uv.lock
    fetch-licenses: "true"
    vulnerabilities: osv
# What is actually installed: the venv this job built, checked against the lock.
- run: uv sync --frozen
- uses: millsks/pixi-sbom@v1
  with:
    prefix: .venv
    output: sbom-installed
    artifact-name: sbom-installed
    diff-against: uv.lock
    fail-on-diff: "true"

The second step documents the environment the job built and fails if it drifted from uv.lock (see Does this venv still match its lock?). CI runs both shapes, on a uv project and a venv, on a runner with no pixi installed.

Input Default Meaning
version the action's own tag (@v1: the newest 1.x.y), else the latest release pixi-sbom version to run
lockfile upward search The lockfile to describe: any kind pixi-sbom reads (pixi.lock, uv.lock, pylock.toml, poetry.lock, pdm.lock, conda-lock.yml, an explicit spec file). The action downloads its own binary, so pixi is not needed on the runner
format, spec-version, environment, platform, all-environments, all-platforms as the CLI Selection and format, see the options above
scan Describe every project under this directory, one document per directory with a lockfile of any supported kind, under output at the same relative path
prefix Describe an installed environment instead: a conda or pixi environment, a venv, or a Python installation
from-sbom Read an existing CycloneDX or SPDX document instead, and write it again with the other inputs applied
config Configuration file; the pyproject.toml table or pixi-sbom.toml next to the lockfile are read by default, none reads nothing. format is always passed and wins over the file; the other inputs are passed only when set
output sboms Output file (*.json, or - for the log) or directory; a single document lands in the directory as sbom.cdx.json / sbom.spdx.json
fetch-licenses, license-texts, embedded-sboms, pypi-mapping, primary-purl as the CLI Enrichment
allow-license, deny-license, require-license License policy; whitespace-separated lists
ignore-license Packages the policy does not apply to, one per line: PACKAGE or PACKAGE:justification
scorecard, scorecard-min, fail-on-scorecard false, 5, With fetch-licenses: record each repository's OpenSSF Scorecard, and fail the step (exit code 9) below the given score
fail-on-yanked false With fetch-licenses, fail the step on a yanked release (exit code 7)
fail-on-policy true Fail the step on a policy violation; with false it becomes a warning and the policy-violated output is true
vulnerabilities, kev osv looks findings up and records them; kev: "true" marks the known-exploited ones
fail-on-severity, fail-on-kev The vulnerability gate (exit code 4)
ignore-vuln Accepted findings, one per line: ID, ID:text or ID:state[:justification][:response,...][:text], as for --ignore-vuln
fail-on-vulnerabilities true Fail the step when the gate trips; with false it becomes a warning and the vulnerabilities-found output is true
vex, vex-open Also write a standalone CycloneDX VEX to that path, and the state it gives findings nobody assessed (in-triage, exploitable). Needs format: cyclonedx.
upload-sarif, sarif-category false, pixi-sbom Write the findings as SARIF and upload them to GitHub code scanning (see below)
diff-against Also compare the environment with the document at this path and put the comparison in the job summary; the file has to be there already (the action fetches nothing)
fail-on-diff With diff-against: true fails the step (exit code 6) on any change, or name the sections — added removed version license
attest, attest-subject false, Sign the documents with a GitHub artifact attestation (see below)
extra-args Any other CLI arguments
upload-artifact, artifact-name true, sboms Artifact upload

Outputs: version, output, document (the file, in single-document mode), policy-violated, vulnerabilities-found, diff-changed, sarif, attestation-url. The action runs on Linux (x64, arm64), macOS (Intel, Apple Silicon) and Windows runners, and describes any platform in the lockfile regardless of the runner (platform: linux-64 on a macOS runner is fine).

Without the action

The binary has no runtime dependencies, so any job can download it from the releases page and run it, or install it with pixi:

- uses: prefix-dev/setup-pixi@v0.8.1
  with:
    run-install: false
- name: Generate SBOMs
  run: |
    pixi global install pixi-sbom
    pixi sbom --all-environments -p linux-64 --output sboms/
- uses: actions/upload-artifact@v4
  with:
    name: sboms
    path: sboms/

Vulnerabilities in the Security tab

With vulnerabilities: osv the document carries the findings; upload-sarif: "true" additionally renders them as SARIF (--report vulnerabilities --report-format sarif, one result per finding and affected package, located at the lockfile, security-severity from the CVSS score or 10 for known-exploited findings, accepted findings as suppressions) and uploads the file with github/codeql-action/upload-sarif, so they appear under Security → Code scanning with the sarif-category you choose. The job needs security-events: write:

permissions:
  contents: read
  security-events: write
steps:
  - uses: actions/checkout@v4
  - uses: millsks/pixi-sbom@v1
    with:
      pypi-mapping: prefix
      vulnerabilities: osv
      kev: "true"
      fail-on-severity: high
      fail-on-kev: "true"
      ignore-vuln: |
        GHSA-2xpw-w6gg-jr37:streaming API is not used
        CVE-2023-43804:false_positive:only reachable through a removed code path
      upload-sarif: "true"

Each document is a SARIF run with its own automation id, so code scanning files the alerts under pixi-sbom/<environment> (or pixi-sbom/<environment>/<platform> in batch mode) rather than under sarif-category. The SARIF report reuses the lookup's cache, so the second run costs no network requests. Outside the action, the same file comes from pixi sbom --vulnerabilities osv --report vulnerabilities --report-format sarif > findings.sarif.

Signed SBOMs

An SBOM published from CI is only as trustworthy as its provenance. attest: "true" signs the documents with a GitHub artifact attestation (Sigstore, bound to the workflow run, listed on the repository's Attestations page) and needs id-token: write and attestations: write:

  • with attest-subject, the path of the artifact the SBOM describes (a wheel, a container image tarball, an archive), the SBOM is attached to that artifact as an SBOM attestation through actions/attest-sbom, single- document mode only;
  • without it, the documents themselves get a build provenance attestation through actions/attest-build-provenance (every *.json in the output directory in batch mode).
permissions:
  contents: read
  id-token: write
  attestations: write
steps:
  - uses: actions/checkout@v4
  - uses: millsks/pixi-sbom@v1
    with:
      attest: "true"

Anyone can then verify a downloaded document against the repository:

gh attestation verify sbom.cdx.json --repo <owner>/<repo>

Pull requests from forks have no id-token, so attest on pushes only (attest: ${{ github.event_name == 'push' && 'true' || 'false' }}), as this repository's own workflow does.

A worked example

This repository dogfoods its own action in .github/workflows/ci.yml: every environment of its lockfile is described with licenses fetched, PyPI identities from the conda-forge mapping and embedded SBOMs attached, a deny list that is known to trip (Python-2.0) proves the policy-violated output with fail-on-policy: "false", and a second step with a satisfiable policy proves exit 0:

- uses: millsks/pixi-sbom@v1
  id: sbom
  with:
    all-environments: "true"
    fetch-licenses: "true"
    pypi-mapping: prefix
    embedded-sboms: "true"
    deny-license: "Python-2.0"
    require-license: "true"
    fail-on-policy: "false"
- run: test "${{ steps.sbom.outputs.policy-violated }}" = "true"

Every input maps to a command-line option of the same name; the action adds nothing the binary cannot do.