Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a guard job to prevent accidental auto-merging of PRs when cross-version tests fail #10210

Merged
merged 20 commits into from Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
95 changes: 95 additions & 0 deletions .github/workflows/auto-merge.js
harupy marked this conversation as resolved.
Show resolved Hide resolved
@@ -0,0 +1,95 @@
module.exports = async ({ github, context }) => {
const {
repo: { owner, repo },
} = context;

const MERGE_INTERVAL_MS = 5000; // 5 seconds pause after a merge
const MAX_RETRIES = 3;
const RETRY_INTERVAL_MS = 10000; // 10 seconds

async function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

async function logRateLimit() {
const { data: rateLimit } = await github.rest.rateLimit.get();
console.log(`Rate limit remaining: ${rateLimit.resources.core.remaining}`);
console.log(
`Rate limit resets at: ${new Date(rateLimit.resources.core.reset * 1000).toISOString()}`
);
}

async function waitUntilMergeable(prNumber) {
for (let i = 0; i < MAX_RETRIES; i++) {
const pullRequest = await github.rest.pulls
.get({
owner,
repo,
pull_number: prNumber,
})
.then((res) => res.data);

if (pullRequest.mergeable !== null) {
return pullRequest.mergeable;
}

console.log(`Waiting for mergeability calculation for PR #${prNumber}...`);
await sleep(RETRY_INTERVAL_MS);
}
return false;
}

async function areAllChecksPassed(sha) {
const { data: checkRuns } = await github.rest.checks.listForRef({
owner,
repo,
ref: sha,
});
return checkRuns.check_runs.every(({ conclusion }) =>
["success", "skipped"].includes(conclusion)
);
}

// Get date from a month ago in ISO format
const oneMonthAgo = new Date();
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
const sinceDate = oneMonthAgo.toISOString();

// List PRs with the "automerge" label created within the last month
const { data: issues } = await github.rest.issues.listForRepo({
owner,
repo,
labels: "automerge",
since: sinceDate,
});

// Filter for pull requests from the list of issues
const pullRequests = issues.filter((issue) => issue.pull_request);

for (const pr of pullRequests) {
if (!(await waitUntilMergeable(pr.number))) {
console.log(`PR #${pr.number} is not mergeable. Skipping...`);
await logRateLimit();
continue;
}

if (await areAllChecksPassed(pullRequest.head.sha)) {
try {
await github.rest.pulls.merge({
owner,
repo,
pull_number: pr.number,
});
console.log(`Merged PR #${pr.number}`);

await sleep(MERGE_INTERVAL_MS);
await logRateLimit();
} catch (error) {
console.log(`Failed to merge PR #${pr.number}. Reason: ${error.message}`);
}
} else {
console.log(`Checks not ready for PR #${pr.number}. Skipping merge.`);
await logRateLimit();
}
}
};
20 changes: 20 additions & 0 deletions .github/workflows/auto-merge.yml
@@ -0,0 +1,20 @@
name: Automerge

on:
schedule:
- cron: "*/10 * * * *" # Run every 10 minutes

jobs:
merge:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Automerge
uses: actions/github-script@v6
with:
github-token: ${{secrets.GITHUB_TOKEN}}
retries: 3
TomeHirata marked this conversation as resolved.
Show resolved Hide resolved
script: |
const script = require('./.github/workflows/auto-merge.js');
await script({github, context});
33 changes: 33 additions & 0 deletions .github/workflows/remove-automerge-label.yml
@@ -0,0 +1,33 @@
name: Remove automerge label

on:
pull_request_target:
types:
- synchronize

jobs:
remove-label:
runs-on: ubuntu-latest
harupy marked this conversation as resolved.
Show resolved Hide resolved
if: ${{ !contains(fromJSON('["OWNER", "COLLABORATOR", "MEMBER"]'), github.event.pull_request.author_association )}}
steps:
- uses: actions/checkout@v4
- name: Remove automerge label
uses: actions/github-script@v5
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const { owner, repo, number: issue_number } = context.issue;
const labelToRemove = "automerge";

// Fetch current labels on the PR
const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({ owner, repo, issue_number });

// Check if the label is present
const hasLabel = currentLabels.some(label => label.name === labelToRemove);

if (hasLabel) {
await github.rest.issues.removeLabel({ owner, repo, issue_number, name: labelToRemove });
console.log(`Removed label "${labelToRemove}" from PR #${issue_number}`);
} else {
console.log(`Label "${labelToRemove}" not found on PR #${issue_number}`);
}