From 50d13c39554e0a9ec138fdc1f2b900c6d6c8cf99 Mon Sep 17 00:00:00 2001 From: Evgeny Prilepin Date: Fri, 1 May 2026 20:56:15 +0100 Subject: [PATCH] Add manual release workflow Add a workflow_dispatch release pipeline with dry-run support, master branch validation, release checks, crates.io environment publishing, and GitHub Release creation from CHANGELOG notes. --- .github/workflows/release.yml | 155 ++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..83e8b3d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,155 @@ +name: "Release" + +on: + workflow_dispatch: + inputs: + dry_run: + description: "Run release checks without publishing the crate or creating a GitHub release" + required: true + default: true + type: boolean + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + verify: + name: Verify release + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + tag: ${{ steps.version.outputs.tag }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install rust toolchain + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + override: true + components: rustfmt, clippy + + - name: Ensure release is run from master + run: | + if [ "${GITHUB_REF}" != "refs/heads/master" ]; then + echo "Releases must be run from master, got ${GITHUB_REF}." + exit 1 + fi + + - name: Read crate version + id: version + run: | + version="$(cargo pkgid | sed -E 's/.*#.*@//')" + tag="v${version}" + echo "version=${version}" >> "${GITHUB_OUTPUT}" + echo "tag=${tag}" >> "${GITHUB_OUTPUT}" + echo "Release version: ${version}" + + - name: Validate changelog + run: | + expected="## ${{ steps.version.outputs.tag }}" + first_heading="$(awk '/^## / { print; exit }' CHANGELOG.md)" + + case "${first_heading}" in + "${expected}"|"${expected} "*) ;; + *) + echo "Top CHANGELOG.md section must start with '${expected}', got '${first_heading}'." + exit 1 + ;; + esac + + - name: Check release tag + run: | + tag="${{ steps.version.outputs.tag }}" + if git rev-parse -q --verify "refs/tags/${tag}" >/dev/null; then + echo "Tag ${tag} already exists." + exit 1 + fi + + - name: Check format + run: cargo fmt --check + + - name: Check code + run: cargo clippy --all-targets + + - name: Run tests + run: cargo test --locked + + - name: Check package + run: cargo publish --dry-run --locked + + publish: + name: Publish release + needs: verify + runs-on: ubuntu-latest + environment: crates-io + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.sha }} + + - name: Install rust toolchain + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + override: true + + - name: Check release tag + run: | + tag="${{ needs.verify.outputs.tag }}" + if git rev-parse -q --verify "refs/tags/${tag}" >/dev/null; then + echo "Tag ${tag} already exists." + exit 1 + fi + + - name: Extract release notes + run: | + tag="${{ needs.verify.outputs.tag }}" + awk -v tag="${tag}" ' + $0 ~ "^## " tag "([[:space:]]|$)" { + in_section = 1 + next + } + in_section && /^## / { + exit + } + in_section { + print + } + ' CHANGELOG.md | sed '/^[[:space:]]*$/d' > release-notes.md + + if [ ! -s release-notes.md ]; then + echo "No release notes found for ${tag} in CHANGELOG.md." + exit 1 + fi + + - name: Publish crate + if: ${{ !inputs.dry_run }} + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: cargo publish --locked + + - name: Create GitHub release + if: ${{ !inputs.dry_run }} + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "${{ needs.verify.outputs.tag }}" \ + --target "${GITHUB_SHA}" \ + --title "${{ needs.verify.outputs.tag }}" \ + --notes-file release-notes.md + + - name: Dry run summary + if: ${{ inputs.dry_run }} + run: | + echo "Dry run completed for ${{ needs.verify.outputs.tag }}." + echo "The workflow verified the crate and release notes." + echo "No crate was published and no GitHub release was created."