Skip to content

security: bump vulnerable dependencies to patched versions #109

security: bump vulnerable dependencies to patched versions

security: bump vulnerable dependencies to patched versions #109

Workflow file for this run

name: Needs Rebase
on:
pull_request_target:
types: [opened, synchronize, reopened]
push:
branches: [main]
permissions:
pull-requests: write
issues: write
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 = '<!-- needs-rebase-bot -->';
// 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}`)
)
));
}
}