Skip to content

Commit 10d3d9c

Browse files
authored
Consolidate ROSS automation workflows (#80)
1 parent 9225587 commit 10d3d9c

40 files changed

Lines changed: 683 additions & 1199 deletions

.github/actionlint.yaml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
paths:
2-
.github/workflows/deploy-public-beta-ross.yml:
3-
ignore:
4-
- 'constant expression "false" in condition'
52
.github/workflows/staging-debug-release-train.yml:
63
ignore:
74
- 'shellcheck reported issue in this script: SC2016:.+'
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
name: Reconcile agent pull requests
2+
3+
on:
4+
schedule:
5+
- cron: "0 * * * *"
6+
workflow_dispatch:
7+
push:
8+
branches: [main]
9+
10+
permissions:
11+
actions: read
12+
contents: read
13+
issues: write
14+
pull-requests: write
15+
16+
concurrency:
17+
group: reconcile-agent-pull-requests
18+
cancel-in-progress: false
19+
20+
jobs:
21+
dispatch:
22+
permissions:
23+
actions: write
24+
contents: read
25+
issues: write
26+
pull-requests: read
27+
runs-on: ubuntu-latest
28+
timeout-minutes: 5
29+
steps:
30+
- name: Dispatch Baseline for unverified exact heads
31+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
32+
with:
33+
script: |
34+
const { owner, repo } = context.repo;
35+
const trustedAssociations = new Set(["OWNER", "MEMBER", "COLLABORATOR"]);
36+
37+
const { data: workflow } = await github.rest.actions.getWorkflow({
38+
owner,
39+
repo,
40+
workflow_id: "baseline.yml",
41+
});
42+
43+
const reportFailure = async (pr, run) => {
44+
const marker = `<!-- ross-baseline-run:${run.id} -->`;
45+
const comments = await github.paginate(github.rest.issues.listComments, {
46+
owner,
47+
repo,
48+
issue_number: pr.number,
49+
per_page: 100,
50+
});
51+
if (comments.some((comment) => comment.body?.includes(marker))) return;
52+
53+
const jobs = await github.paginate(
54+
github.rest.actions.listJobsForWorkflowRun,
55+
{
56+
owner,
57+
repo,
58+
run_id: run.id,
59+
per_page: 100,
60+
},
61+
);
62+
const failures = jobs
63+
.filter((job) => job.conclusion && job.conclusion !== "success" && job.conclusion !== "skipped")
64+
.map((job) => {
65+
const steps = (job.steps || [])
66+
.filter((step) => step.conclusion === "failure")
67+
.map((step) => step.name);
68+
return `- **${job.name}** (${job.conclusion})${steps.length ? ` — ${steps.join(", ")}` : ""}`;
69+
});
70+
71+
await github.rest.issues.createComment({
72+
owner,
73+
repo,
74+
issue_number: pr.number,
75+
body: [
76+
marker,
77+
`Baseline failed for exact head \`${pr.head.sha}\`.`,
78+
"",
79+
failures.length ? failures.join("\n") : `- Workflow conclusion: **${run.conclusion}**`,
80+
"",
81+
`[Open workflow run](${run.html_url})`,
82+
"",
83+
"Bounded automatic repair runs only for eligible low-risk paths. Protected workflow, dependency, migration, deployment, security, legal, governance, or release changes require focused review.",
84+
].join("\n"),
85+
});
86+
};
87+
88+
const pullRequests = await github.paginate(github.rest.pulls.list, {
89+
owner,
90+
repo,
91+
state: "open",
92+
base: "main",
93+
per_page: 100,
94+
});
95+
96+
for (const pr of pullRequests) {
97+
const markedSyncBot =
98+
pr.user?.login === "github-actions[bot]" &&
99+
pr.head.ref.startsWith("agent/upstream-sync-") &&
100+
pr.body?.includes("Automated-Upstream-Mike-Sync: true");
101+
const eligible =
102+
!pr.draft &&
103+
pr.head.repo?.full_name === `${owner}/${repo}` &&
104+
pr.head.ref.startsWith("agent/") &&
105+
(trustedAssociations.has(pr.author_association) || markedSyncBot);
106+
107+
if (!eligible) continue;
108+
109+
const runs = await github.paginate(
110+
github.rest.actions.listWorkflowRuns,
111+
{
112+
owner,
113+
repo,
114+
workflow_id: workflow.id,
115+
branch: pr.head.ref,
116+
per_page: 100,
117+
},
118+
);
119+
120+
const exactHeadRun = runs.find(
121+
(run) =>
122+
run.head_sha === pr.head.sha &&
123+
!(
124+
run.status === "completed" &&
125+
run.conclusion === "action_required"
126+
),
127+
);
128+
if (exactHeadRun) {
129+
core.info(
130+
`PR #${pr.number} already has Baseline run ${exactHeadRun.id} for ${pr.head.sha} (${exactHeadRun.status}/${exactHeadRun.conclusion || "none"}).`,
131+
);
132+
if (
133+
exactHeadRun.status === "completed" &&
134+
exactHeadRun.conclusion &&
135+
exactHeadRun.conclusion !== "success" &&
136+
exactHeadRun.conclusion !== "skipped" &&
137+
exactHeadRun.conclusion !== "neutral"
138+
) {
139+
await reportFailure(pr, exactHeadRun);
140+
}
141+
continue;
142+
}
143+
144+
await github.rest.actions.createWorkflowDispatch({
145+
owner,
146+
repo,
147+
workflow_id: workflow.id,
148+
ref: pr.head.ref,
149+
});
150+
core.notice(
151+
`Dispatched Baseline for PR #${pr.number} exact head ${pr.head.sha}.`,
152+
);
153+
}
154+
reconcile:
155+
permissions:
156+
actions: read
157+
contents: write
158+
pull-requests: write
159+
runs-on: ubuntu-latest
160+
timeout-minutes: 10
161+
steps:
162+
- name: Merge eligible exact-head verified pull requests
163+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
164+
with:
165+
script: |
166+
const { owner, repo } = context.repo;
167+
const trustedAssociations = new Set(["OWNER", "MEMBER", "COLLABORATOR"]);
168+
169+
const pulls = await github.paginate(github.rest.pulls.list, {
170+
owner,
171+
repo,
172+
state: "open",
173+
base: "main",
174+
per_page: 100,
175+
});
176+
177+
const candidates = pulls.filter((pr) => {
178+
const markedSyncBot =
179+
pr.user?.login === "github-actions[bot]" &&
180+
pr.head.ref.startsWith("agent/upstream-sync-") &&
181+
pr.body?.includes("Automated-Upstream-Mike-Sync: true");
182+
return (
183+
!pr.draft &&
184+
pr.head.repo?.full_name === `${owner}/${repo}` &&
185+
pr.head.ref.startsWith("agent/") &&
186+
(trustedAssociations.has(pr.author_association) || markedSyncBot)
187+
);
188+
});
189+
190+
const gateQuery = `
191+
query($owner: String!, $repo: String!, $number: Int!, $after: String) {
192+
repository(owner: $owner, name: $repo) {
193+
pullRequest(number: $number) {
194+
state
195+
isDraft
196+
headRefOid
197+
reviewDecision
198+
mergeable
199+
reviewThreads(first: 100, after: $after) {
200+
nodes { isResolved }
201+
pageInfo { hasNextPage endCursor }
202+
}
203+
}
204+
}
205+
}
206+
`;
207+
208+
const readGate = async (number) => {
209+
let after = null;
210+
let pullRequest = null;
211+
let unresolved = false;
212+
do {
213+
const result = await github.graphql(gateQuery, {
214+
owner,
215+
repo,
216+
number,
217+
after,
218+
});
219+
pullRequest = result.repository.pullRequest;
220+
unresolved ||= pullRequest.reviewThreads.nodes.some(
221+
(thread) => !thread.isResolved,
222+
);
223+
after = pullRequest.reviewThreads.pageInfo.hasNextPage
224+
? pullRequest.reviewThreads.pageInfo.endCursor
225+
: null;
226+
} while (after);
227+
return { pullRequest, unresolved };
228+
};
229+
230+
for (const pr of candidates) {
231+
const runs = await github.paginate(
232+
github.rest.actions.listWorkflowRunsForRepo,
233+
{
234+
owner,
235+
repo,
236+
branch: pr.head.ref,
237+
status: "completed",
238+
per_page: 100,
239+
},
240+
);
241+
const verified = runs.some(
242+
(run) =>
243+
run.name === "Baseline verification" &&
244+
run.conclusion === "success" &&
245+
run.head_sha === pr.head.sha &&
246+
(run.event === "pull_request" || run.event === "workflow_dispatch"),
247+
);
248+
if (!verified) {
249+
core.info(`PR #${pr.number} has no successful exact-head Baseline.`);
250+
continue;
251+
}
252+
253+
let gate;
254+
for (let attempt = 1; attempt <= 6; attempt += 1) {
255+
gate = await readGate(pr.number);
256+
if (gate.pullRequest.mergeable !== "UNKNOWN") break;
257+
if (attempt < 6) await new Promise((resolve) => setTimeout(resolve, 10000));
258+
}
259+
260+
const node = gate.pullRequest;
261+
const blocked =
262+
node.state !== "OPEN" ||
263+
node.isDraft ||
264+
node.headRefOid !== pr.head.sha ||
265+
node.reviewDecision === "CHANGES_REQUESTED" ||
266+
node.mergeable !== "MERGEABLE" ||
267+
gate.unresolved;
268+
if (blocked) {
269+
core.info(`PR #${pr.number} still has a review, head, or mergeability blocker.`);
270+
continue;
271+
}
272+
273+
try {
274+
await github.rest.pulls.merge({
275+
owner,
276+
repo,
277+
pull_number: pr.number,
278+
merge_method: "squash",
279+
sha: pr.head.sha,
280+
});
281+
core.notice(`Reconciled and merged PR #${pr.number} at verified head ${pr.head.sha}.`);
282+
} catch (error) {
283+
core.warning(`PR #${pr.number} was eligible but merge failed: ${error.message}`);
284+
}
285+
}

.github/workflows/apply-upstream-sync-batch-size.yml

Lines changed: 0 additions & 60 deletions
This file was deleted.

.github/workflows/baseline.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ on:
55
pull_request:
66
push:
77
branches: [main]
8+
paths-ignore:
9+
- reports/release-manifest-v1.json
810

911
permissions:
1012
contents: read

0 commit comments

Comments
 (0)