From aff3d3a2eb83319c8b9793b4e3305c0583a88659 Mon Sep 17 00:00:00 2001 From: GitHub Copilot Date: Wed, 20 May 2026 02:20:34 +0000 Subject: [PATCH 1/2] feat: add auto-rebase workflow and helper script for all open PRs Agent-Logs-Url: https://github.com/profullstack/sh1pt/sessions/d2e101e7-892a-4da6-b310-bbf135322962 Co-authored-by: ralyodio <27381+ralyodio@users.noreply.github.com> --- .github/workflows/auto-rebase.yml | 198 +++++++++++++++++++++++++++++ scripts/rebase-prs.sh | 201 ++++++++++++++++++++++++++++++ 2 files changed, 399 insertions(+) create mode 100644 .github/workflows/auto-rebase.yml create mode 100755 scripts/rebase-prs.sh diff --git a/.github/workflows/auto-rebase.yml b/.github/workflows/auto-rebase.yml new file mode 100644 index 00000000..850be203 --- /dev/null +++ b/.github/workflows/auto-rebase.yml @@ -0,0 +1,198 @@ +name: Auto Rebase PRs + +on: + push: + branches: [master] + workflow_dispatch: + inputs: + pr_number: + description: 'Specific PR number to rebase (leave empty for all open PRs)' + required: false + default: '' + +jobs: + rebase: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout master + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9.12.0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Configure git + run: | + git config user.email "github-actions[bot]@users.noreply.github.com" + git config user.name "github-actions[bot]" + + - name: Rebase open PRs onto master + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER_INPUT: ${{ inputs.pr_number }} + run: | + set -euo pipefail + + MASTER_SHA=$(git rev-parse HEAD) + echo "Master HEAD: $MASTER_SHA" + + # Determine which PRs to process + if [ -n "${PR_NUMBER_INPUT:-}" ]; then + PR_LIST="${PR_NUMBER_INPUT}" + else + PR_LIST=$(gh pr list --state open --json number --jq '.[].number' | tr '\n' ' ') + fi + + echo "PRs to process: $PR_LIST" + + for PR_NUMBER in $PR_LIST; do + echo "" + echo "==========================================" + echo "Processing PR #$PR_NUMBER" + echo "==========================================" + + # Get PR details + PR_JSON=$(gh pr view "$PR_NUMBER" --json number,headRefName,headRepository,maintainerCanModify,baseRefName 2>/dev/null) || { + echo "Failed to get PR #$PR_NUMBER details, skipping" + continue + } + + HEAD_BRANCH=$(echo "$PR_JSON" | jq -r '.headRefName') + FORK_REPO=$(echo "$PR_JSON" | jq -r '.headRepository.nameWithOwner') + MAINTAINER_CAN_MODIFY=$(echo "$PR_JSON" | jq -r '.maintainerCanModify') + BASE_REF=$(echo "$PR_JSON" | jq -r '.baseRefName') + + echo " Branch: $HEAD_BRANCH" + echo " Fork: $FORK_REPO" + echo " Base: $BASE_REF" + echo " Maintainer can modify: $MAINTAINER_CAN_MODIFY" + + if [ "$MAINTAINER_CAN_MODIFY" != "true" ]; then + echo " Skipping: maintainerCanModify is not enabled" + continue + fi + + if [ "$BASE_REF" != "master" ]; then + echo " Skipping: base branch is '$BASE_REF', not master" + continue + fi + + # Fetch the PR head + git fetch origin "refs/pull/$PR_NUMBER/head:pr-temp-$PR_NUMBER" 2>/dev/null || { + echo " Failed to fetch PR #$PR_NUMBER, skipping" + continue + } + + # Check if rebase is needed + MERGE_BASE=$(git merge-base "pr-temp-$PR_NUMBER" HEAD) + if [ "$MERGE_BASE" = "$MASTER_SHA" ]; then + echo " Already up to date, skipping" + git branch -D "pr-temp-$PR_NUMBER" 2>/dev/null || true + continue + fi + + echo " Merge base: ${MERGE_BASE:0:8} (behind master, rebasing...)" + + # Create a working branch for the rebase + git checkout -b "rebase-work-$PR_NUMBER" "pr-temp-$PR_NUMBER" + + REBASE_SUCCESS=false + CONFLICT_FILES="" + + # Attempt rebase — prefer PR changes on conflict (-X theirs) + if git rebase -X theirs master 2>&1; then + REBASE_SUCCESS=true + echo " Rebase succeeded (auto-resolved conflicts with PR changes)" + else + echo " Rebase failed, checking conflict types..." + CONFLICT_FILES=$(git diff --name-only --diff-filter=U 2>/dev/null || true) + echo " Conflicting files: $CONFLICT_FILES" + + # Resolve known auto-resolvable conflicts + RESOLVED=true + for file in $CONFLICT_FILES; do + case "$file" in + pnpm-lock.yaml) + echo " Resolving pnpm-lock.yaml by regenerating..." + git checkout --theirs pnpm-lock.yaml 2>/dev/null || true + git add pnpm-lock.yaml + ;; + package-lock.json|yarn.lock|bun.lockb) + echo " Resolving lockfile $file by taking PR version..." + git checkout --theirs "$file" 2>/dev/null || true + git add "$file" + ;; + *) + echo " Cannot auto-resolve: $file" + RESOLVED=false + ;; + esac + done + + if $RESOLVED; then + if GIT_EDITOR=true git rebase --continue 2>&1; then + REBASE_SUCCESS=true + else + echo " Rebase --continue failed even after resolving known conflicts" + git rebase --abort 2>/dev/null || true + REBASE_SUCCESS=false + fi + else + echo " Unresolvable conflicts remain, aborting rebase" + git rebase --abort 2>/dev/null || true + fi + fi + + if $REBASE_SUCCESS; then + # Regenerate pnpm-lock.yaml if package.json changed + if git diff master...HEAD --name-only | grep -q "package.json"; then + echo " package.json changed, regenerating pnpm-lock.yaml..." + pnpm install --no-frozen-lockfile 2>/dev/null || true + if ! git diff --quiet pnpm-lock.yaml 2>/dev/null; then + git add pnpm-lock.yaml + git commit --amend --no-edit 2>/dev/null || git commit -m "chore: regenerate pnpm-lock.yaml after rebase" 2>/dev/null || true + fi + fi + + echo " Pushing rebased branch to $FORK_REPO/$HEAD_BRANCH..." + PUSH_URL="https://x-access-token:${GH_TOKEN}@github.com/${FORK_REPO}.git" + if git push "$PUSH_URL" "HEAD:refs/heads/$HEAD_BRANCH" --force-with-lease 2>&1; then + echo " ✅ PR #$PR_NUMBER successfully rebased onto master" + gh pr comment "$PR_NUMBER" \ + --body "🤖 **Auto-rebase:** This branch has been automatically rebased onto \`master\` by the CI bot. No conflicts were detected." \ + 2>/dev/null || true + else + echo " ❌ Push to fork failed for PR #$PR_NUMBER (fork may not allow maintainer pushes)" + gh pr comment "$PR_NUMBER" \ + --body "🤖 **Auto-rebase:** The branch was rebased successfully locally but could not be pushed to the fork. Please enable **'Allow edits from maintainers'** in the PR settings, or rebase manually: \`git fetch upstream master && git rebase upstream/master\`." \ + 2>/dev/null || true + fi + else + echo " ❌ PR #$PR_NUMBER has unresolvable conflicts" + CONFLICT_LIST=$(echo "$CONFLICT_FILES" | tr '\n' ', ' | sed 's/,$//') + gh pr comment "$PR_NUMBER" \ + --body "🤖 **Auto-rebase failed:** This branch has conflicts with \`master\` that cannot be resolved automatically. Conflicting files: \`${CONFLICT_LIST}\`. Please rebase manually: \`git fetch upstream master && git rebase upstream/master\`." \ + 2>/dev/null || true + fi + + # Cleanup + git checkout master 2>/dev/null || git checkout - + git branch -D "rebase-work-$PR_NUMBER" 2>/dev/null || true + git branch -D "pr-temp-$PR_NUMBER" 2>/dev/null || true + done + + echo "" + echo "Done processing all PRs." diff --git a/scripts/rebase-prs.sh b/scripts/rebase-prs.sh new file mode 100755 index 00000000..4f07286e --- /dev/null +++ b/scripts/rebase-prs.sh @@ -0,0 +1,201 @@ +#!/usr/bin/env bash +# scripts/rebase-prs.sh +# +# Manually rebase all open PRs (or a specific PR) onto master. +# +# Usage: +# ./scripts/rebase-prs.sh # rebase all open PRs +# ./scripts/rebase-prs.sh 242 # rebase PR #242 only +# ./scripts/rebase-prs.sh 242 240 237 # rebase specific PRs +# +# Requirements: +# - gh CLI authenticated with write access to profullstack/sh1pt +# - git configured with user.name and user.email +# - pnpm installed (for lockfile regeneration) +# +# The script will: +# 1. Fetch each PR branch +# 2. Rebase onto master, preferring PR changes on conflict (-X theirs) +# 3. Auto-resolve lockfile conflicts by regenerating +# 4. Push back to the fork branch (requires PR author to allow maintainer edits) +# 5. Comment on each PR with the result + +set -euo pipefail + +REPO="profullstack/sh1pt" +BASE_BRANCH="master" + +log() { echo " $*"; } +ok() { echo " ✅ $*"; } +warn() { echo " ⚠️ $*"; } +err() { echo " ❌ $*"; } + +# Ensure we are on master and it is up to date +echo "Fetching latest master..." +git fetch origin master +git checkout master +git merge --ff-only origin/master 2>/dev/null || true +MASTER_SHA=$(git rev-parse HEAD) +echo "Master HEAD: $MASTER_SHA" + +# Configure git identity if not set +if [ -z "$(git config user.email)" ]; then + git config user.email "auto-rebase@sh1pt.local" + git config user.name "Auto Rebase Script" +fi + +# Determine PR list +if [ $# -gt 0 ]; then + PR_LIST="$*" +else + echo "Fetching open PR list from GitHub..." + PR_LIST=$(gh pr list --repo "$REPO" --state open --json number --jq '.[].number' | tr '\n' ' ') +fi + +echo "PRs to process: $PR_LIST" + +REBASED=() +SKIPPED=() +FAILED=() + +for PR_NUMBER in $PR_LIST; do + echo "" + echo "==========================================" + echo "PR #$PR_NUMBER" + echo "==========================================" + + PR_JSON=$(gh pr view "$PR_NUMBER" --repo "$REPO" \ + --json number,headRefName,headRepository,maintainerCanModify,baseRefName 2>/dev/null) || { + err "Failed to get PR #$PR_NUMBER details" + FAILED+=("$PR_NUMBER") + continue + } + + HEAD_BRANCH=$(echo "$PR_JSON" | jq -r '.headRefName') + FORK_REPO=$(echo "$PR_JSON" | jq -r '.headRepository.nameWithOwner') + MAINTAINER_CAN_MODIFY=$(echo "$PR_JSON" | jq -r '.maintainerCanModify') + BASE_REF=$(echo "$PR_JSON" | jq -r '.baseRefName') + + log "Branch: $HEAD_BRANCH" + log "Fork: $FORK_REPO" + log "Maintainer can modify: $MAINTAINER_CAN_MODIFY" + + if [ "$BASE_REF" != "$BASE_BRANCH" ]; then + warn "Base branch is '$BASE_REF', not '$BASE_BRANCH'. Skipping." + SKIPPED+=("$PR_NUMBER") + continue + fi + + if [ "$MAINTAINER_CAN_MODIFY" != "true" ]; then + warn "maintainerCanModify=false. Skipping (ask PR author to enable 'Allow edits from maintainers')." + SKIPPED+=("$PR_NUMBER") + continue + fi + + # Fetch the PR head + git fetch origin "refs/pull/$PR_NUMBER/head:pr-temp-$PR_NUMBER" 2>/dev/null || { + err "Failed to fetch PR #$PR_NUMBER" + FAILED+=("$PR_NUMBER") + continue + } + + # Check if rebase is needed + MERGE_BASE=$(git merge-base "pr-temp-$PR_NUMBER" HEAD) + if [ "$MERGE_BASE" = "$MASTER_SHA" ]; then + ok "Already up to date" + SKIPPED+=("$PR_NUMBER") + git branch -D "pr-temp-$PR_NUMBER" 2>/dev/null || true + continue + fi + + log "Merge base: ${MERGE_BASE:0:8} (behind master, rebasing...)" + + git checkout -b "rebase-work-$PR_NUMBER" "pr-temp-$PR_NUMBER" + + REBASE_SUCCESS=false + CONFLICT_FILES="" + + if git rebase -X theirs "$BASE_BRANCH" 2>&1; then + REBASE_SUCCESS=true + log "Rebase succeeded (conflicts auto-resolved preferring PR changes)" + else + CONFLICT_FILES=$(git diff --name-only --diff-filter=U 2>/dev/null || true) + warn "Rebase hit conflicts: $CONFLICT_FILES" + + RESOLVED=true + for file in $CONFLICT_FILES; do + case "$file" in + pnpm-lock.yaml|package-lock.json|yarn.lock|bun.lockb) + log "Resolving lockfile $file (taking PR version)..." + git checkout --theirs "$file" 2>/dev/null || true + git add "$file" + ;; + *) + warn "Cannot auto-resolve: $file" + RESOLVED=false + ;; + esac + done + + if $RESOLVED; then + if GIT_EDITOR=true git rebase --continue 2>&1; then + REBASE_SUCCESS=true + else + warn "rebase --continue failed" + git rebase --abort 2>/dev/null || true + REBASE_SUCCESS=false + fi + else + git rebase --abort 2>/dev/null || true + fi + fi + + if $REBASE_SUCCESS; then + # Regenerate pnpm-lock.yaml if package.json changed + if git diff "$BASE_BRANCH"...HEAD --name-only 2>/dev/null | grep -q "package.json"; then + log "package.json changed, regenerating pnpm-lock.yaml..." + pnpm install --no-frozen-lockfile 2>/dev/null || true + if ! git diff --quiet pnpm-lock.yaml 2>/dev/null; then + git add pnpm-lock.yaml + git commit --amend --no-edit 2>/dev/null || \ + git commit -m "chore: regenerate pnpm-lock.yaml after rebase" 2>/dev/null || true + fi + fi + + log "Pushing to $FORK_REPO/$HEAD_BRANCH..." + PUSH_URL="https://github.com/${FORK_REPO}.git" + if git push "$PUSH_URL" "HEAD:refs/heads/$HEAD_BRANCH" --force-with-lease 2>&1; then + ok "PR #$PR_NUMBER rebased and pushed successfully" + REBASED+=("$PR_NUMBER") + gh pr comment "$PR_NUMBER" --repo "$REPO" \ + --body "🤖 **Auto-rebase:** This branch has been automatically rebased onto \`master\`. No conflicts." \ + 2>/dev/null || true + else + err "Push to fork failed. Ask the PR author to enable 'Allow edits from maintainers' and retry." + FAILED+=("$PR_NUMBER") + gh pr comment "$PR_NUMBER" --repo "$REPO" \ + --body "🤖 **Auto-rebase:** The branch rebased cleanly locally but could not be pushed to the fork. Please enable **'Allow edits from maintainers'** in the PR settings, then re-run the rebase workflow, or rebase manually: \`git fetch upstream master && git rebase upstream/master\`." \ + 2>/dev/null || true + fi + else + err "PR #$PR_NUMBER has unresolvable conflicts" + FAILED+=("$PR_NUMBER") + CONFLICT_LIST=$(echo "$CONFLICT_FILES" | tr '\n' ',' | sed 's/,$//') + gh pr comment "$PR_NUMBER" --repo "$REPO" \ + --body "🤖 **Auto-rebase failed:** Conflicts that cannot be auto-resolved: \`${CONFLICT_LIST}\`. Please rebase manually: \`git fetch upstream master && git rebase upstream/master\`." \ + 2>/dev/null || true + fi + + # Cleanup temp branches + git checkout master 2>/dev/null || git checkout - + git branch -D "rebase-work-$PR_NUMBER" 2>/dev/null || true + git branch -D "pr-temp-$PR_NUMBER" 2>/dev/null || true +done + +echo "" +echo "==========================================" +echo "Summary" +echo "==========================================" +echo "Rebased: ${REBASED[*]:-none}" +echo "Skipped: ${SKIPPED[*]:-none}" +echo "Failed: ${FAILED[*]:-none}" From 7fe0b0f092cf779656403acfdc9a5599daa22d5b Mon Sep 17 00:00:00 2001 From: GitHub Copilot Date: Wed, 20 May 2026 02:23:04 +0000 Subject: [PATCH 2/2] fix: address code review feedback on auto-rebase workflow and script Agent-Logs-Url: https://github.com/profullstack/sh1pt/sessions/d2e101e7-892a-4da6-b310-bbf135322962 Co-authored-by: ralyodio <27381+ralyodio@users.noreply.github.com> --- .github/workflows/auto-rebase.yml | 14 ++++++++------ scripts/rebase-prs.sh | 6 ++++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/auto-rebase.yml b/.github/workflows/auto-rebase.yml index 850be203..2475e80c 100644 --- a/.github/workflows/auto-rebase.yml +++ b/.github/workflows/auto-rebase.yml @@ -27,17 +27,20 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v4 with: - version: 9.12.0 + run_install: false - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22 - - name: Configure git + - name: Configure git and GitHub auth + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | git config user.email "github-actions[bot]@users.noreply.github.com" git config user.name "github-actions[bot]" + gh auth setup-git - name: Rebase open PRs onto master env: @@ -158,7 +161,7 @@ jobs: if $REBASE_SUCCESS; then # Regenerate pnpm-lock.yaml if package.json changed - if git diff master...HEAD --name-only | grep -q "package.json"; then + if git diff master..HEAD --name-only | grep -q "package.json"; then echo " package.json changed, regenerating pnpm-lock.yaml..." pnpm install --no-frozen-lockfile 2>/dev/null || true if ! git diff --quiet pnpm-lock.yaml 2>/dev/null; then @@ -168,8 +171,7 @@ jobs: fi echo " Pushing rebased branch to $FORK_REPO/$HEAD_BRANCH..." - PUSH_URL="https://x-access-token:${GH_TOKEN}@github.com/${FORK_REPO}.git" - if git push "$PUSH_URL" "HEAD:refs/heads/$HEAD_BRANCH" --force-with-lease 2>&1; then + if git push "https://github.com/${FORK_REPO}.git" "HEAD:refs/heads/$HEAD_BRANCH" --force-with-lease 2>&1; then echo " ✅ PR #$PR_NUMBER successfully rebased onto master" gh pr comment "$PR_NUMBER" \ --body "🤖 **Auto-rebase:** This branch has been automatically rebased onto \`master\` by the CI bot. No conflicts were detected." \ @@ -182,7 +184,7 @@ jobs: fi else echo " ❌ PR #$PR_NUMBER has unresolvable conflicts" - CONFLICT_LIST=$(echo "$CONFLICT_FILES" | tr '\n' ', ' | sed 's/,$//') + CONFLICT_LIST=$(echo "$CONFLICT_FILES" | tr '\n' ',' | sed 's/,$//') gh pr comment "$PR_NUMBER" \ --body "🤖 **Auto-rebase failed:** This branch has conflicts with \`master\` that cannot be resolved automatically. Conflicting files: \`${CONFLICT_LIST}\`. Please rebase manually: \`git fetch upstream master && git rebase upstream/master\`." \ 2>/dev/null || true diff --git a/scripts/rebase-prs.sh b/scripts/rebase-prs.sh index 4f07286e..c55a200a 100755 --- a/scripts/rebase-prs.sh +++ b/scripts/rebase-prs.sh @@ -39,8 +39,10 @@ MASTER_SHA=$(git rev-parse HEAD) echo "Master HEAD: $MASTER_SHA" # Configure git identity if not set -if [ -z "$(git config user.email)" ]; then +if [ -z "$(git config user.email 2>/dev/null)" ]; then git config user.email "auto-rebase@sh1pt.local" +fi +if [ -z "$(git config user.name 2>/dev/null)" ]; then git config user.name "Auto Rebase Script" fi @@ -152,7 +154,7 @@ for PR_NUMBER in $PR_LIST; do if $REBASE_SUCCESS; then # Regenerate pnpm-lock.yaml if package.json changed - if git diff "$BASE_BRANCH"...HEAD --name-only 2>/dev/null | grep -q "package.json"; then + if git diff "$BASE_BRANCH..HEAD" --name-only 2>/dev/null | grep -q "package.json"; then log "package.json changed, regenerating pnpm-lock.yaml..." pnpm install --no-frozen-lockfile 2>/dev/null || true if ! git diff --quiet pnpm-lock.yaml 2>/dev/null; then