fix(ci): replace rebase sync with batched merge PR, add auto-retarget (#8957)

This commit is contained in:
Gustavo Valverde
2026-04-04 18:42:07 +01:00
committed by GitHub
parent acc0d1c7ec
commit b79c045927
6 changed files with 121 additions and 38 deletions

View File

@@ -52,3 +52,5 @@ moonshotai
kimi
ilike
nvmrc
retarget
retargeted

74
.github/workflows/auto-retarget.yml vendored Normal file
View File

@@ -0,0 +1,74 @@
name: Auto-retarget
on:
pull_request_target:
types: [opened, synchronize, reopened]
branches: [main]
permissions: {}
jobs:
retarget:
if: >
github.repository_owner == 'better-auth' &&
!startsWith(github.event.pull_request.head.ref, 'changeset-release/') &&
github.actor != 'github-actions[bot]' &&
github.actor != 'better-auth-releases[bot]'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Detect max bump type from changesets
id: bump
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
MAX="patch"
FILES=$(gh pr diff "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --name-only | grep '^\.changeset/.*\.md$' | grep -v 'README.md' || true)
if [ -z "$FILES" ]; then
echo "No changeset files in PR"
echo "max=none" >> "$GITHUB_OUTPUT"
exit 0
fi
while IFS= read -r f; do
[ -z "$f" ] && continue
CONTENT=$(gh api "repos/$GITHUB_REPOSITORY/contents/$f?ref=${{ github.event.pull_request.head.sha }}" --jq '.content' | base64 -d 2>/dev/null || true)
[ -z "$CONTENT" ] && continue
BUMPS=$(echo "$CONTENT" | sed -n '/^---$/,/^---$/p' | grep -oE '"[^"]+"\s*:\s*(major|minor|patch)' | grep -oE '(major|minor|patch)$' | sort -u)
if echo "$BUMPS" | grep -q "major"; then
MAX="major"
break
fi
if echo "$BUMPS" | grep -q "minor"; then
MAX="minor"
fi
done <<< "$FILES"
echo "max=$MAX" >> "$GITHUB_OUTPUT"
- name: Check next branch exists
if: steps.bump.outputs.max == 'minor' || steps.bump.outputs.max == 'major'
id: next-check
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if gh api "repos/$GITHUB_REPOSITORY/branches/next" --silent 2>/dev/null; then
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
echo "::warning::next branch does not exist. Cannot retarget."
fi
- name: Retarget PR to next
if: (steps.bump.outputs.max == 'minor' || steps.bump.outputs.max == 'major') && steps.next-check.outputs.exists == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
MAX_BUMP: ${{ steps.bump.outputs.max }}
run: |
gh pr edit "$PR_NUMBER" --base next --repo "$GITHUB_REPOSITORY"
gh pr edit "$PR_NUMBER" --add-label "retargeted-to-next" --repo "$GITHUB_REPOSITORY" || true
gh pr comment "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --body \
"This PR was automatically retargeted from \`main\` to \`next\` because it contains a **${MAX_BUMP}** changeset. The \`main\` branch only accepts \`patch\` (bug fix) changes. Features and breaking changes go through \`next\` for beta testing before promotion to stable."

View File

@@ -67,7 +67,7 @@ jobs:
echo "No changes to commit (previous run already pushed)"
else
git commit -m "chore: exit pre-release mode for v${VERSION}"
git push origin next
git push origin next || { echo "::error::Push to next failed. Check branch protection rules."; exit 1; }
fi
- name: Create promote PR
@@ -83,14 +83,22 @@ jobs:
--base main \
--head next \
--title "chore: promote v${VERSION} to stable" \
--body "Promotes beta to stable. **Use 'Rebase and merge'** to preserve individual commits. Merging publishes v${VERSION} to npm with the \`latest\` tag."
--body "Promotes v${VERSION} from beta to stable. Merging this PR publishes all packages to npm with the \`latest\` tag.\n\n**This PR must be merged by an admin using 'Create a merge commit'** (not squash, not rebase). The admin bypass is required because main enforces linear history for regular PRs, but the promotion needs a merge commit to preserve individual contributor commits and their verified signatures."
fi
- name: Summary
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
echo "### Beta promoted" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "- **Version:** \`$VERSION\`" >> "$GITHUB_STEP_SUMMARY"
echo "- **Action:** Review and merge the PR to publish as stable." >> "$GITHUB_STEP_SUMMARY"
{
echo "### Beta promoted"
echo ""
echo "- **Version:** \`$VERSION\`"
echo "- **Action:** Review and merge the PR with **Create a merge commit** (admin bypass)."
echo ""
echo "### After promotion"
echo "Once the promote PR is merged and the sync PR (main to next) is merged:"
echo '```'
echo "pnpm changeset pre enter beta && git add .changeset/pre.json && git commit -m 'chore: re-enter beta pre-release mode' && git push origin next"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"

View File

@@ -103,15 +103,11 @@ jobs:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }}
NPM_CONFIG_TAG: ${{ steps.npm-tag.outputs.tag }}
- name: Rebase next onto main
- name: Sync main to next via PR
if: github.ref_name == 'main'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }}
run: |
git config user.name "better-auth-releases[bot]"
git config user.email "273320539+better-auth-releases[bot]@users.noreply.github.com"
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
git fetch origin next || { echo "next branch does not exist, skipping"; exit 0; }
AHEAD=$(git rev-list --count origin/next..origin/main)
@@ -121,40 +117,40 @@ jobs:
fi
echo "Found $AHEAD commit(s) to sync from main to next"
# Capture next's SHA for force-with-lease (detached HEAD has no tracking ref)
NEXT_SHA=$(git rev-parse origin/next)
git checkout origin/next
if git rebase origin/main; then
echo "Clean rebase"
EXISTING=$(gh pr list --base next --head "${GITHUB_REPOSITORY_OWNER}:main" --state open --repo "$GITHUB_REPOSITORY" --json number --jq '.[0].number // empty')
if [ -n "$EXISTING" ]; then
echo "Sync PR #$EXISTING already exists ($AHEAD commits pending)"
else
echo "::warning::Rebase conflicts — creating draft PR for manual resolution"
git rebase --abort
SYNC_BODY="Brings stable fixes from main into the next branch."
SYNC_BODY="$SYNC_BODY\n\n**This PR must be merged by a maintainer using 'Create a merge commit'** (not squash, not rebase). This preserves individual fix commits and their verified signatures."
SYNC_BODY="$SYNC_BODY\n\nIf there are conflicts, resolve them by keeping next's versions for \`package.json\` files (next is always ahead of main in version numbers)."
gh pr create \
--base next \
--head main \
--title "chore: sync main to next (conflicts)" \
--body "Automated rebase failed due to code conflicts. Resolve manually by rebasing next onto main." \
--draft \
|| true
exit 0
--repo "$GITHUB_REPOSITORY" \
--title "chore: sync main to next" \
--body "$(echo -e "$SYNC_BODY")" || true
EXISTING=$(gh pr list --base next --head "${GITHUB_REPOSITORY_OWNER}:main" --state open --repo "$GITHUB_REPOSITORY" --json number --jq '.[0].number // empty')
fi
# Re-enter pre-release mode if needed (after a promote cycle),
# but only if there's no open promote PR (to avoid invalidating it)
if [ ! -f .changeset/pre.json ]; then
OPEN_PROMOTE=$(gh pr list --base main --head next --state open --json number --jq 'length')
if [ "$OPEN_PROMOTE" -eq 0 ]; then
pnpm changeset pre enter beta
git add .changeset/pre.json
git commit -m "chore: re-enter beta pre-release mode"
else
echo "Promote PR is open, skipping pre-mode re-entry"
# Check for merge conflicts (GitHub computes merge state asynchronously)
if [ -n "$EXISTING" ]; then
sleep 10
for _ in 1 2 3 4 5; do
MERGEABLE=$(gh pr view "$EXISTING" --repo "$GITHUB_REPOSITORY" --json mergeable --jq '.mergeable')
[ "$MERGEABLE" != "UNKNOWN" ] && break
sleep 10
done
if [ "$MERGEABLE" = "CONFLICTING" ]; then
gh pr edit "$EXISTING" --repo "$GITHUB_REPOSITORY" --add-label "has-conflicts" || true
echo "::warning::Sync PR #$EXISTING has merge conflicts. Resolve by keeping next's versions for package.json files."
elif [ "$MERGEABLE" = "MERGEABLE" ]; then
gh pr edit "$EXISTING" --repo "$GITHUB_REPOSITORY" --remove-label "has-conflicts" 2>/dev/null || true
fi
fi
git push origin HEAD:refs/heads/next --force-with-lease=refs/heads/next:"$NEXT_SHA"
snapshot:
if: github.event_name == 'workflow_dispatch' && inputs.snapshot != '' && github.repository_owner == 'better-auth'
runs-on: ubuntu-latest

