CD — digipin-rs #43
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Publish / CD for digipin-rs (library) | |
| # - Keyless cosign via OIDC (id-token: write) | |
| # - Verified GitHub actions only (actions/*, github/*, sigstore/cosign-action); Syft pinned/verified | |
| # - Produces dual SBOMs (CycloneDX + SPDX), validates SBOM, creates GitHub SLSA attestation, | |
| # cosign attestation/sign (keyless via sigstore action), uploads release assets, then publishes to crates.io | |
| # | |
| # Required repository secrets / variables (set these before using the workflow): | |
| # - CARGO_REGISTRY_TOKEN : (store as an Environment secret in 'production') crates.io API token | |
| # - SYFT_VERSION : pinned Syft version (e.g. "0.71.0") | |
| # - SYFT_SHA256 : sha256 of the syft tarball you will download | |
| # - TEST_MODE : set "true" for test/draft-only | |
| name: CD — digipin-rs | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: publish-digipin-rs-${{ github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| publish: | |
| name: Publish digipin-rs | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| packages: write | |
| id-token: write | |
| attestations: write | |
| environment: production | |
| env: | |
| TEST_MODE: ${{ vars.TEST_MODE }} | |
| SYFT_VERSION: ${{ vars.SYFT_VERSION }} | |
| SYFT_SHA256: ${{ vars.SYFT_SHA256 }} | |
| defaults: | |
| run: | |
| shell: bash | |
| steps: | |
| - name: Checkout digipin-rs | |
| uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 | |
| with: | |
| fetch-depth: 0 | |
| - name: Inspect checkout (debug) | |
| run: | | |
| echo "GITHUB_REF=${GITHUB_REF}" | |
| echo "GITHUB_REF_NAME=${GITHUB_REF_NAME:-${GITHUB_REF##*/}}" | |
| echo "GITHUB_SHA=${GITHUB_SHA}" | |
| ls -la | |
| - name: Set derived variables | |
| id: set_vars | |
| run: | | |
| set -euo pipefail | |
| TAG="${GITHUB_REF_NAME:-${GITHUB_REF##*/}}" | |
| SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-8) | |
| RUN_ID="${GITHUB_RUN_NUMBER:-0}" | |
| echo "TAG=${TAG}" >> $GITHUB_ENV | |
| echo "SHORT_SHA=${SHORT_SHA}" >> $GITHUB_ENV | |
| echo "RUN_ID=${RUN_ID}" >> $GITHUB_ENV | |
| echo "GIT_OWNER=${GITHUB_REPOSITORY_OWNER}" >> $GITHUB_ENV | |
| - name: Validate tag matches Cargo.toml version | |
| id: check_tag_version | |
| run: | | |
| set -euo pipefail | |
| TAG="${TAG:-${GITHUB_REF_NAME:-${GITHUB_REF##*/}}}" | |
| TAG_NORMAL="${TAG#refs/tags/}" | |
| CRATE_VERSION=$(sed -n 's/^version *= *"\(.*\)".*/\1/p' Cargo.toml | head -n1 || true) | |
| if [ -z "$CRATE_VERSION" ]; then | |
| echo "ERROR: Could not find version in Cargo.toml" | |
| exit 1 | |
| fi | |
| if [ "v${CRATE_VERSION}" != "$TAG_NORMAL" ] && [ "${CRATE_VERSION}" != "$TAG_NORMAL" ]; then | |
| echo "ERROR: Tag ($TAG_NORMAL) does not match Cargo.toml version ($CRATE_VERSION)" | |
| exit 2 | |
| fi | |
| echo "Tag matches Cargo.toml version: $CRATE_VERSION" | |
| echo "CRATE_VERSION=${CRATE_VERSION}" >> $GITHUB_ENV | |
| - name: Install Rust toolchain | |
| run: | | |
| set -euo pipefail | |
| if ! command -v rustup >/dev/null 2>&1; then | |
| curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y | |
| source "$HOME/.cargo/env" | |
| fi | |
| rustup toolchain install stable --profile minimal || true | |
| rustup default stable | |
| rustc --version | |
| cargo --version | |
| - name: Cache cargo registry & build | |
| uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| target | |
| key: ${{ runner.os }}-digipin-rs-cd-${{ hashFiles('Cargo.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-digipin-rs-cd- | |
| - name: Produce .crate (cargo package) | |
| run: | | |
| set -euo pipefail | |
| echo "Packaging crate (Cargo.toml version ${CRATE_VERSION})..." | |
| cargo package | |
| mkdir -p artifacts | |
| CRATE_PATH=$(ls target/package/*.crate | head -n1) | |
| if [ -z "$CRATE_PATH" ]; then | |
| echo "ERROR: cargo package did not produce a .crate file" | |
| exit 1 | |
| fi | |
| echo "CRATE_PATH=${CRATE_PATH}" >> $GITHUB_ENV | |
| CRATE_FILE=$(basename "$CRATE_PATH") | |
| echo "CRATE_FILE=${CRATE_FILE}" >> $GITHUB_ENV | |
| cp "$CRATE_PATH" artifacts/ | |
| echo "Copied crate to artifacts/" | |
| echo "Listing artifacts/:" | |
| ls -la artifacts | |
| - name: Create deterministic checksum for artifact | |
| run: | | |
| set -euo pipefail | |
| mkdir -p artifacts | |
| CRATE_FILE="${CRATE_FILE}" | |
| sha256sum "artifacts/${CRATE_FILE}" | awk '{print $1 " " $2}' > "artifacts/${CRATE_FILE}.sha256" | |
| echo "Checksum written to artifacts/${CRATE_FILE}.sha256" | |
| - name: Install pinned Syft and verify checksum | |
| id: install_syft | |
| run: | | |
| set -euo pipefail | |
| if [ -z "${SYFT_VERSION:-}" ] || [ -z "${SYFT_SHA256:-}" ]; then | |
| echo "SYFT_VERSION and SYFT_SHA256 must be set in production environment as environment variables" | |
| exit 1 | |
| fi | |
| ARCH=$(uname -m) | |
| if [ "$ARCH" = "x86_64" ]; then TARCH="linux_amd64"; else TARCH="$ARCH"; fi | |
| SYFT_TGZ_URL="https://github.com/anchore/syft/releases/download/v${SYFT_VERSION}/syft_${SYFT_VERSION}_${TARCH}.tar.gz" | |
| echo "Downloading Syft from $SYFT_TGZ_URL" | |
| curl --proto '=https' --tlsv1.2 -fsSL --retry 3 "$SYFT_TGZ_URL" -o /tmp/syft.tar.gz | |
| DOWN_SHA=$(sha256sum /tmp/syft.tar.gz | awk '{print $1}') | |
| if [ "$DOWN_SHA" != "$SYFT_SHA256" ]; then | |
| echo "Syft checksum mismatch. expected=${SYFT_SHA256}, got=${DOWN_SHA}" | |
| exit 2 | |
| fi | |
| sudo mkdir -p /usr/local/bin | |
| sudo tar -xzf /tmp/syft.tar.gz -C /usr/local/bin | |
| /usr/local/bin/syft --version | |
| - name: Generate dual SBOMs (CycloneDX + SPDX) for the .crate | |
| run: | | |
| set -euo pipefail | |
| mkdir -p sbom | |
| CRATE_FILE="${CRATE_FILE}" | |
| /usr/local/bin/syft "artifacts/${CRATE_FILE}" -o cyclonedx-json > sbom/sbom-cyclonedx.json | |
| /usr/local/bin/syft "artifacts/${CRATE_FILE}" -o spdx-json > sbom/sbom-spdx.json | |
| echo "Generated SBOMs:" | |
| ls -la sbom || true | |
| cat sbom/sbom-cyclonedx.json | |
| cat sbom/sbom-spdx.json | |
| test -s sbom/sbom-cyclonedx.json | |
| test -s sbom/sbom-spdx.json | |
| - name: Attest provenance for crate | |
| uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0 | |
| with: | |
| subject-path: | | |
| artifacts/${{ env.CRATE_FILE }} | |
| artifacts/${{ env.CRATE_FILE }}.sha256 | |
| - name: Attest provenance for SBOMs | |
| uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0 | |
| with: | |
| subject-path: | | |
| sbom/sbom-cyclonedx.json | |
| sbom/sbom-spdx.json | |
| - name: Check if GitHub release already exists | |
| id: check_release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| if gh release view "$TAG" >/dev/null 2>&1; then | |
| echo "RELEASE_EXISTS=true" >> "$GITHUB_ENV" | |
| IS_DRAFT=$(gh release view "$TAG" --json isDraft -q '.isDraft') | |
| echo "RELEASE_IS_DRAFT=${IS_DRAFT}" >> "$GITHUB_ENV" | |
| echo "Release exists (draft=${IS_DRAFT})" | |
| else | |
| echo "RELEASE_EXISTS=false" >> "$GITHUB_ENV" | |
| echo "Release does not exist" | |
| fi | |
| - name: Abort if release already published | |
| if: env.RELEASE_EXISTS == 'true' && env.RELEASE_IS_DRAFT != 'true' | |
| run: | | |
| echo "ERROR: Release $TAG already exists and is published." | |
| echo "Refusing to modify an immutable release." | |
| exit 1 | |
| - name: Create GitHub release | |
| if: env.RELEASE_EXISTS != 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| START_TAG: ${{ vars.START_TAG }} | |
| run: | | |
| set -euo pipefail | |
| gh release create "$TAG" \ | |
| --draft \ | |
| --generate-notes \ | |
| --title "$TAG" \ | |
| ${START_TAG:+--notes-start-tag "$START_TAG"} \ | |
| - name: Upload release assets | |
| if: env.TEST_MODE != 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| gh release upload "$TAG" \ | |
| artifacts/${CRATE_FILE} \ | |
| artifacts/${CRATE_FILE}.sha256 \ | |
| sbom/sbom-cyclonedx.json \ | |
| sbom/sbom-spdx.json \ | |
| --clobber | |
| - name: Publish GitHub release | |
| if: env.TEST_MODE != 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| if [ "${RELEASE_IS_DRAFT:-true}" = "true" ]; then | |
| gh release edit "$TAG" --draft=false | |
| echo "Release published" | |
| else | |
| echo "Release already published — skipping" | |
| fi | |
| - name: Verify SLSA provenance (self-check) | |
| if: env.TEST_MODE != 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| echo "Verifying SLSA provenance for ${CRATE_FILE}" | |
| gh attestation verify "artifacts/${CRATE_FILE}" \ | |
| --repo "${GITHUB_REPOSITORY}" | |
| echo "SLSA provenance verification passed" | |
| - name: Cleanup build artifacts before publish | |
| if: env.TEST_MODE != 'true' | |
| run: | | |
| set -euo pipefail | |
| rm -rf artifacts sbom | |
| - name: Enforce clean git tree before publish | |
| if: env.TEST_MODE != 'true' | |
| run: | | |
| set -euo pipefail | |
| if [ -n "$(git status --porcelain)" ]; then | |
| echo "ERROR: working tree is dirty before cargo publish" | |
| git status --porcelain | |
| exit 1 | |
| fi | |
| - name: Publish crate to crates.io | |
| if: env.TEST_MODE != 'true' | |
| env: | |
| CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "${CARGO_REGISTRY_TOKEN:-}" ]; then | |
| echo "CARGO_REGISTRY_TOKEN not set; skipping publish" | |
| exit 1 | |
| fi | |
| cargo publish | |
| echo "cargo publish completed" | |
| - name: Finalize — print verification instructions | |
| run: | | |
| set -euo pipefail | |
| echo "Release completed for tag ${TAG}, artifact artifacts/${CRATE_FILE}" | |
| echo "TEST_MODE=${TEST_MODE}" | |
| echo "If TEST_MODE=true the release was created as a DRAFT and crates.io publish was skipped." | |
| echo "Verification commands (local):" | |
| echo " sha256sum ${CRATE_FILE} && cat ${CRATE_FILE}.sha256" | |
| echo "Verify provenance (SLSA L3):" | |
| echo " slsa-verifier verify-artifact ${CRATE_FILE} \\" | |
| echo " --source-uri github.com/${GITHUB_REPOSITORY} \\" | |
| echo " --source-tag ${TAG}" |