Skip to content

Prepare Release PR

Prepare Release PR #36

---
name: Prepare Release PR
on:
schedule:
- cron: '0 20 * * 0,3'
workflow_dispatch:
inputs:
force-release:
description: Force a release even when no open release PR exists
required: false
default: false
type: boolean
permissions:
contents: read
concurrency:
group: release
cancel-in-progress: false
jobs:
prepare-release:
name: Prepare Release PR
runs-on: ubuntu-latest
steps:
- id: get-app-token
name: Get GitHub App Installation Token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ secrets.APPLICATION_ID }}
private-key: ${{ secrets.APPLICATION_PRIVATE_KEY }}
owner: ${{ github.repository_owner}}
- id: discover-release-pr
name: Discover existing release PR
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ steps.get-app-token.outputs.token }}
run: |
PR_NUMBER=$(gh pr list --head next --base release --state open --json number --jq '.[0].number // empty')
echo "pr-number=${PR_NUMBER}" >> "$GITHUB_OUTPUT"
if [ -n "$PR_NUMBER" ]; then
echo 'has-open-pr=true' >> "$GITHUB_OUTPUT"
else
echo 'has-open-pr=false' >> "$GITHUB_OUTPUT"
fi
- id: git-user
name: Get GitHub App User ID and Setup Git user
env:
GH_TOKEN: ${{ steps.get-app-token.outputs.token }}
run: |
name="${{ steps.get-app-token.outputs.app-slug }}[bot]"
user_id=$(gh api "/users/${name}" --jq .id)
email="${user_id}+${name}@users.noreply.github.com"
echo "user-email=${email}" >> "$GITHUB_OUTPUT"
echo "user-name=${name}" >> "$GITHUB_OUTPUT"
git config --global user.email "${email}"
git config --global user.name "${name}"
- name: Checkout `release` branch
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
filter: blob:none
ref: release
show-progress: false
token: ${{ steps.get-app-token.outputs.token }}
- name: Reset to last release tag and merge `main`
run: |
set -euo pipefail
last_tag=$(git tag --list 'v*' --sort=-version:refname | { grep -vF '+harness' || true; } | head -1)
echo "Resetting to ${last_tag}"
git reset --hard "${last_tag}"
git fetch origin main
if ! git merge --no-ff -Xtheirs -m 'skip: merge main [skip release]' origin/main; then
# Typical trigger: rename/rename on dist/artifact-*.js (the bundle hash
# changes every commit on main). -Xtheirs handles content conflicts but
# cannot resolve rename/rename. Bias fully to main's tree on conflict —
# the release branch's purpose is to mirror main.
echo "Merge had unresolvable conflicts; taking main's tree verbatim."
git merge --abort
git checkout origin/main -- .
git add -A
git commit --no-verify -m 'skip: merge main [skip release] (conflict-resolved)'
fi
- name: Setup Node.js and Bun
uses: ./.github/actions/setup
- id: release-preview
name: Release Preview
run: node --experimental-strip-types --experimental-transform-types scripts/release/preview-next-release.ts --to HEAD
- if: steps.release-preview.outputs.next_version != ''
name: Push `next` branch
run: git push --no-verify --force-with-lease origin HEAD:next
- if: steps.release-preview.outputs.next_version != ''
name: Generate PR body
run: |
VERSION="${{ steps.release-preview.outputs.next_version }}"
last_tag=$(git tag --list 'v*' --sort=-version:refname | { grep -vF '+harness' || true; } | head -1)
COMMIT_LIST=$(git log "${last_tag}..HEAD" --format='* %s (%h)' --no-merges --max-count=50)
TIMESTAMP=$(date -u +"%Y-%m-%d %H:%M UTC")
cat > /tmp/pr-body.md <<EOF
## Pending Release: v${VERSION}
**Merge this PR with a merge commit to trigger a release.**
### Commits Since Last Release
EOF
echo "${COMMIT_LIST:-No new commits}" >> /tmp/pr-body.md
echo "" >> /tmp/pr-body.md
echo "---" >> /tmp/pr-body.md
echo "*Auto-generated by the release pipeline. Updated: ${TIMESTAMP}*" >> /tmp/pr-body.md
- if: steps.release-preview.outputs.next_version != ''
env:
GH_TOKEN: ${{ steps.get-app-token.outputs.token }}
id: upsert-release-pr
name: Create or Update Release PR
run: |
PR_NUMBER="${{ steps.discover-release-pr.outputs.pr-number }}"
VERSION="${{ steps.release-preview.outputs.next_version }}"
TITLE="chore(release): pending release v${VERSION}"
if [ -n "$PR_NUMBER" ]; then
gh pr edit "$PR_NUMBER" --title "$TITLE" --body-file /tmp/pr-body.md
else
gh pr create --head next --base release --title "$TITLE" --body-file /tmp/pr-body.md
PR_NUMBER=$(gh pr list --head next --base release --state open --json number --jq '.[0].number // empty')
fi
echo "pr-number=${PR_NUMBER}" >> "$GITHUB_OUTPUT"
- if: steps.release-preview.outputs.next_version != ''
env:
GH_TOKEN: ${{ steps.get-app-token.outputs.token }}
PR_NUMBER: ${{ steps.upsert-release-pr.outputs.pr-number }}
name: Wait for release PR mergeability
run: |
if [ -z "$PR_NUMBER" ]; then
echo 'Missing release PR number after upsert'
exit 1
fi
attempt=0
while [ "$attempt" -lt 10 ]; do
MERGE_STATE=$(gh pr view "$PR_NUMBER" --json mergeStateStatus --jq '.mergeStateStatus')
if [ "$MERGE_STATE" = 'DIRTY' ] || [ "$MERGE_STATE" = 'BLOCKED' ]; then
echo "Release PR is not mergeable: ${MERGE_STATE}"
exit 1
fi
if [ "$MERGE_STATE" = 'CLEAN' ] || [ "$MERGE_STATE" = 'HAS_HOOKS' ] || [ "$MERGE_STATE" = 'UNSTABLE' ]; then
exit 0
fi
sleep 2
attempt=$((attempt + 1))
done
echo 'Timed out waiting for release PR mergeability'
exit 1
- if: steps.release-preview.outputs.next_version == '' && steps.discover-release-pr.outputs.has-open-pr == 'true'
env:
GH_TOKEN: ${{ steps.get-app-token.outputs.token }}
PR_NUMBER: ${{ steps.discover-release-pr.outputs.pr-number }}
name: Close stale pending release PR
run: gh pr close "$PR_NUMBER" --comment "Closing stale pending release PR because the latest preview predicts no release."
- if: steps.release-preview.outputs.next_version == ''
name: Delete stale `next` branch
run: git push --no-verify origin --delete next || true
- id: merge-policy
if: steps.release-preview.outputs.next_version != ''
name: Decide whether to merge release PR now
run: |
SHOULD_MERGE=false
if [ "${{ github.event_name }}" = 'schedule' ]; then
SHOULD_MERGE=true
fi
if [ "${{ github.event_name }}" = 'workflow_dispatch' ] && [ "${{ github.event.inputs.force-release }}" = 'true' ]; then
SHOULD_MERGE=true
fi
if [ "${{ github.event_name }}" = 'workflow_dispatch' ] && [ "${{ steps.discover-release-pr.outputs.has-open-pr }}" = 'true' ]; then
SHOULD_MERGE=true
fi
echo "should-merge=${SHOULD_MERGE}" >> "$GITHUB_OUTPUT"
- if: steps.release-preview.outputs.next_version != '' && steps.merge-policy.outputs.should-merge == 'true'
env:
GH_TOKEN: ${{ steps.get-app-token.outputs.token }}
PR_NUMBER: ${{ steps.upsert-release-pr.outputs.pr-number }}
name: Merge release PR with merge commit
run: |
if [ -n "$PR_NUMBER" ]; then
gh pr merge "$PR_NUMBER" --merge --delete-branch=false
fi