name: Release and Publish Rust Crate
on:
workflow_dispatch:
permissions: {}
env:
CARGO_TERM_COLOR: always
jobs:
version:
name: Derive and validate version
runs-on: ubuntu-24.04
permissions:
contents: read
outputs:
version: ${{ steps.version.outputs.file_version }}
tag: ${{ steps.version.outputs.tag }}
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
with:
persist-credentials: false
- name: Install pinned Rust toolchain
uses: ./.github/actions/rust-toolchain
- name: Derive version and fail if it already exists
id: version
shell: bash
run: |
set -Eeuo pipefail
FILE_VERSION=$(awk -F '"' '/^version[[:space:]]*=/ {print $2; exit}' Cargo.toml)
if [[ -z "${FILE_VERSION:-}" ]]; then
echo "::error::version not found in Cargo.toml"; exit 1
fi
TAG="v${FILE_VERSION}"
if git ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/dev/null 2>&1; then
echo "::error::tag ${TAG} already exists"; exit 1
fi
echo "file_version=${FILE_VERSION}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
- name: Validate crate packaging (dry run)
run: cargo publish --dry-run --locked
build-artifacts:
name: Build artifacts (${{ matrix.target }})
needs: version
strategy:
matrix:
include:
- target: x86_64-unknown-linux-gnu
runner: ubuntu-24.04
- target: aarch64-unknown-linux-gnu
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
permissions:
contents: read
id-token: write
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
with:
persist-credentials: false
- name: Install pinned Rust toolchain
uses: ./.github/actions/rust-toolchain
with:
targets: ${{ matrix.target }}
- name: Build (${{ matrix.target }})
shell: bash
run: |
set -euxo pipefail
mkdir -p dist
./scripts/build-release.sh "${{ matrix.target }}"
cp "target/${{ matrix.target }}/release/cdi" dist/
cp "target/${{ matrix.target }}/release/validate" dist/
cp "target/${{ matrix.target }}/release/libcontainer_device_interface.so" dist/
- name: Package release tarball (reproducible)
shell: bash
run: |
set -euxo pipefail
SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)
cd dist
BASE="container-device-interface-${{ matrix.target }}"
# Create reproducible tarball with stable mtimes/owners
tar --sort=name --owner=0 --group=0 --numeric-owner --mtime="@${SOURCE_DATE_EPOCH}" \
-cf "${BASE}.tar" \
cdi validate libcontainer_device_interface.so
xz -T0 -9e -f "${BASE}.tar"
- name: Compute digest (sha256sum format)
shell: bash
run: |
cd dist
# "<64-hex><space><space><filename>" - consumed by the provenance job
sha256sum "container-device-interface-${{ matrix.target }}.tar.xz" \
| tee "container-device-interface-${{ matrix.target }}.tar.xz.sha256"
- name: Install cosign
uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac
- name: Keyless sign tarball
env:
COSIGN_EXPERIMENTAL: "true"
run: |
# Cosign signing outputs: *.sig, *.cert, *.bundle.json (Rekor bundle)
TARXZ="dist/container-device-interface-${{ matrix.target }}.tar.xz"
cosign sign-blob --yes "$TARXZ" \
--output-signature "${TARXZ}.sig" \
--output-certificate "${TARXZ}.cert" \
--bundle "${TARXZ}.bundle.json"
- name: Upload dist as artifact (per target)
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with:
name: dist-${{ matrix.target }}
path: |
dist/*.tar.xz
dist/*.tar.xz.*
if-no-files-found: error
overwrite: true
sbom:
name: Generate and sign SBOM
needs: version
runs-on: ubuntu-24.04
permissions:
contents: read
id-token: write
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
with:
persist-credentials: false
- name: Create dist directory
run: mkdir -p dist
- name: Generate SBOM (SPDX JSON)
uses: anchore/sbom-action@f8bdd1d8ac5e901a77a92f111440fdb1b593736b with:
path: .
format: spdx-json
output-file: dist/sbom-container-device-interface.spdx.json
upload-artifact: false
- name: Install cosign
uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac
- name: Keyless sign SBOM
env:
COSIGN_EXPERIMENTAL: "true"
run: |
SBOM="dist/sbom-container-device-interface.spdx.json"
cosign sign-blob --yes "$SBOM" \
--output-signature "${SBOM}.sig" \
--output-certificate "${SBOM}.cert" \
--bundle "${SBOM}.bundle.json"
- name: Upload SBOM as artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with:
name: dist-sbom
path: dist/*
if-no-files-found: error
overwrite: true
create-release:
name: Create draft GitHub Release
needs: [version, build-artifacts, sbom]
runs-on: ubuntu-24.04
permissions:
contents: write actions: read steps:
- name: Download all dist artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with:
pattern: dist-*
path: dist
merge-multiple: true
- name: Create draft GitHub Release and upload assets
uses: softprops/action-gh-release@aec2ec56f94eb8180ceec724245f64ef008b89f5 with:
tag_name: ${{ needs.version.outputs.tag }}
target_commitish: ${{ github.sha }}
name: ${{ needs.version.outputs.tag }}
draft: true
body: |
Release ${{ needs.version.outputs.version }}.
Crate on crates.io: https://crates.io/crates/container-device-interface/${{ needs.version.outputs.version }}
files: |
dist/container-device-interface-x86_64-unknown-linux-gnu.tar.xz
dist/container-device-interface-x86_64-unknown-linux-gnu.tar.xz.sig
dist/container-device-interface-x86_64-unknown-linux-gnu.tar.xz.cert
dist/container-device-interface-x86_64-unknown-linux-gnu.tar.xz.bundle.json
dist/container-device-interface-aarch64-unknown-linux-gnu.tar.xz
dist/container-device-interface-aarch64-unknown-linux-gnu.tar.xz.sig
dist/container-device-interface-aarch64-unknown-linux-gnu.tar.xz.cert
dist/container-device-interface-aarch64-unknown-linux-gnu.tar.xz.bundle.json
dist/sbom-container-device-interface.spdx.json
dist/sbom-container-device-interface.spdx.json.sig
dist/sbom-container-device-interface.spdx.json.cert
dist/sbom-container-device-interface.spdx.json.bundle.json
provenance-hash-all:
name: Collect provenance subjects
needs: build-artifacts
runs-on: ubuntu-24.04
permissions:
actions: read outputs:
map: ${{ steps.mkmap.outputs.map }}
steps:
- name: Download all dist artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with:
pattern: dist-*
path: dist
merge-multiple: true
- name: Build base64 map from *.sha256
id: mkmap
shell: bash
run: |
set -euo pipefail
cd dist
# Build JSON map: { "<tarball>": "<base64(sha256sum line)>", ... }
echo '{}' > map.json
for f in *.sha256; do
[ -f "$f" ] || continue
key="${f%.sha256}"
b64=$(base64 -w0 "$f")
jq --arg k "$key" --arg v "$b64" '. + {($k): $v}' map.json > tmp.json && mv tmp.json map.json
done
echo "map=$(jq -c . map.json)" >> "$GITHUB_OUTPUT"
provenance:
needs: provenance-hash-all
strategy:
matrix:
target:
- x86_64-unknown-linux-gnu
- aarch64-unknown-linux-gnu
permissions:
actions: read
id-token: write
contents: write
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 with:
base64-subjects: ${{ fromJSON(needs.provenance-hash-all.outputs.map)[format('container-device-interface-{0}.tar.xz', matrix.target)] }}
upload-assets: false
provenance-publish:
name: Attach provenance to draft Release
needs: [version, provenance, create-release]
runs-on: ubuntu-24.04
permissions:
contents: write actions: read strategy:
matrix:
target:
- x86_64-unknown-linux-gnu
- aarch64-unknown-linux-gnu
steps:
- name: Download provenance artifact for this target
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with:
name: container-device-interface-${{ matrix.target }}.tar.xz.intoto.jsonl
path: prov
- name: Upload provenance to draft Release
uses: softprops/action-gh-release@aec2ec56f94eb8180ceec724245f64ef008b89f5 with:
tag_name: ${{ needs.version.outputs.tag }}
target_commitish: ${{ github.sha }}
draft: true
files: prov/container-device-interface-${{ matrix.target }}.tar.xz.intoto.jsonl
publish-release:
name: Verify assets and publish Release
needs: [version, provenance-publish]
runs-on: ubuntu-24.04
permissions:
contents: write steps:
- name: Verify draft release has all required assets
id: draft
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ needs.version.outputs.tag }}
run: |
set -euo pipefail
# Drafts are not addressable by tag (the tag does not exist yet);
# look the release up by tag_name and address it by id.
RELEASE=$(gh api "repos/${{ github.repository }}/releases" \
--jq "[.[] | select(.tag_name==\"${TAG}\" and .draft)][0]")
if [[ -z "$RELEASE" || "$RELEASE" == "null" ]]; then
echo "::error::no draft release found for ${TAG}"; exit 1
fi
RELEASE_ID=$(jq -r '.id' <<<"$RELEASE")
echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT"
ASSETS=$(jq -r '.assets[].name' <<<"$RELEASE" | sort)
# Define expected assets
EXPECTED=(
"container-device-interface-x86_64-unknown-linux-gnu.tar.xz"
"container-device-interface-x86_64-unknown-linux-gnu.tar.xz.sig"
"container-device-interface-x86_64-unknown-linux-gnu.tar.xz.cert"
"container-device-interface-x86_64-unknown-linux-gnu.tar.xz.bundle.json"
"container-device-interface-x86_64-unknown-linux-gnu.tar.xz.intoto.jsonl"
"container-device-interface-aarch64-unknown-linux-gnu.tar.xz"
"container-device-interface-aarch64-unknown-linux-gnu.tar.xz.sig"
"container-device-interface-aarch64-unknown-linux-gnu.tar.xz.cert"
"container-device-interface-aarch64-unknown-linux-gnu.tar.xz.bundle.json"
"container-device-interface-aarch64-unknown-linux-gnu.tar.xz.intoto.jsonl"
"sbom-container-device-interface.spdx.json"
"sbom-container-device-interface.spdx.json.sig"
"sbom-container-device-interface.spdx.json.cert"
"sbom-container-device-interface.spdx.json.bundle.json"
)
MISSING=()
for asset in "${EXPECTED[@]}"; do
if ! echo "$ASSETS" | grep -qx "$asset"; then
MISSING+=("$asset")
fi
done
if [[ ${#MISSING[@]} -gt 0 ]]; then
echo "ERROR: Draft release is missing required assets:" >&2
printf ' - %s\n' "${MISSING[@]}" >&2
exit 1
fi
echo "All ${#EXPECTED[@]} required assets present in draft release"
- name: Publish draft release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_ID: ${{ steps.draft.outputs.release_id }}
run: |
gh api -X PATCH "repos/${{ github.repository }}/releases/${RELEASE_ID}" \
-F draft=false
publish-crate:
name: Publish to crates.io (Trusted Publishing)
needs: [version, publish-release]
runs-on: ubuntu-24.04
environment: release
permissions:
contents: read
id-token: write steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
with:
persist-credentials: false
- name: Install pinned Rust toolchain
uses: ./.github/actions/rust-toolchain
- name: Authenticate to crates.io (OIDC)
id: auth
uses: rust-lang/crates-io-auth-action@e919bc7605cde86df457cf5b93c5e103838bd879
- name: Publish
run: cargo publish --locked
env:
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
verify-release:
name: Verify signatures and provenance
needs: [version, publish-crate]
runs-on: ubuntu-24.04
permissions:
contents: read steps:
- name: Install Cosign
uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac
- name: Install SLSA verifier
uses: slsa-framework/slsa-verifier/actions/installer@ea584f4502babc6f60d9bc799dbbb13c1caa9ee6
- name: Download release assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ needs.version.outputs.tag }}
run: |
gh release download "${TAG}" --repo "${{ github.repository }}" --dir .
- name: Verify tarballs, SBOM signatures, Rekor, and SLSA provenance
env:
WORKFLOW_REF: ${{ github.workflow_ref }}
WORKFLOW_NAME: ${{ github.workflow }}
GH_REF: ${{ github.ref }}
run: |
set -euxo pipefail
# Reusable Cosign identity pins (GitHub Actions keyless cert constraints)
COSIGN_PINS=(
--certificate-identity "https://github.com/${WORKFLOW_REF}"
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
--certificate-github-workflow-repository "${{ github.repository }}"
--certificate-github-workflow-ref "${GH_REF}"
--certificate-github-workflow-sha "${{ github.sha }}"
--certificate-github-workflow-name "${WORKFLOW_NAME}"
--certificate-github-workflow-trigger "${{ github.event_name }}"
)
for target in x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu; do
TARXZ="container-device-interface-${target}.tar.xz"
# 1) ONLINE verification via Rekor transparency log
cosign verify-blob \
--rekor-url https://rekor.sigstore.dev \
"${COSIGN_PINS[@]}" \
--certificate "${TARXZ}.cert" \
--signature "${TARXZ}.sig" \
"${TARXZ}"
# 2) OFFLINE verification using the Sigstore bundle (no network)
cosign verify-blob \
--bundle "${TARXZ}.bundle.json" \
"${COSIGN_PINS[@]}" \
"${TARXZ}"
# 3) Verify SLSA provenance (built from main)
slsa-verifier verify-artifact "${TARXZ}" \
--provenance-path "${TARXZ}.intoto.jsonl" \
--source-uri "github.com/${{ github.repository }}" \
--source-branch "main"
done
# SBOM signature - online and offline
SBOM="sbom-container-device-interface.spdx.json"
cosign verify-blob \
--rekor-url https://rekor.sigstore.dev \
"${COSIGN_PINS[@]}" \
--certificate "${SBOM}.cert" \
--signature "${SBOM}.sig" \
"${SBOM}"
cosign verify-blob \
--bundle "${SBOM}.bundle.json" \
"${COSIGN_PINS[@]}" \
"${SBOM}"