Skip to content

Promote next to main #10

Promote next to main

Promote next to main #10

Workflow file for this run

name: Promote next to main
# One intuitive button to ship `next` to `main` the RIGHT way: a rebase-merge
# that preserves every conventional commit, so commitlint passes per commit and
# release-please reads the scopes to bump each plugin. A squash here collapses
# the commits into one (often non-conventional) message and breaks both, which
# is exactly the failure this workflow prevents.
#
# Run it from the Actions tab (Run workflow). It creates a linear promotion
# branch from main, opens a PR to main, and enables rebase auto-merge. CI gates
# it, then it merges itself.
on:
workflow_dispatch:
concurrency:
group: promote
cancel-in-progress: false
permissions: {}
jobs:
promote:
name: Open and rebase-merge next into main
runs-on: ubuntu-latest
steps:
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
id: app-token
with:
app-id: ${{ secrets.AIDD_BOT_APP_ID }}
private-key: ${{ secrets.AIDD_BOT_PRIVATE_KEY }}
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
- name: Open or reuse the promote PR, enable rebase auto-merge
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
git config user.name "aidd-bot[bot]"
git config user.email "aidd-bot[bot]@users.noreply.github.com"
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git"
git fetch origin main next --prune
BRANCH="promote/next-to-main-linear"
git switch -C "$BRANCH" origin/main
# A "sync commit" marks a point where main's history was already
# folded into next by back-merge.yml, so it (and everything before
# it) must NOT be replayed onto main again. back-merge.yml lands a
# sync two ways:
# - no conflicts: a direct-pushed real merge commit (2 parents)
# - conflicts: a PR merged by squash (this repo disallows
# merge-commit PRs), whose subject is stamped verbatim from
# back-merge.yml's fixed PR title
# Both must count as boundaries, or a squash sync gets mistaken for
# genuine next-only work and cherry-picked back onto main, colliding
# with content main already has. Keep the title prefix below in
# sync with back-merge.yml's `gh pr create --title`.
is_sync_commit() {
git rev-parse --verify -q "$1^2" >/dev/null 2>&1 && return 0
local subject
subject="$(git log -1 --format=%s "$1")"
[[ "$subject" == "chore: back-merge main into next"* ]]
}
mapfile -t CANDIDATES < <(git rev-list --first-parent --reverse origin/main..origin/next)
SYNCS=()
for c in "${CANDIDATES[@]}"; do
is_sync_commit "$c" && SYNCS+=("$c")
done
if [ "${#SYNCS[@]}" -ge 2 ]; then
START="${SYNCS[$((${#SYNCS[@]} - 2))]}"
else
START="origin/main"
fi
COMMITS=()
while IFS= read -r c; do
is_sync_commit "$c" || COMMITS+=("$c")
done < <(git rev-list --first-parent --reverse "${START}..origin/next")
if [ "${#COMMITS[@]}" -eq 0 ]; then
echo "No commits to promote."
exit 0
fi
git cherry-pick "${COMMITS[@]}"
if [ "$(git rev-parse HEAD^{tree})" != "$(git rev-parse origin/next^{tree})" ]; then
echo "Linear promotion branch does not match origin/next."
git diff --stat HEAD origin/next
exit 1
fi
git push --force-with-lease origin "HEAD:refs/heads/${BRANCH}"
# Reuse an open linear promote PR if one exists, else open one with a
# conventional, squash-safe title.
PR=$(gh pr list --repo "$REPO" --base main --head "$BRANCH" --state open --json number --jq '.[0].number' || true)
if [ -z "$PR" ]; then
PR=$(gh pr create --repo "$REPO" --base main --head "$BRANCH" \
--title "chore: promote next to main" \
--body "Automated linear promotion of \`next\` to \`main\`. Merged by **rebase** to preserve conventional commits for release-please. Do not squash." \
| grep -oE '[0-9]+$')
fi
echo "Promote PR: #$PR"
# Rebase auto-merge: GitHub merges it once CI is green. Never squash.
gh pr merge "$PR" --repo "$REPO" --rebase --auto