Skip to content

chore(ci): harden the release supply chain #298

chore(ci): harden the release supply chain

chore(ci): harden the release supply chain #298

Workflow file for this run

# https://github.com/actions-rs/example/blob/23ffb1bf0016f41999902ba7542b4f1bb1a89c48/.github/workflows/quickstart.yml#L4
name: CI
on:
push:
branches:
- main
# See:
# https://stackoverflow.com/questions/62968897/is-it-possible-to-not-run-github-action-for-readme-updates
# and
# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-excluding-paths
paths-ignore:
- '**.md'
pull_request:
paths-ignore:
- '**.md'
env:
CARGO_TERM_COLOR: always
permissions:
contents: read
jobs:
check:
name: Check
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Run cargo check
run: cargo check
test:
name: Test Suite
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Run cargo test with backtrace
run: cargo test -- --nocapture
env:
RUST_BACKTRACE: 1
lints:
name: Lints
runs-on: ubuntu-latest
env:
RUSTFLAGS: "-Dwarnings"
steps:
- name: Checkout sources
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Run cargo fmt
run: cargo fmt --all -- --check
- name: Run cargo clippy
run: cargo clippy --all-targets --all-features
# The release is created as a DRAFT and only published once every asset --
# binaries plus the generated DotSlash files -- has been uploaded. This
# ordering is required for immutable releases: once a release is published,
# GitHub rejects further asset uploads with
# "Cannot upload assets to an immutable release".
# See https://github.com/actions/attest-build-provenance/issues/734
release:
runs-on: ubuntu-latest
permissions:
contents: write
needs:
- test
- lints
- check
outputs:
new_version: ${{ steps.check_for_version_changes.outputs.new_version }}
changed: ${{ steps.check_for_version_changes.outputs.changed }}
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
# https://stackoverflow.com/questions/65944700/how-to-run-git-diff-in-github-actions
# TLDR – By default this action fetches no history.
# We need a bit of history to be able to check if we've recently updated the version in Cargo.toml
fetch-depth: 2
- name: Check for version changes in Cargo.toml
id: check_for_version_changes
run: |
# When there are no changes, VERSION_CHANGES will be empty
# Without the echo, this command would exit with a 1, causing the GitHub Action to fail
# Instead, we want it to succeed, but just evaluate `changed=false` in the other branch of the conditional
VERSION_CHANGES=$(git diff HEAD~1 HEAD Cargo.toml | grep "\+version" || echo "")
if [[ -n $VERSION_CHANGES ]]; then
NEW_VERSION=$(echo $VERSION_CHANGES | awk -F'"' '{print $2}')
echo "changed=true" >> $GITHUB_OUTPUT
echo "new_version=v$NEW_VERSION" >> $GITHUB_OUTPUT
else
echo "changed=false" >> $GITHUB_OUTPUT
fi
- name: Create draft GitHub Release if current commit has updated the version in Cargo.toml
if: steps.check_for_version_changes.outputs.changed == 'true'
run: |
gh release create "$NEW_VERSION" --target "$COMMIT_SHA" --generate-notes --draft
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NEW_VERSION: ${{ steps.check_for_version_changes.outputs.new_version }}
COMMIT_SHA: ${{ github.sha }}
upload-mac-universal-bin:
needs: release
runs-on: macos-latest
permissions:
contents: write
# Required by actions/attest-build-provenance: id-token to mint the OIDC
# token for the Sigstore signing certificate, attestations to persist the
# resulting attestation.
id-token: write
attestations: write
if: needs.release.outputs.new_version != ''
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Build
run: cargo build --release --target aarch64-apple-darwin --target x86_64-apple-darwin
- name: Create universal binary
run: |
# This combines the intel and m1 binaries into a single binary
lipo -create -output target/codeowners target/aarch64-apple-darwin/release/codeowners target/x86_64-apple-darwin/release/codeowners
# lipo carries over the per-architecture linker (ad-hoc) signatures,
# but Apple recommends re-signing the merged binary so it carries a
# single coherent signature. All code on Apple silicon must be at
# least ad-hoc signed or it is SIGKILLed on launch, so verify rather
# than assume. https://developer.apple.com/forums/thread/708552
codesign --force --sign - target/codeowners
codesign --verify --verbose target/codeowners
codesign -dv target/codeowners
# Creates artifact for homebrew. -C means run from `target` directory
# NOTE: any mutation of the binary must happen BEFORE signing, and
# any repackaging must happen BEFORE attestation, or the digest the
# attestation covers will not match what users download.
tar -czf target/codeowners-mac.tar.gz -C target codeowners
- name: Attest build provenance
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: target/codeowners-mac.tar.gz
- name: Upload mac universal binary
run: |
# This tarball is a binary that is executable
gh release upload "$NEW_VERSION" target/codeowners-mac.tar.gz
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NEW_VERSION: ${{ needs.release.outputs.new_version }}
upload-linux-bin:
needs: release
if: needs.release.outputs.new_version != ''
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
attestations: write
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Update local toolchain
run: |
cargo install cross
- name: Build linux binaries
run: |
cross build --release --target x86_64-unknown-linux-gnu
cross build --release --target aarch64-unknown-linux-gnu
- name: Package linux binaries
run: |
tar -czf target/x86_64-unknown-linux-gnu.tar.gz -C target/x86_64-unknown-linux-gnu/release codeowners
tar -czf target/aarch64-unknown-linux-gnu.tar.gz -C target/aarch64-unknown-linux-gnu/release codeowners
- name: Attest build provenance
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: |
target/x86_64-unknown-linux-gnu.tar.gz
target/aarch64-unknown-linux-gnu.tar.gz
- name: Upload linux binaries
run: |
gh release upload "$NEW_VERSION" target/x86_64-unknown-linux-gnu.tar.gz
gh release upload "$NEW_VERSION" target/aarch64-unknown-linux-gnu.tar.gz
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NEW_VERSION: ${{ needs.release.outputs.new_version }}
generate-dotslash-files:
name: Generating and uploading DotSlash files
needs:
- release
- upload-linux-bin
- upload-mac-universal-bin
if: needs.release.outputs.new_version != ''
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: facebook/dotslash-publish-release@2539c4d8ae00a42773306c8731d2dd3724d979d2 # v1
# This is necessary because the action uses
# `gh release upload` to publish the generated DotSlash file(s)
# as part of the release.
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
# Additional file that lives in your repo that defines
# how your DotSlash file(s) should be generated.
config: .github/workflows/dotslash-config.json
# Tag for the release to target.
tag: ${{ needs.release.outputs.new_version }}
# Publishing last is what makes immutable releases workable: every asset is
# in place before the release becomes visible and frozen.
publish-release:
name: Publish the release
needs:
- release
- generate-dotslash-files
if: needs.release.outputs.new_version != ''
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Undraft the release
run: |
gh release edit "$NEW_VERSION" --draft=false --repo "$GITHUB_REPOSITORY"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NEW_VERSION: ${{ needs.release.outputs.new_version }}