automate more of the release process (#5409)

This commit is contained in:
Jack Amadeo
2025-11-05 06:54:42 -05:00
committed by GitHub
parent 89f7384d57
commit c59ec1de07
9 changed files with 457 additions and 0 deletions
@@ -0,0 +1,44 @@
name: 'Generate Release Notes'
description: 'Generate release notes for a given version'
inputs:
version:
description: 'The version being released'
required: true
head_ref:
description: 'The commit SHA or reference of the head of the release branch'
required: true
prior_ref:
description: 'The previous version to compare against'
required: true
outputs:
pr_body_file:
description: 'Path to the generated PR body file'
value: ${{ steps.generate.outputs.pr_body_file }}
runs:
using: 'composite'
steps:
- name: Generate release notes
id: generate
shell: bash
env:
VERSION: ${{ inputs.version }}
HEAD_REF: ${{ inputs.head_ref }}
PRIOR_REF: ${{ inputs.prior_ref }}
TEMPLATE_FILE: "${{ github.action_path }}/pr_body_template.txt"
run: |
git fetch origin --tags
{
sed -e "s/{{VERSION}}/${VERSION}/g" \
-e "s/{{PRIOR_VERSION}}/${PRIOR_REF}/g" \
"$TEMPLATE_FILE"
git log --pretty=format:"- %s (%h)" --reverse ${PRIOR_REF}..${HEAD_REF}
echo ""
echo "---"
echo "*This release PR was generated automatically.*"
} > pr_body.txt
echo "pr_body_file=pr_body.txt" >> $GITHUB_OUTPUT
@@ -0,0 +1,19 @@
# Release v{{VERSION}}
## How to Release
Push the release tag to trigger the release:
```bash
git fetch && git tag v{{VERSION}} origin/release/{{VERSION}}
git push origin v{{VERSION}}
```
This PR will auto-merge once the tag is pushed.
## Important Notes
- All commits in this release should have corresponding cherry-picks in `main`
- This PR can be closed if the release is not needed.
## Changes in This Release
**Comparing:** `{{PRIOR_VERSION}}...v{{VERSION}}`
+57
View File
@@ -0,0 +1,57 @@
name: Check Release PR
on:
pull_request:
types:
- opened
- synchronize
branches:
- main
jobs:
check-commits:
runs-on: ubuntu-latest
if: startsWith(github.head_ref, 'release/')
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
with:
ref: ${{ github.head_ref }}
fetch-depth: 0
- name: Check all PR commits exist in main
run: |
git fetch origin main
CHERRY_OUTPUT=$(git cherry origin/main HEAD)
if [ -z "$CHERRY_OUTPUT" ]; then
echo "✅ All commits already exist in main"
exit 0
fi
echo "Cherry check results:"
echo "$CHERRY_OUTPUT"
MISSING_COMMITS=$(echo "$CHERRY_OUTPUT" | grep '^+' | cut -d' ' -f2)
if [ -z "$MISSING_COMMITS" ]; then
echo "✅ All commits exist in main"
exit 0
fi
COMMIT_COUNT=$(echo "$MISSING_COMMITS" | wc -l)
FIRST_COMMIT=$(git rev-list --reverse HEAD ^origin/main | head -1)
if [ "$COMMIT_COUNT" -eq 1 ] && echo "$MISSING_COMMITS" | grep -q "$FIRST_COMMIT"; then
echo "✅ Only version bump commit is unique"
git log --oneline -1 "$FIRST_COMMIT"
else
echo "❌ Found commits that should exist in main:"
for commit in $MISSING_COMMITS; do
if [ "$commit" != "$FIRST_COMMIT" ]; then
git log --oneline -1 "$commit"
fi
done
echo "Make sure commits have equivalents in main. If you've since updated main, re-run this job"
exit 1
fi
+82
View File
@@ -0,0 +1,82 @@
name: Release Workflow
on:
workflow_call:
inputs:
bump_type:
description: 'Type of version bump (minor or patch)'
required: true
type: string
target_branch:
description: 'Target branch for the pull request'
required: false
type: string
default: 'main'
permissions:
contents: write
pull-requests: write
jobs:
create-release:
runs-on: ubuntu-latest
env:
BUMP_TYPE: ${{ inputs.bump_type }}
TARGET_BRANCH: ${{ inputs.target_branch }}
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
with:
ref: ${{ inputs.target_branch }}
fetch-depth: 0 # to generate complete release log
- uses: cashapp/activate-hermit@e49f5cb4dd64ff0b0b659d1d8df499595451155a # v1
- name: Validate input and set old version
run: |
if [[ "$BUMP_TYPE" != "minor" && "$BUMP_TYPE" != "patch" ]]; then
echo "Error: bump_type must be 'minor' or 'patch'"
exit 1
fi
- name: create release branch
run: |
PRIOR_VERSION=$(just get-tag-version)
if [[ "$BUMP_TYPE" == "minor" ]]; then
VERSION=$(just get-next-minor-version)
else
VERSION=$(just get-next-patch-version)
fi
echo "prior_ref=v$PRIOR_VERSION" >> $GITHUB_ENV
echo "version=$VERSION" >> $GITHUB_ENV
echo "Version: $VERSION"
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
just prepare-release $VERSION
BRANCH_NAME=$(git branch --show-current)
echo "branch_name=$BRANCH_NAME" >> $GITHUB_ENV
echo "Branch: $BRANCH_NAME"
- name: push release branch
run: |
git push origin "${{ env.branch_name }}"
- name: Generate release notes
uses: ./.github/actions/generate-release-pr-body
with:
version: ${{ env.version }}
head_ref: ${{ github.event.pull_request.head.sha || github.sha }}
prior_ref: ${{ env.prior_ref }}
- name: Create Pull Request
run: |
gh pr create \
-B "$TARGET_BRANCH" \
-H "${{ env.branch_name }}" \
--title "chore(release): release version ${{ env.version }} ($BUMP_TYPE)" \
--body-file pr_body.txt
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,148 @@
name: Merge release PR on tag push
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
permissions:
actions: write
contents: write
pull-requests: write
checks: read
jobs:
trigger-patch-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- name: Extract version from tag
id: version
env:
TAG: ${{ github.ref_name }}
run: |
VERSION=${TAG#v}
BRANCH="release/${VERSION}"
echo "tag=${TAG}" >> $GITHUB_OUTPUT
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "branch=${BRANCH}" >> $GITHUB_OUTPUT
echo "Tag: ${TAG}"
echo "Version: ${VERSION}"
echo "Expected branch: ${BRANCH}"
- name: Find matching PR
id: find_pr
env:
GH_TOKEN: ${{ github.token }}
BRANCH: ${{ steps.version.outputs.branch }}
run: |
PR_NUMBER=$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number // empty')
if [ -z "$PR_NUMBER" ]; then
echo "❌ No open PR found for branch: $BRANCH"
echo "pr_found=false" >> $GITHUB_OUTPUT
exit 1
else
echo "✅ Found PR #$PR_NUMBER for branch: $BRANCH"
echo "pr_found=true" >> $GITHUB_OUTPUT
echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
fi
- name: Get PR details and check status
if: steps.find_pr.outputs.pr_found == 'true'
id: pr_status
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ steps.find_pr.outputs.pr_number }}
run: |
PR_DATA=$(gh pr view $PR_NUMBER --json title,headRefName,baseRefName,mergeable,statusCheckRollup)
TITLE=$(echo "$PR_DATA" | jq -r '.title')
HEAD_BRANCH=$(echo "$PR_DATA" | jq -r '.headRefName')
BASE_BRANCH=$(echo "$PR_DATA" | jq -r '.baseRefName')
MERGEABLE=$(echo "$PR_DATA" | jq -r '.mergeable')
echo "PR Title: $TITLE"
echo "Head Branch: $HEAD_BRANCH"
echo "Base Branch: $BASE_BRANCH"
echo "Mergeable: $MERGEABLE"
if [ "$MERGEABLE" != "MERGEABLE" ]; then
echo "❌ PR is not in a mergeable state: $MERGEABLE"
echo "can_merge=false" >> $GITHUB_OUTPUT
echo "merge_reason=PR is not mergeable (state: $MERGEABLE)" >> $GITHUB_OUTPUT
exit 0
fi
STATUS_CHECKS=$(echo "$PR_DATA" | jq -r '.statusCheckRollup[]? | select(.conclusion != null) | "\(.context): \(.conclusion)"')
FAILED_CHECKS=$(echo "$PR_DATA" | jq -r '.statusCheckRollup[]? | select(.conclusion == "FAILURE" or .conclusion == "CANCELLED" or .conclusion == "TIMED_OUT") | .context')
PENDING_CHECKS=$(echo "$PR_DATA" | jq -r '.statusCheckRollup[]? | select(.conclusion == null) | .context')
echo "Status checks:"
if [ -n "$STATUS_CHECKS" ]; then
echo "$STATUS_CHECKS"
else
echo "No status checks found"
fi
if [ -n "$FAILED_CHECKS" ]; then
echo "❌ Failed checks found:"
echo "$FAILED_CHECKS"
echo "can_merge=false" >> $GITHUB_OUTPUT
echo "merge_reason=Some checks are failing" >> $GITHUB_OUTPUT
exit 0
fi
if [ -n "$PENDING_CHECKS" ]; then
echo "⏳ Pending checks found:"
echo "$PENDING_CHECKS"
echo "can_merge=false" >> $GITHUB_OUTPUT
echo "merge_reason=Some checks are still pending" >> $GITHUB_OUTPUT
exit 0
fi
echo "✅ All checks are passing and PR is ready to merge"
echo "can_merge=true" >> $GITHUB_OUTPUT
- name: Get branch SHA before merge
if: steps.find_pr.outputs.pr_found == 'true' && steps.pr_status.outputs.can_merge == 'true'
id: branch_info
env:
BRANCH: ${{ steps.version.outputs.branch }}
run: |
git fetch origin
BRANCH_SHA=$(git rev-parse "origin/$BRANCH")
echo "branch_sha=$BRANCH_SHA" >> $GITHUB_OUTPUT
echo "Branch SHA: $BRANCH_SHA"
- name: Merge PR
if: steps.find_pr.outputs.pr_found == 'true' && steps.pr_status.outputs.can_merge == 'true'
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ steps.find_pr.outputs.pr_number }}
VERSION: ${{ steps.version.outputs.version }}
TAG: ${{ steps.version.outputs.tag }}
run: |
gh pr merge $PR_NUMBER --squash --delete-branch --subject "Release $VERSION" --body "Auto-merged release PR after tag $TAG was pushed"
- name: Restore branch
if: steps.find_pr.outputs.pr_found == 'true' && steps.pr_status.outputs.can_merge == 'true'
env:
BRANCH: ${{ steps.version.outputs.branch }}
BRANCH_SHA: ${{ steps.branch_info.outputs.branch_sha }}
run: |
git checkout -b "$BRANCH" "$BRANCH_SHA"
git push origin "$BRANCH"
- name: Trigger patch release
env:
BRANCH: ${{ steps.version.outputs.branch }}
GH_TOKEN: ${{ github.token }}
run: |
gh workflow run patch-release.yaml \
--field target_branch=$BRANCH
+16
View File
@@ -0,0 +1,16 @@
name: Create Minor Release PR
permissions:
contents: write
pull-requests: write
on:
schedule:
- cron: '0 0 * * 2'
workflow_dispatch:
jobs:
release:
uses: ./.github/workflows/create-release-pr.yaml
with:
bump_type: "minor"
+20
View File
@@ -0,0 +1,20 @@
name: Create Patch Release PR
permissions:
contents: write
pull-requests: write
on:
workflow_dispatch:
inputs:
target_branch:
description: 'Target branch for hotfix'
required: true
type: string
jobs:
hotfix:
uses: ./.github/workflows/create-release-pr.yaml
with:
bump_type: "patch"
target_branch: ${{ inputs.target_branch }}
+65
View File
@@ -0,0 +1,65 @@
name: Update Release Notes
on:
pull_request:
types:
- synchronize
branches:
- main
- release/**
permissions:
contents: read
pull-requests: write
jobs:
update-release-notes:
runs-on: ubuntu-latest
if: startsWith(github.event.pull_request.head.ref, 'release/') && github.event.pull_request.user.login == 'github-actions[bot]'
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
with:
fetch-depth: 0 # to generate complete release log
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
with:
ref: ${{ github.base_ref }}
path: './prior-version'
- uses: cashapp/activate-hermit@e49f5cb4dd64ff0b0b659d1d8df499595451155a # v1
- name: Extract version from branch name
env:
REF_NAME: ${{ github.head_ref }}
run: |
BRANCH_NAME="$REF_NAME"
VERSION=$(echo "$BRANCH_NAME" | sed 's/release\///')
echo "version=$VERSION" >> $GITHUB_ENV
echo "Version: $VERSION"
- name: Get prior version
working-directory: './prior-version'
run: |
PRIOR_VERSION=$(just get-tag-version)
echo "prior_ref=v$PRIOR_VERSION" >> $GITHUB_ENV
- name: Generate release notes
uses: ./.github/actions/generate-release-pr-body
with:
version: ${{ env.version }}
head_ref: ${{ github.event.pull_request.head.sha }}
prior_ref: ${{ env.prior_ref }}
- name: Update Pull Request
env:
REF_NAME: ${{ github.head_ref }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR_NUMBER=$(gh pr list --head "$REF_NAME" --json number --jq '.[0].number')
if [[ -z "$PR_NUMBER" || "$PR_NUMBER" == "null" ]]; then
echo "No PR found for branch $REF_NAME"
exit 1
fi
gh pr edit "$PR_NUMBER" --body-file pr_body.txt
+6
View File
@@ -268,6 +268,12 @@ validate version:
exit 1
fi
get-next-minor-version:
@python -c "import sys; v=sys.argv[1].split('.'); print(f'{v[0]}.{int(v[1])+1}.0')" $(just get-tag-version)
get-next-patch-version:
@python -c "import sys; v=sys.argv[1].split('.'); print(f'{v[0]}.{v[1]}.{int(v[2])+1}')" $(just get-tag-version)
# set cargo and app versions, must be semver
prepare-release version:
@just validate {{ version }} || exit 1