name: Tag release
on:
push:
branches: [main]
paths:
- Cargo.toml
workflow_dispatch:
permissions:
contents: read
concurrency:
group: tag-release
cancel-in-progress: false
jobs:
tag:
name: tag the merged version
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
contents: write
actions: write
steps:
- uses: actions/checkout@v7
timeout-minutes: 2
with:
fetch-tags: true
- name: Tag when the version has no tag
timeout-minutes: 3
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
# Read the version bare, whatever is in the manifest. A previous
# release bump wrote `version = "v0.6.0"` into Cargo.toml, and this
# line then produced the orphan `vv0.6.0` tag. auto-release.yml now
# strips the prefix, but this step is the one that creates a public
# tag and fires the release — it must not trust its input blindly.
RAW="$(grep -m1 '^version' Cargo.toml | sed 's/version = "\(.*\)"/\1/')"
VERSION="${RAW#v}"
if [ "$VERSION" != "$RAW" ]; then
echo "::error::Cargo.toml version '$RAW' carries a leading 'v'. Cargo needs a bare semver; refusing to tag a release from a malformed manifest."
exit 1
fi
if ! printf '%s' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+'; then
echo "::error::Cargo.toml version '$VERSION' is not a bare x.y.z semver; refusing to tag."
exit 1
fi
TAG="v$VERSION"
# Ask the remote, not just the local clone: a tag that exists upstream
# but was not fetched would skip a local-only guard and then fail the
# `git push` with "already exists", turning a no-op release check into
# a red run. One extra round trip buys a deterministic outcome.
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null \
|| git ls-remote --exit-code --tags origin "refs/tags/$TAG" >/dev/null 2>&1; then
echo "$TAG already exists — nothing to do."
exit 0
fi
echo "Cargo.toml is at $VERSION with no $TAG — tagging $GITHUB_SHA."
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag "$TAG" "$GITHUB_SHA"
git push origin "$TAG"
# A tag pushed with GITHUB_TOKEN starts no workflows (push events
# from the token are not among the documented exceptions), so the
# native `tags: ["v*"]` triggers on release-binaries.yml and
# publish.yml stay silent for this automated path. workflow_dispatch
# is the exception — fire the two consumers here instead.
#
# These dispatches are the ONLY trigger on the automated path, and
# nothing retries them (the merge poller re-dispatches tag-release
# only while the TAG is missing — by here it exists). A swallowed
# failure would leave a tagged version with no binaries and no
# crates.io publish, green. Fail loud instead.
failed=0
gh workflow run release-binaries.yml --ref "$TAG" || failed=1
gh workflow run publish.yml --ref "$TAG" || failed=1
[ "$failed" = 0 ] || { echo "::error::dispatch failed for $TAG; re-run this workflow once the API recovers (it no-ops when the tag exists)."; exit 1; }
echo "Pushed $TAG and dispatched release-binaries.yml + publish.yml at it."