Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions .github/workflows/dispatch-unverified-agent-heads.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: Dispatch unverified agent heads

on:
schedule:
- cron: "*/5 * * * *"
workflow_dispatch:
push:
branches: [main]

permissions:
actions: write
contents: read
pull-requests: read

concurrency:
group: dispatch-unverified-agent-heads
cancel-in-progress: false

jobs:
dispatch:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Dispatch Baseline for unverified exact heads
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
with:
script: |
const { owner, repo } = context.repo;
const trustedAssociations = new Set(["OWNER", "MEMBER", "COLLABORATOR"]);

const { data: workflow } = await github.rest.actions.getWorkflow({
owner,
repo,
workflow_id: "baseline.yml",
});

const pullRequests = await github.paginate(github.rest.pulls.list, {
owner,
repo,
state: "open",
base: "main",
per_page: 100,
});

for (const pr of pullRequests) {
const eligible =
!pr.draft &&
pr.head.repo?.full_name === `${owner}/${repo}` &&
pr.head.ref.startsWith("agent/") &&
trustedAssociations.has(pr.author_association);

if (!eligible) continue;

const runs = await github.paginate(
github.rest.actions.listWorkflowRuns,
{
owner,
repo,
workflow_id: workflow.id,
branch: pr.head.ref,
per_page: 100,
},
);

const exactHeadRun = runs.find((run) => run.head_sha === pr.head.sha);
if (exactHeadRun) {
core.info(
`PR #${pr.number} already has Baseline run ${exactHeadRun.id} for ${pr.head.sha} (${exactHeadRun.status}/${exactHeadRun.conclusion || "none"}).`,
);
continue;
}

await github.rest.actions.createWorkflowDispatch({
owner,
repo,
workflow_id: workflow.id,
ref: pr.head.ref,
});
core.notice(
`Dispatched Baseline for PR #${pr.number} exact head ${pr.head.sha}.`,
);
}
64 changes: 49 additions & 15 deletions .github/workflows/merge-verified-agent-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ jobs:
merge:
if: >-
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.pull_requests[0] != null
(github.event.workflow_run.event == 'pull_request' ||
github.event.workflow_run.event == 'workflow_dispatch')
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
Expand All @@ -27,18 +27,52 @@ jobs:
with:
script: |
const run = context.payload.workflow_run;
const linked = run.pull_requests?.[0];
if (!linked) {
core.info("No pull request is linked to this Baseline 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 { owner, repo } = context.repo;
const { data: pr } = await github.rest.pulls.get({
owner,
repo,
pull_number: linked.number,
});
const { pr, verifiedHead } = resolved;

const trustedAssociations = new Set(["OWNER", "MEMBER", "COLLABORATOR"]);
const eligible =
Expand All @@ -47,7 +81,7 @@ jobs:
pr.head.repo?.full_name === `${owner}/${repo}` &&
pr.head.ref.startsWith("agent/") &&
trustedAssociations.has(pr.author_association) &&
pr.head.sha === linked.head.sha;
pr.head.sha === verifiedHead;

if (!eligible) {
core.info("PR is not an eligible trusted same-repository agent PR at the verified head.");
Expand Down Expand Up @@ -108,7 +142,7 @@ jobs:
const blocked =
node.state !== "OPEN" ||
node.isDraft ||
node.headRefOid !== pr.head.sha ||
node.headRefOid !== verifiedHead ||
node.reviewDecision === "CHANGES_REQUESTED" ||
node.mergeable !== "MERGEABLE" ||
gate.unresolved;
Expand All @@ -123,6 +157,6 @@ jobs:
repo,
pull_number: pr.number,
merge_method: "squash",
sha: pr.head.sha,
sha: verifiedHead,
});
core.notice(`Merged PR #${pr.number} immediately after successful final-head Baseline verification.`);
55 changes: 43 additions & 12 deletions .github/workflows/repair-failed-baseline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ permissions:
pull-requests: read

concurrency:
group: repair-baseline-${{ github.event.workflow_run.pull_requests[0].number || github.event.workflow_run.id }}
group: repair-baseline-${{ github.event.workflow_run.head_branch || github.event.workflow_run.id }}
cancel-in-progress: false

jobs:
qualify:
if: >-
github.event.workflow_run.conclusion == 'failure' &&
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.pull_requests[0] != null
(github.event.workflow_run.event == 'pull_request' ||
github.event.workflow_run.event == 'workflow_dispatch')
runs-on: ubuntu-latest
outputs:
eligible: ${{ steps.gate.outputs.eligible }}
Expand All @@ -34,30 +34,61 @@ jobs:
with:
script: |
const run = context.payload.workflow_run;
const linked = run.pull_requests?.[0];
const { owner, repo } = context.repo;
const deny = (reason) => {
core.setOutput('eligible', 'false');
core.setOutput('reason', reason);
core.notice(`Automatic repair skipped: ${reason}`);
};

if (!linked) return deny('no linked pull request');
const { data: pr } = await github.rest.pulls.get({
owner,
repo,
pull_number: linked.number,
});
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,
);
return matches.length === 1
? { pr: matches[0], verifiedHead: run.head_sha }
: null;
};

const resolved = await resolvePullRequest();
if (!resolved) return deny('no exact linked pull request');
const { pr, verifiedHead } = resolved;

core.setOutput('pr_number', String(pr.number));
core.setOutput('head_sha', pr.head.sha);
core.setOutput('head_sha', verifiedHead);
core.setOutput('head_ref', pr.head.ref);

const trusted = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']);
if (pr.state !== 'open' || pr.draft) return deny('PR is closed or draft');
if (pr.head.repo?.full_name !== `${owner}/${repo}`) return deny('fork PR');
if (!pr.head.ref.startsWith('agent/')) return deny('branch is not an agent branch');
if (!trusted.has(pr.author_association)) return deny('author is not trusted');
if (pr.head.sha !== linked.head.sha) return deny('failed run is not for the current head');
if (pr.head.sha !== verifiedHead) return deny('failed run is not for the current head');

const files = await github.paginate(github.rest.pulls.listFiles, {
owner,
Expand Down