Skip to content

Merge verified agent pull requests #32

Merge verified agent pull requests

Merge verified agent pull requests #32

name: Merge verified agent pull requests
on:
workflow_run:
workflows: ["Baseline verification"]
types: [completed]
permissions:
contents: write
pull-requests: write
concurrency:
group: merge-verified-${{ github.event.workflow_run.id }}
cancel-in-progress: false
jobs:
merge:
if: >-
github.event.workflow_run.conclusion == 'success' &&
(github.event.workflow_run.event == 'pull_request' ||
github.event.workflow_run.event == 'workflow_dispatch')
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Verify exact PR head and merge
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
with:
script: |
const run = context.payload.workflow_run;
const { owner, repo } = context.repo;
const resolvePullRequest = async () => {
const linked = run.pull_requests?.[0];
if (run.event === "pull_request" && linked) {
const { data: pr } = await github.rest.pulls.get({
owner,
repo,
pull_number: linked.number,
});
return { pr, verifiedHead: linked.head.sha };
}
if (run.event !== "workflow_dispatch" || !run.head_branch) {
return null;
}
const candidates = await github.paginate(github.rest.pulls.list, {
owner,
repo,
state: "open",
base: "main",
head: `${owner}:${run.head_branch}`,
per_page: 100,
});
const matches = candidates.filter(
(pr) =>
pr.head.repo?.full_name === `${owner}/${repo}` &&
pr.head.ref === run.head_branch &&
pr.head.sha === run.head_sha,
);
if (matches.length !== 1) {
core.info(
`Dispatched Baseline resolved ${matches.length} exact open PRs; expected one.`,
);
return null;
}
return { pr: matches[0], verifiedHead: run.head_sha };
};
const resolved = await resolvePullRequest();
if (!resolved) {
core.info("No exact pull request is linked to this Baseline run.");
return;
}
const { pr, verifiedHead } = resolved;
const trustedAssociations = new Set(["OWNER", "MEMBER", "COLLABORATOR"]);
const markedSyncBot =
pr.user?.login === "github-actions[bot]" &&
pr.head.ref.startsWith("agent/upstream-sync-") &&
pr.body?.includes("Automated-Upstream-Mike-Sync: true");
const eligible =
pr.state === "open" &&
!pr.draft &&
pr.head.repo?.full_name === `${owner}/${repo}` &&
pr.head.ref.startsWith("agent/") &&
(trustedAssociations.has(pr.author_association) || markedSyncBot) &&
pr.head.sha === verifiedHead;
if (!eligible) {
core.info("PR is not an eligible trusted same-repository agent PR at the verified head.");
return;
}
const query = `
query($owner: String!, $repo: String!, $number: Int!, $after: String) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
state
isDraft
headRefOid
reviewDecision
mergeable
reviewThreads(first: 100, after: $after) {
nodes { isResolved }
pageInfo { hasNextPage endCursor }
}
}
}
}
`;
const readGate = async () => {
let after = null;
let pullRequest = null;
let unresolved = false;
do {
const result = await github.graphql(query, {
owner,
repo,
number: pr.number,
after,
});
pullRequest = result.repository.pullRequest;
unresolved ||= pullRequest.reviewThreads.nodes.some(
(thread) => !thread.isResolved,
);
after = pullRequest.reviewThreads.pageInfo.hasNextPage
? pullRequest.reviewThreads.pageInfo.endCursor
: null;
} while (after);
return { pullRequest, unresolved };
};
let gate;
for (let attempt = 1; attempt <= 6; attempt += 1) {
gate = await readGate();
if (gate.pullRequest.mergeable !== "UNKNOWN") break;
core.info(`Mergeability is still UNKNOWN (attempt ${attempt}/6).`);
if (attempt < 6) await new Promise((resolve) => setTimeout(resolve, 10000));
}
const node = gate.pullRequest;
const blocked =
node.state !== "OPEN" ||
node.isDraft ||
node.headRefOid !== verifiedHead ||
node.reviewDecision === "CHANGES_REQUESTED" ||
node.mergeable !== "MERGEABLE" ||
gate.unresolved;
if (blocked) {
core.info("PR has changed or has a review/merge blocker; it will not be merged.");
return;
}
await github.rest.pulls.merge({
owner,
repo,
pull_number: pr.number,
merge_method: "squash",
sha: verifiedHead,
});
core.notice(`Merged PR #${pr.number} immediately after successful final-head Baseline verification.`);