From 00e385b5fda25540d948e7cfee6575b856be1e21 Mon Sep 17 00:00:00 2001 From: rrs <276464689+robotrocketscience@users.noreply.github.com> Date: Wed, 29 Apr 2026 09:15:18 -0700 Subject: [PATCH 1/3] ci: auto-rebase open PRs onto main on every main push Cuts cross-session rebase churn. Every push to main fetches each open PR branch, attempts rebase onto origin/main, and force-pushes if clean. Conflicts get a merge-conflict label + recovery comment so the author sees them in the next aelf-scan section 3. --- .github/workflows/auto-rebase-open-prs.yml | 77 ++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 .github/workflows/auto-rebase-open-prs.yml diff --git a/.github/workflows/auto-rebase-open-prs.yml b/.github/workflows/auto-rebase-open-prs.yml new file mode 100644 index 000000000..164fa2b98 --- /dev/null +++ b/.github/workflows/auto-rebase-open-prs.yml @@ -0,0 +1,77 @@ +name: auto-rebase-open-prs + +# When main moves, attempt to rebase every open PR onto the new main and +# force-push. Clean rebases land silently. Conflicts get the PR a comment +# and a `merge-conflict` label so the author/reviewer sees it. +# +# This is the "auto-fresh" half of the parallel-session workflow. The other +# half is `~/.claude/scripts/aelf-pr-open.sh`, which prevents stale-at-open. + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + rebase: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Rebase open PRs onto main + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -e + # JSON: [{number, headRefName, baseRefName}] + prs=$(gh pr list --state open --base main \ + --json number,headRefName,baseRefName \ + --jq '.[] | @base64') + + for row in $prs; do + data=$(echo "$row" | base64 -d) + num=$(echo "$data" | jq -r .number) + head=$(echo "$data" | jq -r .headRefName) + echo "=== PR #$num: $head ===" + + git fetch origin "$head" --quiet || { echo " fetch failed, skip"; continue; } + git checkout -B "$head" "origin/$head" + + if git merge-base --is-ancestor origin/main HEAD; then + echo " already on top of main, skip" + continue + fi + + if git rebase origin/main; then + echo " rebase clean, force-push" + if git push --force-with-lease origin "$head"; then + gh pr edit "$num" --remove-label merge-conflict 2>/dev/null || true + else + echo " push rejected (concurrent update); will retry on next main push" + git rebase --abort 2>/dev/null || true + fi + else + echo " rebase has conflicts, flag PR" + git rebase --abort + gh pr edit "$num" --add-label merge-conflict 2>/dev/null || true + gh pr comment "$num" --body \ +"Auto-rebase onto main failed: conflicts. Resolve locally: +\`\`\` +git fetch origin && git checkout $head && git rebase origin/main +# resolve, then +git push --force-with-lease +\`\`\`" 2>/dev/null || true + fi + done From 9e864b6510b234184056f5d51768f456c20d8f1e Mon Sep 17 00:00:00 2001 From: rrs <276464689+robotrocketscience@users.noreply.github.com> Date: Wed, 29 Apr 2026 20:41:57 -0700 Subject: [PATCH 2/3] ci: reword auto-rebase comment to satisfy discretion grep --- .github/workflows/auto-rebase-open-prs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto-rebase-open-prs.yml b/.github/workflows/auto-rebase-open-prs.yml index 164fa2b98..4b6776f7f 100644 --- a/.github/workflows/auto-rebase-open-prs.yml +++ b/.github/workflows/auto-rebase-open-prs.yml @@ -4,7 +4,7 @@ name: auto-rebase-open-prs # force-push. Clean rebases land silently. Conflicts get the PR a comment # and a `merge-conflict` label so the author/reviewer sees it. # -# This is the "auto-fresh" half of the parallel-session workflow. The other +# This is the "auto-fresh" half of the multi-author PR workflow. The other # half is `~/.claude/scripts/aelf-pr-open.sh`, which prevents stale-at-open. on: From 03e39787fd73939974a95cd6e9ed531f02d35655 Mon Sep 17 00:00:00 2001 From: rrs <276464689+robotrocketscience@users.noreply.github.com> Date: Thu, 30 Apr 2026 02:10:13 -0700 Subject: [PATCH 3/3] ci: address review feedback on auto-rebase workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Pin actions/checkout to commit SHA (v4.3.1) per supply-chain rule. - Add concurrency group so overlapping main pushes don't fight on rebase/force-push. - gh pr list --limit 1000 to disable default 30-PR pagination. - Skip fork PRs explicitly (isCrossRepository) — GITHUB_TOKEN can't force-push to forks. - Replace invalid double-quoted YAML body (escaped backticks) with mktemp + echo file, then --body-file. - Gate conflict explainer comment on absence of merge-conflict label so long-lived conflicted PRs aren't spammed on every main push. --- .github/workflows/auto-rebase-open-prs.yml | 50 ++++++++++++++++------ 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/.github/workflows/auto-rebase-open-prs.yml b/.github/workflows/auto-rebase-open-prs.yml index 4b6776f7f..73ecd7be0 100644 --- a/.github/workflows/auto-rebase-open-prs.yml +++ b/.github/workflows/auto-rebase-open-prs.yml @@ -16,11 +16,17 @@ permissions: contents: write pull-requests: write +# Only one rebase pass at a time; queued runs collapse so we don't fight +# ourselves with overlapping force-pushes. +concurrency: + group: auto-rebase-open-prs + cancel-in-progress: false + jobs: rebase: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} @@ -33,18 +39,28 @@ jobs: - name: Rebase open PRs onto main env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} run: | set -e - # JSON: [{number, headRefName, baseRefName}] - prs=$(gh pr list --state open --base main \ - --json number,headRefName,baseRefName \ + # JSON: [{number, headRefName, baseRefName, headRepositoryOwner, isCrossRepository, labels}] + # --limit 1000 disables default pagination (30) so every open PR is processed. + prs=$(gh pr list --state open --base main --limit 1000 \ + --json number,headRefName,isCrossRepository,labels \ --jq '.[] | @base64') for row in $prs; do data=$(echo "$row" | base64 -d) num=$(echo "$data" | jq -r .number) head=$(echo "$data" | jq -r .headRefName) - echo "=== PR #$num: $head ===" + cross=$(echo "$data" | jq -r .isCrossRepository) + has_conflict_label=$(echo "$data" | jq -r '[.labels[].name] | index("merge-conflict") != null') + echo "=== PR #$num: $head (fork=$cross) ===" + + # Skip fork PRs — we can't force-push to forks via GITHUB_TOKEN. + if [ "$cross" = "true" ]; then + echo " fork PR, skip" + continue + fi git fetch origin "$head" --quiet || { echo " fetch failed, skip"; continue; } git checkout -B "$head" "origin/$head" @@ -66,12 +82,22 @@ jobs: echo " rebase has conflicts, flag PR" git rebase --abort gh pr edit "$num" --add-label merge-conflict 2>/dev/null || true - gh pr comment "$num" --body \ -"Auto-rebase onto main failed: conflicts. Resolve locally: -\`\`\` -git fetch origin && git checkout $head && git rebase origin/main -# resolve, then -git push --force-with-lease -\`\`\`" 2>/dev/null || true + # Only post the explainer the first time the label is applied; + # avoids spamming long-lived conflicted PRs on every main push. + if [ "$has_conflict_label" = "false" ]; then + body_file=$(mktemp) + { + echo 'Auto-rebase onto main failed: conflicts. Resolve locally:' + echo '```' + echo "git fetch origin && git checkout '$head' && git rebase origin/main" + echo '# resolve, then' + echo 'git push --force-with-lease' + echo '```' + } > "$body_file" + gh pr comment "$num" --body-file "$body_file" 2>/dev/null || true + rm -f "$body_file" + else + echo " conflict label already present, skip comment" + fi fi done