name: Release
on:
push:
tags:
- "v*.*.*"
permissions:
contents: write
jobs:
publish:
name: Publish ${{ github.ref_name }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Install Rust toolchain (stable)
uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Run the release test suite
run: cargo test --workspace --lib --tests --examples --locked
- name: Verify the tag matches Cargo.toml's version
run: |
TAG="${GITHUB_REF_NAME#v}"
PKG_VERSION=$(cargo metadata --no-deps --format-version 1 \
| python3 -c "import sys, json; print(json.load(sys.stdin)['packages'][0]['version'])")
if [ "$TAG" != "$PKG_VERSION" ]; then
echo "Tag v$TAG does not match Cargo.toml version $PKG_VERSION" >&2
exit 1
fi
- name: Check crates.io version
id: crates_io
run: |
VERSION="${GITHUB_REF_NAME#v}"
STATUS=$(curl --silent --show-error --output /dev/null --write-out "%{http_code}" \
--header "User-Agent: feichai0017/holt release workflow" \
"https://crates.io/api/v1/crates/holt/$VERSION" || true)
case "$STATUS" in
200)
echo "published=true" >> "$GITHUB_OUTPUT"
echo "holt $VERSION already exists on crates.io; skipping cargo publish."
;;
404)
echo "published=false" >> "$GITHUB_OUTPUT"
echo "holt $VERSION is not on crates.io yet; cargo publish will run."
;;
*)
echo "published=false" >> "$GITHUB_OUTPUT"
echo "crates.io lookup returned HTTP $STATUS; cargo publish will decide."
;;
esac
- name: Publish to crates.io
if: steps.crates_io.outputs.published != 'true'
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
ERR_FILE="${RUNNER_TEMP:-/tmp}/holt-publish.err"
set +e
cargo publish --locked 2> "$ERR_FILE"
STATUS=$?
set -e
if [ "$STATUS" -eq 0 ]; then
exit 0
fi
if grep -q "already exists on crates.io index" "$ERR_FILE"; then
cat "$ERR_FILE"
echo "holt ${GITHUB_REF_NAME#v} was published by another run; continuing."
exit 0
fi
cat "$ERR_FILE" >&2
exit "$STATUS"
- name: Build release notes from CHANGELOG slice
id: changelog
run: |
VERSION="${GITHUB_REF_NAME#v}"
# Pull everything between `## [VERSION]` and the next
# top-level `## [` header out of CHANGELOG.md. `awk` is
# more portable here than `sed` ranges across BSD / GNU.
awk -v v="$VERSION" '
$0 ~ "^## \\[" v "\\]" { p = 1; next }
p && $0 ~ "^## \\[" { exit }
p { print }
' CHANGELOG.md > release_notes.md
echo "notes_path=release_notes.md" >> "$GITHUB_OUTPUT"
- name: Create GitHub Release
uses: softprops/action-gh-release@v3
with:
name: ${{ github.ref_name }}
body_path: ${{ steps.changelog.outputs.notes_path }}
draft: false
prerelease: ${{ contains(github.ref_name, '-') }}