name: Release
on:
workflow_dispatch:
inputs:
version:
description: 'Release version, e.g. 1.2.0 or v1.2.0'
required: true
type: string
permissions:
contents: write
concurrency:
group: release
cancel-in-progress: false
env:
CARGO_TERM_COLOR: always
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Validate version
env:
VERSION_INPUT: ${{ inputs.version }}
run: |
set -euo pipefail
version="${VERSION_INPUT#v}"
if ! printf '%s' "$version" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$'; then
echo "::error::Invalid version '$VERSION_INPUT'. Expected semver such as 1.2.0 or v1.2.0."
exit 1
fi
echo "VERSION=$version" >> "$GITHUB_ENV"
echo "TAG=v$version" >> "$GITHUB_ENV"
- name: Ensure the version matches Cargo.toml
run: |
set -euo pipefail
crate_version="$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version')"
if [ "$crate_version" != "$VERSION" ]; then
echo "::error::Cargo.toml carries version $crate_version but the release input is $VERSION."
exit 1
fi
- name: Ensure tag does not already exist
run: |
set -euo pipefail
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
echo "::error::Tag $TAG already exists locally."
exit 1
fi
if git ls-remote --exit-code --tags origin "refs/tags/$TAG" >/dev/null 2>&1; then
echo "::error::Tag $TAG already exists on the remote."
exit 1
fi
- name: Extract changelog section
run: |
set -euo pipefail
awk -v ver="$VERSION" '
$0 ~ ("^## \\[" ver "\\]") { capture=1; next }
capture && (/^## / || /^\[[^][]+\]:[[:space:]]/) { exit }
capture { print }
' CHANGELOG.md | sed -e '/./,$!d' > "$RUNNER_TEMP/release-notes.md"
if [ ! -s "$RUNNER_TEMP/release-notes.md" ]; then
echo "::error::No changelog section found for [$VERSION] in CHANGELOG.md."
exit 1
fi
echo "Release notes for $TAG:"
echo "----------------------------------------"
cat "$RUNNER_TEMP/release-notes.md"
echo "----------------------------------------"
- name: Run local checks (format, lints, unit and behavior tests)
run: |
cargo fmt --check
cargo clippy --all-targets -- -D warnings
cargo test
- name: Run system tests (live API)
env:
IPREGISTRY_API_KEY: ${{ secrets.IPREGISTRY_API_KEY }}
run: |
set -euo pipefail
if [ -z "${IPREGISTRY_API_KEY:-}" ]; then
echo "::error::IPREGISTRY_API_KEY secret is required to run system tests before a release."
exit 1
fi
cargo test --test integration -- --ignored
- name: Package the crate
run: |
set -euo pipefail
cargo package
test -f "target/package/ipregistry-$VERSION.crate"
- name: Publish to crates.io
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
set -euo pipefail
# Sparse-index path for the crate: <first two>/<next two>/<name>.
if curl -fsSL "https://index.crates.io/ip/re/ipregistry" 2>/dev/null \
| grep -q "\"vers\":\"$VERSION\""; then
echo "::notice::ipregistry $VERSION is already on crates.io; skipping publish."
else
cargo publish --no-verify # already verified by the package step
fi
- name: Create tag and GitHub release
env:
GH_TOKEN: ${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
prerelease=""
case "$VERSION" in
*-*) prerelease="--prerelease" ;;
esac
gh release create "$TAG" \
--target "$GITHUB_SHA" \
--title "$TAG" \
--notes-file "$RUNNER_TEMP/release-notes.md" \
$prerelease \
"target/package/ipregistry-$VERSION.crate"
echo "Released $TAG: ${{ github.server_url }}/${{ github.repository }}/releases/tag/$TAG"