From 654d701d7d0b218c8196e3f69f2901b847f44dcb Mon Sep 17 00:00:00 2001 From: Paige Patton <64206430+paigerube14@users.noreply.github.com> Date: Wed, 27 May 2026 12:15:27 -0400 Subject: [PATCH] refactor: move needs-rebase to reusable workflow in krkn-chaos/actions (#1330) Signed-off-by: Paige Patton --- .github/workflows/needs-rebase.yml | 164 +---------------------------- 1 file changed, 2 insertions(+), 162 deletions(-) diff --git a/.github/workflows/needs-rebase.yml b/.github/workflows/needs-rebase.yml index 20f7efb5..c3cc5e1c 100644 --- a/.github/workflows/needs-rebase.yml +++ b/.github/workflows/needs-rebase.yml @@ -12,165 +12,5 @@ permissions: contents: read jobs: - needs-rebase: - runs-on: ubuntu-latest - steps: - - name: Check for merge conflicts and label PR - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const LABEL = 'needs-rebase'; - const COMMENT_MARKER = ''; - - // Poll until GitHub has computed the mergeable status (it's async). - // maxAttempts=1 skips retries for bulk push scans to avoid per-PR sleeps. - // Uses exponential backoff: 5s, 10s, 20s, 30s, 30s, ... (capped at 30s). - async function getMergeableStatus(prNumber, maxAttempts = 10) { - for (let attempt = 0; attempt < maxAttempts; attempt++) { - const { data: pr } = await github.rest.pulls.get({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: prNumber, - }); - if (pr.mergeable !== null) return { mergeable: pr.mergeable, author: pr.user.login }; - if (attempt + 1 < maxAttempts) { - const delay = Math.min(5000 * Math.pow(2, attempt), 30000); - core.info(`PR #${prNumber}: mergeable not computed yet, retrying in ${delay / 1000}s (attempt ${attempt + 1}/${maxAttempts})`); - await new Promise(r => setTimeout(r, delay)); - } - } - if (maxAttempts > 1) { - core.warning(`PR #${prNumber}: could not determine mergeable status after ${maxAttempts} attempts — skipping`); - } else { - core.info(`PR #${prNumber}: mergeable not yet computed by GitHub — skipping (will recheck on next PR event)`); - } - return { mergeable: null, author: null }; - } - - async function findBotComment(prNumber) { - const comments = await github.paginate(github.rest.issues.listComments, { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - }); - return comments.find(c => c.body.includes(COMMENT_MARKER)); - } - - async function hasLabel(prNumber) { - const { data: labels } = await github.rest.issues.listLabelsOnIssue({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - }); - return labels.some(l => l.name === LABEL); - } - - async function handlePR(prNumber, knownAuthor, maxAttempts = 6) { - const { mergeable, author } = await getMergeableStatus(prNumber, maxAttempts); - const prAuthor = knownAuthor || author; - - if (mergeable === null) return; // unknown — leave the PR alone - - const alreadyLabeled = await hasLabel(prNumber); - const existingComment = await findBotComment(prNumber); - - if (!mergeable) { - // PR has conflicts — add label + comment if not already done - if (!alreadyLabeled) { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - labels: [LABEL], - }); - core.info(`PR #${prNumber}: added '${LABEL}' label`); - } - - if (!existingComment) { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - body: [ - COMMENT_MARKER, - `Hi @${prAuthor}! :wave: This PR has merge conflicts with \`main\` and needs to be rebased before it can be merged.`, - ``, - `**To rebase your branch:**`, - `\`\`\`bash`, - `git fetch upstream`, - `git rebase upstream/main`, - `# resolve any conflicts, then:`, - `git add .`, - `git rebase --continue`, - `git push --force-with-lease`, - `\`\`\``, - ``, - `> **Tip:** \`--force-with-lease\` is safer than \`--force\` — it will fail if someone else has pushed to your branch since your last fetch.`, - ``, - `For more details, see the [Rebase with upstream](https://krkn-chaos.dev/docs/contribution-guidelines/git-pointers/#rebase-with-upstream) guide.`, - ``, - `The \`${LABEL}\` label and this comment will be removed automatically once conflicts are resolved.`, - ].join('\n'), - }); - core.info(`PR #${prNumber}: posted needs-rebase comment`); - } - } else { - // PR is clean — remove label and comment - if (alreadyLabeled) { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - name: LABEL, - }); - core.info(`PR #${prNumber}: removed '${LABEL}' label`); - } - - if (existingComment) { - await github.rest.issues.deleteComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existingComment.id, - }); - core.info(`PR #${prNumber}: removed needs-rebase comment`); - } - } - } - - if (context.eventName === 'pull_request_target') { - // A PR was opened or updated — check just this PR - const prNumber = context.payload.pull_request.number; - const author = context.payload.pull_request.user.login; - core.info(`Triggered by PR #${prNumber} (${context.payload.action})`); - try { - await handlePR(prNumber, author); - } catch (e) { - core.warning(`PR #${prNumber}: unexpected error — ${e.message}`); - } - - } else if (context.eventName === 'push') { - // main was pushed to — re-check all open PRs targeting main. - // Use a single read attempt per PR (no retry sleep): mergeable is - // likely null for all PRs right after a push and will be recomputed - // by GitHub asynchronously. PRs with null status are skipped here - // and will be picked up on the next pull_request_target event. - // PRs are processed in concurrent batches to reduce elapsed time. - core.info('Triggered by push to main — checking all open PRs'); - const prs = await github.paginate(github.rest.pulls.list, { - owner: context.repo.owner, - repo: context.repo.repo, - state: 'open', - base: 'main', - }); - core.info(`Found ${prs.length} open PRs targeting main`); - const CONCURRENCY = 5; - for (let i = 0; i < prs.length; i += CONCURRENCY) { - const batch = prs.slice(i, i + CONCURRENCY); - await Promise.all(batch.map(pr => - handlePR(pr.number, pr.user.login, 3).catch(e => - core.warning(`PR #${pr.number}: unexpected error — ${e.message}`) - ) - )); - } - } + rebase: + uses: krkn-chaos/actions/.github/workflows/needs-rebase.yml@main