From e92a402066eac59f5eef70a6fbce81611baf6360 Mon Sep 17 00:00:00 2001 From: Michael Kubacki Date: Tue, 14 Jan 2025 14:58:54 -0500 Subject: [PATCH] Add crate release workflow (#393) Publishes crates in Rust repos to the repo designated registry (crates.io default). Co-authored-by: Joey Vagedes Signed-off-by: Michael Kubacki --- .github/actions/rust-tool-cache/action.yml | 64 ++++++++++ .github/workflows/ReleaseWorkflow.yml | 133 +++++++++++++++++++++ .sync/Files.yml | 9 ++ .sync/workflows/leaf/publish-release.yml | 39 ++++++ 4 files changed, 245 insertions(+) create mode 100644 .github/actions/rust-tool-cache/action.yml create mode 100644 .github/workflows/ReleaseWorkflow.yml create mode 100644 .sync/workflows/leaf/publish-release.yml diff --git a/.github/actions/rust-tool-cache/action.yml b/.github/actions/rust-tool-cache/action.yml new file mode 100644 index 00000000..b5db6de3 --- /dev/null +++ b/.github/actions/rust-tool-cache/action.yml @@ -0,0 +1,64 @@ +# A GitHub action that loads rust tools and toolchains from cache. If there is a miss, it will install +# them. the tools are read from the tools section of the rust-toolchain.toml file at the root of the repository. +# +# Copyright (c) Microsoft Corporation. +# SPDX-License-Identifier: BSD-2-Clause-Patent +# + +name: "Install Rust Tools" +description: "This action loads rust tools and toolchains from cache, or installs them." + +runs: + using: composite + steps: + - name: Rust Tool Cache + id: tool-cache + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.rustup/toolchains/ + key: ${{ runner.os }}-rust-tools-${{ hashFiles('**/rust-toolchain.toml' )}} + + - name: Install cargo-binstall + uses: cargo-bins/cargo-binstall@v1.10.17 + + # Read any tools from rust-toolchain.toml file and installs them + - name: Install Rust Tools + shell: bash + run: | + FILE="rust-toolchain.toml" + + if [ ! -f "$FILE" ]; then + echo "::error::File $FILE not found." + exit 1 + fi + + if ! grep -q '^\[tools\]' "$FILE"; then + echo "::warning::[tools] section not found in $FILE." + exit 1 + fi + + # Extract tools section from rust-toolchain.toml + sed -n '/\[tools\]/,/^$/p' "$FILE" | grep -v '\[tools\]' | while read -r line; do + # Extract tool name and clean it + TOOL_NAME=${line%%=*} + TOOL_NAME=${TOOL_NAME//[[:space:]]/} + TOOL_NAME="${TOOL_NAME//$'\n'/}" + + # Extract tool version and clean it + TOOL_VERSION=${line#*=} + TOOL_VERSION=${TOOL_VERSION//[[:space:]]/} + TOOL_VERSION=${TOOL_VERSION//\"/} + TOOL_VERSION="${TOOL_VERSION//$'\n'/}" + + echo "" + echo "##################################################################" + echo "Installing $TOOL_NAME@$TOOL_VERSION" + echo "##################################################################" + echo "" + + # Attempt to binstall the tool first. If it fails, install it using cargo + cargo binstall -y $TOOL_NAME --version $TOOL_VERSION || cargo install $TOOL_NAME --version $TOOL_VERSION + done + if: steps.tool-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/ReleaseWorkflow.yml b/.github/workflows/ReleaseWorkflow.yml new file mode 100644 index 00000000..bcd38ae1 --- /dev/null +++ b/.github/workflows/ReleaseWorkflow.yml @@ -0,0 +1,133 @@ +# @file ReleaseWorkflow.yml +# +# A reusable CI workflow that releases all crates in a repository. +# +## +# Copyright (c) Microsoft Corporation. +# SPDX-License-Identifier: BSD-2-Clause-Patent +## +name: Publish + +on: + workflow_call: + secrets: + CRATES_IO_TOKEN: + description: 'The token to use for authenticating with crates.io' + required: true + +jobs: + run: + name: Publish + + runs-on: ubuntu-latest + + permissions: + contents: write + actions: read + + steps: + - name: ✅ Checkout Repository ✅ + uses: actions/checkout@v4 + + - name: 🛠️ Download Rust Tools 🛠️ + uses: microsoft/mu_devops/.github/actions/rust-tool-cache@main + + - name: Get Current Draft Release + id: draft_release + uses: actions/github-script@v7 + with: + script: | + const releases = await github.rest.repos.listReleases({ + owner: context.repo.owner, + repo: context.repo.repo, + }); + + const draftReleaseList = releases.data.filter(release => release.draft); + + if (draftReleaseList.length === 0) { + core.setFailed("No draft release found. Exiting with error."); + } else if (draftReleaseList.length > 1) { + core.setFailed("Multiple draft releases found. Exiting with error."); + } else { + const draftRelease = draftReleaseList[0]; + + let tag = draftRelease.tag_name; + if (tag.startsWith('v')) { + tag = tag.slice(1); + } + core.setOutput("id", draftRelease.id); + core.setOutput("tag", tag); + console.log(`Draft Release ID: ${draftRelease.id}`); + console.log(`Draft Release Tag: ${tag}`); + } + + - name: Cargo Release Dry Run + run: cargo release ${{ steps.draft_release.outputs.tag }} --workspace + env: + RUSTC_BOOTSTRAP: 1 + + - name: Login to Crates.io + run: cargo login ${{ secrets.CRATES_IO_TOKEN }} + + - name: Update git credentials + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + - name: Cargo Release + run: cargo release ${{ steps.draft_release.outputs.tag }} -x --no-tag --no-confirm --workspace + env: + RUSTC_BOOTSTRAP: 1 + + - name: Wait for Release Draft Updater + uses: actions/github-script@v7 + with: + script: | + const workflowId = "release-draft.yml"; + const ref = "main"; + const owner = context.repo.owner; + const repo = context.repo.repo; + + // Try for 10 minutes. It should only take a few seconds + let maxAttempts = 40; + let attempt = 0; + let completed = false + + while (attempt < maxAttempts && !completed) { + await new Promise(resolve => setTimeout(resolve, 15000)); + const runs = await github.rest.actions.listWorkflowRuns({ + owner, + repo, + workflow_id: workflowId, + branch: ref, + event: 'push', + status: 'in_progress', + }); + + if (runs.data.workflow_runs.length === 0) { + completed = true; + } else { + attempt++; + } + } + + if (!completed) { + core.setFailed("Release Drafter did not complete in time. Please perform the release manually."); + } + + - name: Publish Release + uses: actions/github-script@v7 + with: + script: | + const releaseId = ${{ steps.draft_release.outputs.id }}; + + const response = await github.rest.repos.updateRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + release_id: releaseId, + draft: false, + }); + + if (response.status !== 200) { + core.setFailed(`Failed to publish release. Exiting with error.`); + } diff --git a/.sync/Files.yml b/.sync/Files.yml index e34b2097..d773e355 100644 --- a/.sync/Files.yml +++ b/.sync/Files.yml @@ -557,6 +557,15 @@ group: repos: | microsoft/mu_devops +# Leaf Workflow - Publish Release + - files: + - source: .sync/workflows/leaf/publish-release.yml + dest: .github/workflows/publish-release.yml + repos: | + microsoft/mu_rust_helpers + microsoft/mu_rust_hid + microsoft/mu_rust_pi + # Leaf Workflow - Pull Request Validator - files: - source: .sync/workflows/leaf/pull-request-formatting-validator.yml diff --git a/.sync/workflows/leaf/publish-release.yml b/.sync/workflows/leaf/publish-release.yml new file mode 100644 index 00000000..2408d162 --- /dev/null +++ b/.sync/workflows/leaf/publish-release.yml @@ -0,0 +1,39 @@ +# @file publish-release.yml +# +# A Github workflow that publishes all crates in a repository to crates.io and creates a release on +# GitHub. +# +# Copyright (c) Microsoft Corporation. +# SPDX-License-Identifier: BSD-2-Clause-Patent +## + +name: Publish Release + +on: + workflow_dispatch: + +jobs: + validate_branch: + name: Validate Branch + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Validate Branch + run: | + if [ "${GITHUB_REF}" != "refs/heads/main" ]; then + echo "This workflow can only be run on the main branch." + exit 1 + fi + + release: + name: Release + needs: validate_branch + uses: microsoft/mu_devops/.github/workflows/ReleaseWorkflow.yml@main + secrets: + CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} + permissions: + contents: write + actions: read