Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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."
Loading