View File

@@ -127,11 +127,11 @@ jobs:
for f in $CHANGESET_FILES; do
BUMPS=$(sed -n '/^---$/,/^---$/p' "$f" | grep -oE '(major|minor|patch)' | sort -u)
if echo "$BUMPS" | grep -q "major"; then
echo "::error::Changeset $f contains a 'major' bump. Only 'patch' bumps are allowed on '$BASE_REF'."
echo "::error::Changeset $f contains a 'major' bump. Only 'patch' bumps are allowed on '$BASE_REF'. PRs with minor/major bumps should target 'next' (auto-retarget may not have run yet)."
exit 1
fi
if echo "$BUMPS" | grep -q "minor"; then
echo "::error::Changeset $f contains a 'minor' bump. Only 'patch' bumps are allowed on '$BASE_REF'."
echo "::error::Changeset $f contains a 'minor' bump. Only 'patch' bumps are allowed on '$BASE_REF'. PRs with minor/major bumps should target 'next' (auto-retarget may not have run yet)."
exit 1
fi
done

3
.github/zizmor.yml vendored
View File

@@ -5,6 +5,9 @@ rules:
# never checks out fork code. The pull_request_target trigger is required
# for the action to post/delete PR comments.
- semantic-pull-request.yml
# Intentional: reads .changeset/*.md content via GitHub API only.
# No checkout of fork code. No code execution.
- auto-retarget.yml
cache-poisoning:
ignore:
# False positive: neither setup-node call has caching enabled (no `cache:` param).