name: Create Release
on:
push:
tags:
- '[0-9]+.[0-9]+.[0-9]+'
permissions:
contents: read
defaults:
run:
shell: bash
jobs:
verify-ci:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
actions: read
contents: read
steps:
- name: Require green CI on the tagged commit
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
run: |
set -euo pipefail
run_id=$(gh run list --commit "${{ github.sha }}" \
--workflow ci.yaml --limit 1 --json databaseId \
--jq '.[0].databaseId // empty')
if [ -z "$run_id" ]; then
echo "::error::No CI run found for ${{ github.sha }}. Refusing to create a release."
exit 1
fi
gh run watch "$run_id" --exit-status
create-release:
needs: [verify-ci]
runs-on: ubuntu-latest
permissions:
contents: write
actions: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Create GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
tag="${{ github.ref_name }}"
link="https://github.com/${{ github.repository }}/blob/${tag}/CHANGELOG.md"
# Point at this version's CHANGELOG section. GitHub's heading anchor
# includes the release date ("## [2.2.1] - 2026-09-01" -> #221---2026-09-01),
# so it has to be derived from the heading rather than the version alone.
# If the heading is not found, the link degrades to the file itself --
# never to a broken anchor.
heading=$(grep -m1 "^## \[${tag}\]" CHANGELOG.md || true)
if [ -n "$heading" ]; then
anchor=$(printf '%s' "$heading" | sed 's/^## //' | tr -d '[].' | tr ' ' '-' | tr 'A-Z' 'a-z')
link="${link}#${anchor}"
fi
gh release create "${tag}" \
--title "${tag}" \
--notes "📖 [Changelog for ${tag}](${link})" \
--generate-notes
- name: Start the publishing workflows
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
for wf in release-crates release-npm release-pypi release-executable; do
echo "Dispatching ${wf}.yaml at ${{ github.ref_name }}"
gh workflow run "${wf}.yaml" --ref "${{ github.ref_name }}"
done