chore(ci): harden the release supply chain #298
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
| # 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 }} |