From 3de53dcb998bb71df26595c450f6c1c1bab65c2e Mon Sep 17 00:00:00 2001 From: UebelAndre Date: Wed, 10 Jun 2026 12:55:14 -0700 Subject: [PATCH] Add attestation support to releases --- .github/workflows/publish.yaml | 5 +- .github/workflows/release.yaml | 245 ++++++++---------------------- .github/workflows/release_prep.sh | 126 +++++++++++++++ 3 files changed, 189 insertions(+), 187 deletions(-) create mode 100755 .github/workflows/release_prep.sh diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index f76bbb103f..69cbb81f81 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -26,11 +26,10 @@ on: description: Override the ref to read .bcr templates from jobs: publish: - uses: bazel-contrib/publish-to-bcr/.github/workflows/publish.yaml@v1.1.0 + uses: bazel-contrib/publish-to-bcr/.github/workflows/publish.yaml@v1.4.1 with: author_name: bazel-io author_email: 5028808+bazel-io@users.noreply.github.com - attest: false draft: false tag_name: ${{ inputs.release_version }} # Tags don't include a "v" prefix @@ -40,5 +39,7 @@ jobs: templates_ref: ${{ inputs.templates_ref || inputs.release_version }} permissions: contents: write + id-token: write + attestations: write secrets: publish_token: ${{ secrets.publish_token || secrets.BCR_PUBLISH_TOKEN }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 7e8376c78f..11eb49cb3e 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -9,6 +9,7 @@ on: - crate_universe/tools/cross_installer/** - version.bzl - .github/workflows/release.yaml + - .github/workflows/release_prep.sh push: branches: - main @@ -121,205 +122,79 @@ jobs: TARGET: "${{ matrix.env.TARGET }}" - uses: actions/upload-artifact@v4 with: + # The artifact name MUST be the target triple — release_prep.sh + # locates each binary at ${GITHUB_WORKSPACE}//. name: "${{ matrix.env.TARGET }}" path: ${{ github.workspace }}/crate_universe/target/artifacts/${{ matrix.env.TARGET }} if-no-files-found: error - archive: - needs: builds + + # Create and push the version tag at the current main commit. The release + # job's reusable workflow (release_ruleset.yaml) checks out at this tag, so + # it must exist before that job runs. + tag: + if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && startsWith(github.ref, 'refs/heads/main')) + needs: [validation, builds] runs-on: ubuntu-22.04 + permissions: + contents: write outputs: - release_version: ${{ steps.version.outputs.release_version }} - archive_sha256_base64: ${{ steps.archive.outputs.archive_sha256_base64 }} + tag: ${{ steps.create_tag.outputs.tag }} steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 with: - path: ${{ github.workspace }}/crate_universe/target/artifacts - - name: Detect the current version - id: version - run: | - version="$(grep 'VERSION =' ${{ github.workspace }}/version.bzl | grep -o '[[:digit:].]\+')" - echo "RELEASE_VERSION=${version}" >> $GITHUB_ENV - echo "release_version=${version}" >> $GITHUB_OUTPUT - - name: Comment out module overrides in .bazelrc files - run: | - # Find all .bazelrc files and comment out rules_rust module overrides - find . -name "*.bazelrc" -type f | while read -r file; do - if grep -q "^common --override_module=rules_rust=" "$file"; then - echo "Commenting out module override in: $file" - sed -i 's/^common --override_module=rules_rust=/# &/' "$file" - fi - done - - name: Create the rules archive - id: archive + fetch-depth: 0 + - name: Create and push tag + id: create_tag run: | - # Update urls and sha256 values - bazel ${BAZEL_STARTUP_FLAGS[@]} run //crate_universe/tools/urls_generator \ - -- --artifacts-dir="${ARTIFACTS_DIR}" --url-prefix="${URL_PREFIX}" - - bazel clean - - # Build an archive of the repo contents. - # `examples/hello_world` is included for the BCR presubmit; it must appear before --exclude="examples" - tar -czf ${{ github.workspace }}/.github/rules_rust.tar.gz \ - -C ${{ github.workspace }} \ - --exclude=".git" \ - --exclude=".github" \ - --exclude="crate_universe/target" \ - examples/hello_world \ - --exclude="examples" \ - . - - # Save the sha256 checksum of the distro archive to the environment and output - sha256_base64="$(shasum --algorithm 256 ${{ github.workspace }}/.github/rules_rust.tar.gz | awk '{ print $1 }' | xxd -r -p | base64)" - echo "ARCHIVE_SHA256_BASE64=${sha256_base64}" >> $GITHUB_ENV - echo "archive_sha256_base64=${sha256_base64}" >> $GITHUB_OUTPUT - env: - CARGO_BAZEL_GENERATOR_URL: file://${{ github.workspace }}/crate_universe/target/artifacts/x86_64-unknown-linux-gnu/cargo-bazel - ARTIFACTS_DIR: ${{ github.workspace }}/crate_universe/target/artifacts - URL_PREFIX: https://github.com/${{ github.repository_owner }}/rules_rust/releases/download/${{ env.RELEASE_VERSION }} + set -euo pipefail + version="$(grep 'VERSION =' ${{ github.workspace }}/version.bzl | sed 's/VERSION = "//' | sed 's/"//')" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + # Pin the tag to the resolved SHA so a concurrent merge to main + # can't shift the release contents out from under us. + git tag -a "${version}" -m "rules_rust ${version}" "${{ github.sha }}" + git push origin "${version}" + echo "tag=${version}" >> $GITHUB_OUTPUT - # Upload the archive for review in PRs or manual recovery if release fails - - uses: actions/upload-artifact@v4 - with: - name: "rules_rust.tar.gz" - path: ${{ github.workspace }}/.github/rules_rust.tar.gz - if-no-files-found: error + # Build the source archive, attest its provenance under the BCR-trusted + # release_ruleset builder ID, and publish the GitHub release. The actual + # build runs in .github/workflows/release_prep.sh (hardcoded path in + # release_ruleset.yaml). release: - if: startsWith(github.ref, 'refs/heads/main') - needs: [archive] - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - with: - path: ${{ github.workspace }}/artifacts - - name: Set release version - run: | - echo "RELEASE_VERSION=${{ needs.archive.outputs.release_version }}" >> $GITHUB_ENV - echo "ARCHIVE_SHA256_BASE64=${{ needs.archive.outputs.archive_sha256_base64 }}" >> $GITHUB_ENV - - name: Generate release notes - run: | - # Generate the release notes - sed 's#{version}#${{ env.RELEASE_VERSION }}#g' ${{ github.workspace }}/.github/release_notes.template \ - | sed 's#{sha256_base64}#${{ env.ARCHIVE_SHA256_BASE64 }}#g' \ - > ${{ github.workspace }}/.github/release_notes.txt - - name: Create release - uses: softprops/action-gh-release@v1 - id: rules_rust_release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - generate_release_notes: true - tag_name: ${{ env.RELEASE_VERSION }} - body_path: ${{ github.workspace }}/.github/release_notes.txt - target_commitish: ${{ github.base_ref }} - - - name: "Upload the rules archive" - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.rules_rust_release.outputs.upload_url }} - asset_name: rules_rust-${{ env.RELEASE_VERSION }}.tar.gz - asset_path: ${{ github.workspace }}/artifacts/rules_rust.tar.gz/rules_rust.tar.gz - asset_content_type: application/gzip + needs: tag + permissions: + contents: write + id-token: write + attestations: write + uses: bazel-contrib/.github/.github/workflows/release_ruleset.yaml@v7.7.0 + with: + # Tests already ran on the PR that bumped version.bzl. Override the + # `bazel test //...` default — rules_rust's full suite can't fit on a + # single ubuntu-latest runner. + bazel_test_command: "bazel info release" + release_files: | + rules_rust-*.tar.gz + cargo-bazel-aarch64-apple-darwin + cargo-bazel-aarch64-pc-windows-msvc.exe + cargo-bazel-aarch64-unknown-linux-gnu + cargo-bazel-aarch64-unknown-linux-musl + cargo-bazel-s390x-unknown-linux-gnu + cargo-bazel-x86_64-apple-darwin + cargo-bazel-x86_64-pc-windows-gnu.exe + cargo-bazel-x86_64-pc-windows-msvc.exe + cargo-bazel-x86_64-unknown-linux-gnu + cargo-bazel-x86_64-unknown-linux-musl + prerelease: false + tag_name: ${{ needs.tag.outputs.tag }} - # There must be a upload action for each platform triple we create - - name: "Upload aarch64-apple-darwin" - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.rules_rust_release.outputs.upload_url }} - asset_name: cargo-bazel-aarch64-apple-darwin - asset_path: ${{ github.workspace }}/artifacts/aarch64-apple-darwin/cargo-bazel - asset_content_type: application/octet-stream - - name: "Upload aarch64-pc-windows-msvc" - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.rules_rust_release.outputs.upload_url }} - asset_name: cargo-bazel-aarch64-pc-windows-msvc.exe - asset_path: ${{ github.workspace }}/artifacts/aarch64-pc-windows-msvc/cargo-bazel.exe - asset_content_type: application/octet-stream - - name: "Upload aarch64-unknown-linux-gnu" - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.rules_rust_release.outputs.upload_url }} - asset_name: cargo-bazel-aarch64-unknown-linux-gnu - asset_path: ${{ github.workspace }}/artifacts/aarch64-unknown-linux-gnu/cargo-bazel - asset_content_type: application/octet-stream - - name: "Upload s390x-unknown-linux-gnu" - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.rules_rust_release.outputs.upload_url }} - asset_name: cargo-bazel-s390x-unknown-linux-gnu - asset_path: ${{ github.workspace }}/artifacts/s390x-unknown-linux-gnu/cargo-bazel - asset_content_type: application/octet-stream - - name: "Upload x86_64-apple-darwin" - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.rules_rust_release.outputs.upload_url }} - asset_name: cargo-bazel-x86_64-apple-darwin - asset_path: ${{ github.workspace }}/artifacts/x86_64-apple-darwin/cargo-bazel - asset_content_type: application/octet-stream - - name: "Upload x86_64-pc-windows-gnu" - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.rules_rust_release.outputs.upload_url }} - asset_name: cargo-bazel-x86_64-pc-windows-gnu.exe - asset_path: ${{ github.workspace }}/artifacts/x86_64-pc-windows-gnu/cargo-bazel.exe - asset_content_type: application/octet-stream - - name: "Upload x86_64-pc-windows-msvc" - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.rules_rust_release.outputs.upload_url }} - asset_name: cargo-bazel-x86_64-pc-windows-msvc.exe - asset_path: ${{ github.workspace }}/artifacts/x86_64-pc-windows-msvc/cargo-bazel.exe - asset_content_type: application/octet-stream - - name: "Upload x86_64-unknown-linux-gnu" - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.rules_rust_release.outputs.upload_url }} - asset_name: cargo-bazel-x86_64-unknown-linux-gnu - asset_path: ${{ github.workspace }}/artifacts/x86_64-unknown-linux-gnu/cargo-bazel - asset_content_type: application/octet-stream - - name: "Upload x86_64-unknown-linux-musl" - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.rules_rust_release.outputs.upload_url }} - asset_name: cargo-bazel-x86_64-unknown-linux-musl - asset_path: ${{ github.workspace }}/artifacts/x86_64-unknown-linux-musl/cargo-bazel - asset_content_type: application/octet-stream - - name: "Upload aarch64-unknown-linux-musl" - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.rules_rust_release.outputs.upload_url }} - asset_name: cargo-bazel-aarch64-unknown-linux-musl - asset_path: ${{ github.workspace }}/artifacts/aarch64-unknown-linux-musl/cargo-bazel - asset_content_type: application/octet-stream publish: - needs: [archive, release] + needs: [tag, release] + permissions: + contents: write + id-token: write + attestations: write uses: ./.github/workflows/publish.yaml with: - release_version: ${{ needs.archive.outputs.release_version }} + release_version: ${{ needs.tag.outputs.tag }} secrets: BCR_PUBLISH_TOKEN: ${{ secrets.BCR_PUBLISH_TOKEN }} diff --git a/.github/workflows/release_prep.sh b/.github/workflows/release_prep.sh new file mode 100755 index 0000000000..349d293900 --- /dev/null +++ b/.github/workflows/release_prep.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +# Build the rules_rust release archive and stage release assets. +# Invoked by bazel-contrib/.github/.github/workflows/release_ruleset.yaml. +# +# Args: +# $1: tag name (e.g. 0.61.0). Must match VERSION in version.bzl. +# +# Side effects: +# Writes rules_rust-${TAG}.tar.gz and renamed cargo-bazel-[.exe] +# binaries to the current directory. These match the release_files glob in +# release.yaml. +# +# Output: +# Release notes to stdout. The release_ruleset workflow redirects this into +# release_notes.txt for the GitHub release body. All build noise (bazel, +# tar, sed, etc.) is redirected to stderr. + +set -euo pipefail + +# Redirect all stdout to stderr; keep fd 3 for the final release-notes write. +exec 3>&1 1>&2 + +TAG="${1:?tag_name required}" + +WORKSPACE="${GITHUB_WORKSPACE:-$(pwd)}" +REPOSITORY_OWNER="${GITHUB_REPOSITORY_OWNER:-bazelbuild}" + +# Validate the tag matches version.bzl. +ON_DISK_VERSION="$(grep 'VERSION =' "${WORKSPACE}/version.bzl" | sed 's/VERSION = "//' | sed 's/"//')" +if [[ "${ON_DISK_VERSION}" != "${TAG}" ]]; then + echo "ERROR: tag ${TAG} does not match version.bzl VERSION=${ON_DISK_VERSION}" + exit 1 +fi + +# Triples must match each artifact's `name:` in the `builds` matrix of release.yaml. +TRIPLES=( + aarch64-apple-darwin + aarch64-pc-windows-msvc + aarch64-unknown-linux-gnu + aarch64-unknown-linux-musl + s390x-unknown-linux-gnu + x86_64-apple-darwin + x86_64-pc-windows-gnu + x86_64-pc-windows-msvc + x86_64-unknown-linux-gnu + x86_64-unknown-linux-musl +) + +# actions/download-artifact@v8 with no inputs places each artifact at +# ${GITHUB_WORKSPACE}//...; the matrix uploads use the triple as +# the name. Restructure into the layout urls_generator expects and restore the +# executable bit (download-artifact strips it on cross-runner downloads). +ARTIFACTS_DIR="${WORKSPACE}/crate_universe/target/artifacts" +mkdir -p "${ARTIFACTS_DIR}" +for triple in "${TRIPLES[@]}"; do + src_dir="${WORKSPACE}/${triple}" + if [[ ! -d "${src_dir}" ]]; then + echo "ERROR: missing matrix artifact directory ${src_dir}" + exit 1 + fi + mkdir -p "${ARTIFACTS_DIR}/${triple}" + if [[ "${triple}" == *windows* ]]; then + binary="cargo-bazel.exe" + else + binary="cargo-bazel" + fi + cp "${src_dir}/${binary}" "${ARTIFACTS_DIR}/${triple}/${binary}" + chmod +x "${ARTIFACTS_DIR}/${triple}/${binary}" +done + +# Comment out rules_rust module overrides in any .bazelrc files so released +# users don't inherit local development overrides. +find "${WORKSPACE}" -name "*.bazelrc" -type f | while read -r file; do + if grep -q "^common --override_module=rules_rust=" "${file}"; then + echo "Commenting out module override in: ${file}" + sed -i 's/^common --override_module=rules_rust=/# &/' "${file}" + fi +done + +# Update crate_universe/private/urls.bzl with download URLs and SHA256s for +# each platform's cargo-bazel binary. +export CARGO_BAZEL_GENERATOR_URL="file://${ARTIFACTS_DIR}/x86_64-unknown-linux-gnu/cargo-bazel" +URL_PREFIX="https://github.com/${REPOSITORY_OWNER}/rules_rust/releases/download/${TAG}" +( + cd "${WORKSPACE}" + bazel run //crate_universe/tools/urls_generator -- \ + --artifacts-dir="${ARTIFACTS_DIR}" \ + --url-prefix="${URL_PREFIX}" + bazel clean +) + +# Build the source archive. The on-disk filename matches the URL in +# .bcr/source.template.json — the SLSA attestation subject must equal the +# published asset name. +ARCHIVE="rules_rust-${TAG}.tar.gz" +# `examples/hello_world` is included for the BCR presubmit; it must appear +# before --exclude="examples". +tar -czf "${ARCHIVE}" \ + -C "${WORKSPACE}" \ + --exclude=".git" \ + --exclude=".github" \ + --exclude="crate_universe/target" \ + examples/hello_world \ + --exclude="examples" \ + . + +# Rename cargo-bazel binaries to the release asset names expected by the +# release_files glob in release.yaml. +for triple in "${TRIPLES[@]}"; do + if [[ "${triple}" == *windows* ]]; then + cp "${ARTIFACTS_DIR}/${triple}/cargo-bazel.exe" "cargo-bazel-${triple}.exe" + else + cp "${ARTIFACTS_DIR}/${triple}/cargo-bazel" "cargo-bazel-${triple}" + fi +done + +# Compute the SRI-compatible base64 sha256 of the source archive for the +# release notes template. +SHA256_BASE64="$(shasum --algorithm 256 "${ARCHIVE}" | awk '{ print $1 }' | xxd -r -p | base64)" + +# Render release notes and emit only the rendered content on the original +# stdout (fd 3) so the reusable workflow captures clean notes. +NOTES="$(mktemp)" +sed "s#{version}#${TAG}#g; s#{sha256_base64}#${SHA256_BASE64}#g" \ + "${WORKSPACE}/.github/release_notes.template" > "${NOTES}" +cat "${NOTES}" >&3