Repository navigation
Promote next to main #10
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |