name: Release
on:
workflow_dispatch:
inputs:
target_branch:
description: Release branch
required: true
type: choice
options:
- develop
- main
version:
description: Release version (X.Y.Z)
required: true
type: string
pull_request:
types: [closed]
branches:
- develop
- main
permissions:
actions: write
contents: write
pull-requests: read
concurrency:
group: release-${{ github.event_name == 'workflow_dispatch' && inputs.target_branch || github.event.pull_request.base.ref }}
cancel-in-progress: false
env:
CARGO_TERM_COLOR: always
MAIN_BASE_VERSION: ${{ vars.MAIN_BASE_VERSION }}
DEVELOP_BASE_VERSION: ${{ vars.DEVELOP_BASE_VERSION }}
jobs:
plan:
if: github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true
runs-on: ubuntu-latest
outputs:
should_release: ${{ steps.plan.outputs.should_release }}
reason: ${{ steps.plan.outputs.reason }}
bump: ${{ steps.plan.outputs.bump }}
base_version: ${{ steps.plan.outputs.base_version }}
previous_version: ${{ steps.plan.outputs.previous_version }}
version: ${{ steps.plan.outputs.version }}
target_branch: ${{ steps.plan.outputs.target_branch }}
source_branch: ${{ steps.plan.outputs.source_branch }}
merge_sha: ${{ steps.context.outputs.merge_sha }}
steps:
- name: Checkout selected branch
if: github.event_name == 'workflow_dispatch'
uses: actions/checkout@v4
with:
ref: ${{ inputs.target_branch }}
fetch-depth: 0
- name: Checkout merge commit
if: github.event_name == 'pull_request'
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}
fetch-depth: 0
- name: Fetch tags
run: git fetch --force --tags
- name: Resolve base versions
id: bases
shell: bash
run: |
set -euo pipefail
current_version="$(
sed -nE 's/^version = "([0-9]+\.[0-9]+\.[0-9]+)"/\1/p' Cargo.toml | head -n1
)"
if [[ -z "${current_version}" ]]; then
echo "Could not determine the current Cargo version." >&2
exit 1
fi
echo "main_base_version=${MAIN_BASE_VERSION:-$current_version}" >> "$GITHUB_OUTPUT"
echo "develop_base_version=${DEVELOP_BASE_VERSION:-$current_version}" >> "$GITHUB_OUTPUT"
- name: Resolve release context
id: context
shell: bash
run: |
set -euo pipefail
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
echo "target_branch=${{ inputs.target_branch }}" >> "$GITHUB_OUTPUT"
echo "source_branch=workflow_dispatch" >> "$GITHUB_OUTPUT"
echo "release_version=${{ inputs.version }}" >> "$GITHUB_OUTPUT"
echo "merge_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
else
echo "target_branch=${{ github.event.pull_request.base.ref }}" >> "$GITHUB_OUTPUT"
echo "source_branch=${{ github.event.pull_request.head.ref }}" >> "$GITHUB_OUTPUT"
echo "release_version=" >> "$GITHUB_OUTPUT"
echo "merge_sha=${{ github.event.pull_request.merge_commit_sha }}" >> "$GITHUB_OUTPUT"
fi
- name: Plan release
id: plan
env:
TARGET_BRANCH: ${{ steps.context.outputs.target_branch }}
SOURCE_BRANCH: ${{ steps.context.outputs.source_branch }}
RELEASE_VERSION: ${{ steps.context.outputs.release_version }}
MAIN_BASE_VERSION: ${{ steps.bases.outputs.main_base_version }}
DEVELOP_BASE_VERSION: ${{ steps.bases.outputs.develop_base_version }}
run: devops/ga-release-plan.sh
- name: Stop on non-release branches
if: steps.plan.outputs.should_release != 'true'
shell: bash
run: |
echo "Skipping release: ${{ steps.plan.outputs.reason }}"
prepare_release:
needs: plan
if: needs.plan.outputs.should_release == 'true'
runs-on: ubuntu-latest
outputs:
release_sha: ${{ steps.release_commit.outputs.release_sha }}
steps:
- name: Checkout target branch
uses: actions/checkout@v4
with:
ref: ${{ needs.plan.outputs.target_branch }}
fetch-depth: 0
- name: Fetch tags
run: git fetch --force --tags
- name: Verify tag does not already exist
shell: bash
run: |
if git rev-parse "refs/tags/${{ needs.plan.outputs.version }}" >/dev/null 2>&1; then
echo "Tag ${{ needs.plan.outputs.version }} already exists."
exit 1
fi
- name: Ensure release commit includes merged change
if: github.event_name == 'pull_request'
shell: bash
run: |
set -euo pipefail
if ! git merge-base --is-ancestor "${{ needs.plan.outputs.merge_sha }}" HEAD; then
echo "The target branch no longer contains merge commit ${{ needs.plan.outputs.merge_sha }}." >&2
exit 1
fi
- name: Configure git author
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Create release commit
id: release_commit
shell: bash
run: |
set -euo pipefail
version="${{ needs.plan.outputs.version }}"
perl -pi -e 's/^version = "[0-9]+\.[0-9]+\.[0-9]+"/version = "'"$version"'"/' Cargo.toml
perl -0pi -e 's/name = "commitbot"\nversion = "[0-9]+\.[0-9]+\.[0-9]+"/name = "commitbot"\nversion = "'"$version"'"/m' Cargo.lock
if git diff --quiet -- Cargo.toml Cargo.lock; then
echo "Release version ${version} is already present in Cargo metadata." >&2
exit 1
fi
git add Cargo.toml Cargo.lock
git commit -m "Release ${version}"
git tag "${version}"
echo "release_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Push release commit and tag
shell: bash
run: |
set -euo pipefail
git push origin "HEAD:${{ needs.plan.outputs.target_branch }}"
git push origin "refs/tags/${{ needs.plan.outputs.version }}"
create_release:
needs: [plan, prepare_release]
if: needs.plan.outputs.should_release == 'true'
runs-on: ubuntu-latest
steps:
- name: Check out release commit
uses: actions/checkout@v4
with:
ref: ${{ needs.prepare_release.outputs.release_sha }}
- name: Generate release notes
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
NOTES_ARGS=()
if [[ "${{ needs.plan.outputs.previous_version }}" != "${{ needs.plan.outputs.base_version }}" ]]; then
NOTES_ARGS+=(-f previous_tag_name="${{ needs.plan.outputs.previous_version }}")
fi
gh api \
-H "Accept: application/vnd.github+json" \
"/repos/${{ github.repository }}/releases/generate-notes" \
-f tag_name="${{ needs.plan.outputs.version }}" \
-f target_commitish="${{ needs.prepare_release.outputs.release_sha }}" \
"${NOTES_ARGS[@]}" \
--jq .body > generated-release-notes.md
bash devops/render-release-notes.sh \
"${{ needs.plan.outputs.version }}" \
"${{ github.repository }}" \
generated-release-notes.md > release-notes.md
- name: Create GitHub release
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
gh release create "${{ needs.plan.outputs.version }}" \
--draft \
--verify-tag \
--target "${{ needs.prepare_release.outputs.release_sha }}" \
--title "${{ needs.plan.outputs.version }}" \
--notes-file release-notes.md
- name: Dispatch release-assets workflow
id: dispatch_assets
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
dispatch_started_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
source_run_url="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
gh workflow run release-assets.yml \
--ref "${{ needs.plan.outputs.target_branch }}" \
-f tag="${{ needs.plan.outputs.version }}" \
-f source_run_id="${{ github.run_id }}" \
-f source_run_url="${source_run_url}"
assets_run_url=""
for _ in {1..12}; do
sleep 5
assets_run_url="$(
gh api "/repos/${{ github.repository }}/actions/workflows/release-assets.yml/runs?event=workflow_dispatch&branch=${{ needs.plan.outputs.target_branch }}&per_page=20" \
--jq '.workflow_runs
| map(select(.head_sha == "${{ needs.prepare_release.outputs.release_sha }}" and .status != null))
| sort_by(.created_at)
| reverse
| .[0].html_url // ""'
)"
if [[ -n "${assets_run_url}" ]]; then
break
fi
done
echo "dispatch_started_at=${dispatch_started_at}" >> "$GITHUB_OUTPUT"
echo "assets_run_url=${assets_run_url}" >> "$GITHUB_OUTPUT"
- name: Add release summary
shell: bash
run: |
release_url="https://github.com/${{ github.repository }}/releases/tag/${{ needs.plan.outputs.version }}"
assets_url="${{ steps.dispatch_assets.outputs.assets_run_url }}"
{
echo "## Release"
echo
echo "- Version: \`${{ needs.plan.outputs.version }}\`"
echo "- Release: [${{ needs.plan.outputs.version }}](${release_url})"
if [[ -n "${assets_url}" ]]; then
echo "- Release assets workflow: [Open run](${assets_url})"
else
echo "- Release assets workflow: dispatched, but the run URL was not discovered yet."
echo "- Release assets workflow page: [Open workflow](https://github.com/${{ github.repository }}/actions/workflows/release-assets.yml)"
fi
} >> "$GITHUB_STEP_SUMMARY"