Skip to content

Commit 98c8aa1

Browse files
authored
[ARM Auto Signoff] Use statuses for LintDiff and Avocado (#36472)
1 parent 3bfc547 commit 98c8aa1

File tree

4 files changed

+112
-96
lines changed

4 files changed

+112
-96
lines changed

.github/workflows/arm-auto-signoff.yaml

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
11
name: ARM Auto SignOff
22

33
on:
4-
issue_comment:
5-
types:
6-
- edited
74
# Must run on pull_request_target instead of pull_request, since the latter cannot trigger on
85
# labels from bot accounts in fork PRs. pull_request_target is also more similar to the other
9-
# triggers "issue_comment" and "workflow_run" -- they are all privileged# and run in the target
10-
# branch and repo -- which simplifies implementation.
6+
# trigger "workflow_run" -- they are both privileged and run in the target branch and repo --
7+
# which simplifies implementation.
118
pull_request_target:
129
types:
1310
# Depends on labels, so must re-evaluate whenever a relevant label is manually added or removed.
1411
- labeled
1512
- unlabeled
1613
workflow_run:
17-
workflows: ["ARM Incremental TypeSpec"]
14+
workflows:
15+
["ARM Incremental TypeSpec", "Swagger Avocado - Set Status", "Swagger LintDiff - Set Status"]
1816
types: [completed]
1917

2018
permissions:
@@ -28,9 +26,8 @@ jobs:
2826
arm-auto-signoff:
2927
name: ARM Auto SignOff
3028

29+
# workflow_run - already filtered by triggers above
3130
# pull_request_target:labeled - filter to only the input and output labels
32-
# issue_comment:edited - filter to only PR comments containing "next steps to merge",
33-
# a signal that checks like "Swagger LintDiff" or "Swagger Avocado" status may have changed
3431
if: |
3532
github.event_name == 'workflow_run' ||
3633
(github.event_name == 'pull_request_target' &&
@@ -41,10 +38,7 @@ jobs:
4138
github.event.label.name == 'ARMReview' ||
4239
github.event.label.name == 'ARMSignedOff' ||
4340
github.event.label.name == 'NotReadyForARMReview' ||
44-
github.event.label.name == 'SuppressionReviewRequired')) ||
45-
(github.event_name == 'issue_comment' &&
46-
github.event.issue.pull_request &&
47-
contains(github.event.comment.body, 'next steps to merge'))
41+
github.event.label.name == 'SuppressionReviewRequired'))
4842
4943
runs-on: ubuntu-24.04
5044

.github/workflows/src/arm-auto-signoff.js

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
// @ts-check
22

33
import { setEquals } from "../../shared/src/equality.js";
4-
import { PER_PAGE_MAX } from "../../shared/src/github.js";
4+
import { CommitStatusState, PER_PAGE_MAX } from "../../shared/src/github.js";
5+
import { byDate, invert } from "../../shared/src/sort.js";
56
import { extractInputs } from "./context.js";
67
import { LabelAction } from "./label.js";
78

@@ -142,52 +143,63 @@ export async function getLabelActionImpl({ owner, repo, issue_number, head_sha,
142143
return removeAction;
143144
}
144145

145-
const checkRuns = await github.paginate(github.rest.checks.listForRef, {
146+
const statuses = await github.paginate(github.rest.repos.listCommitStatusesForRef, {
146147
owner: owner,
147148
repo: repo,
148149
ref: head_sha,
149150
per_page: PER_PAGE_MAX,
150151
});
151152

152-
const requiredCheckNames = ["Swagger LintDiff", "Swagger Avocado"];
153+
core.info("Statuses:");
154+
statuses.forEach((status) => {
155+
core.info(`- ${status.context}: ${status.state}`);
156+
});
157+
158+
const requiredStatusNames = ["Swagger LintDiff", "Swagger Avocado"];
153159

154160
/**
155-
* @type {typeof checkRuns.check_runs}
161+
* @type {typeof statuses}
156162
*/
157-
let requiredCheckRuns = [];
158-
159-
for (const checkName of requiredCheckNames) {
160-
const matchingRuns = checkRuns.filter((run) => run.name === checkName);
161-
162-
if (matchingRuns.length > 1) {
163-
throw new Error(`Unexpected number of checks named '${checkName}': ${matchingRuns.length}`);
164-
}
165-
166-
const matchingRun = matchingRuns.length === 1 ? matchingRuns[0] : undefined;
167-
168-
core.info(
169-
`${checkName}: Status='${matchingRun?.status}', Conclusion='${matchingRun?.conclusion}'`,
170-
);
171-
172-
if (matchingRun && matchingRun.status === "completed" && matchingRun.conclusion !== "success") {
173-
core.info(`Check '${checkName}' did not succeed`);
163+
let requiredStatuses = [];
164+
165+
for (const statusName of requiredStatusNames) {
166+
// The "statuses" array may contain multiple statuses with the same "context" (aka "name"),
167+
// but different states and update times. We only care about the latest.
168+
const matchingStatuses = statuses
169+
.filter((status) => status.context.toLowerCase() === statusName.toLowerCase())
170+
.sort(invert(byDate((status) => status.updated_at)));
171+
172+
// undefined if matchingStatuses.length === 0 (which is OK)
173+
const matchingStatus = matchingStatuses[0];
174+
175+
core.info(`${statusName}: State='${matchingStatus?.state}'`);
176+
177+
if (
178+
matchingStatus &&
179+
(matchingStatus.state === CommitStatusState.ERROR ||
180+
matchingStatus.state === CommitStatusState.FAILURE)
181+
) {
182+
core.info(`Status '${matchingStatus}' did not succeed`);
174183
return removeAction;
175184
}
176185

177-
if (matchingRun) {
178-
requiredCheckRuns.push(matchingRun);
186+
if (matchingStatus) {
187+
requiredStatuses.push(matchingStatus);
179188
}
180189
}
181190

182191
if (
183-
setEquals(new Set(requiredCheckRuns.map((run) => run.name)), new Set(requiredCheckNames)) &&
184-
requiredCheckRuns.every((run) => run.status === "completed" && run.conclusion === "success")
192+
setEquals(
193+
new Set(requiredStatuses.map((status) => status.context)),
194+
new Set(requiredStatusNames),
195+
) &&
196+
requiredStatuses.every((status) => status.state === CommitStatusState.SUCCESS)
185197
) {
186198
core.info("All requirements met for auto-signoff");
187199
return labelActions[LabelAction.Add];
188200
}
189201

190-
// If any checks are missing or not completed, no-op to prevent frequent remove/add label as checks re-run
191-
core.info("One or more checks are still in-progress");
202+
// If any statuses are missing or pending, no-op to prevent frequent remove/add label as checks re-run
203+
core.info("One or more statuses are still pending");
192204
return labelActions[LabelAction.None];
193205
}

.github/workflows/test/arm-auto-signoff.test.js

Lines changed: 66 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { describe, expect, it } from "vitest";
2+
import { CommitStatusState } from "../../shared/src/github.js";
23
import { getLabelActionImpl } from "../src/arm-auto-signoff.js";
34
import { LabelAction } from "../src/label.js";
45
import { createMockCore, createMockGithub as createMockGithubBase } from "./mocks.js";
@@ -218,16 +219,13 @@ describe("getLabelActionImpl", () => {
218219
github.rest.issues.listLabelsOnIssue.mockResolvedValue({
219220
data: [{ name: "ARMAutoSignedOff" }, { name: "ARMReview" }],
220221
});
221-
github.rest.checks.listForRef.mockResolvedValue({
222-
data: {
223-
check_runs: [
224-
{
225-
name: check,
226-
status: "completed",
227-
conclusion: "failure",
228-
},
229-
],
230-
},
222+
github.rest.repos.listCommitStatusesForRef.mockResolvedValue({
223+
data: [
224+
{
225+
context: check,
226+
state: CommitStatusState.FAILURE,
227+
},
228+
],
231229
});
232230

233231
await expect(
@@ -243,29 +241,54 @@ describe("getLabelActionImpl", () => {
243241
},
244242
);
245243

246-
it("thows if multiple runs for same check", async () => {
244+
it.each([
245+
[CommitStatusState.ERROR, ["ARMReview", "ARMAutoSignedOff"]],
246+
[CommitStatusState.FAILURE, ["ARMReview", "ARMAutoSignedOff"]],
247+
[CommitStatusState.PENDING, ["ARMReview"]],
248+
[CommitStatusState.SUCCESS, ["ARMReview"]],
249+
])("uses latest status if multiple (%o)", async (state, labels) => {
247250
const github = createMockGithub({ incrementalTypeSpec: true });
248251

249252
github.rest.issues.listLabelsOnIssue.mockResolvedValue({
250-
data: [{ name: "ARMReview" }],
253+
data: labels.map((l) => ({
254+
name: l,
255+
})),
251256
});
252257

253-
github.rest.checks.listForRef.mockResolvedValue({
254-
data: {
255-
check_runs: [
256-
{
257-
name: "Swagger LintDiff",
258-
status: "in_progress",
259-
conclusion: null,
260-
},
261-
{
262-
name: "Swagger LintDiff",
263-
status: "in_progress",
264-
conclusion: null,
265-
},
266-
],
267-
},
258+
github.rest.repos.listCommitStatusesForRef.mockResolvedValue({
259+
data: [
260+
{
261+
context: "Swagger Avocado",
262+
state: CommitStatusState.SUCCESS,
263+
updated_at: "2025-01-01",
264+
},
265+
{
266+
context: "Swagger LintDiff",
267+
state: CommitStatusState.PENDING,
268+
updated_at: "2025-01-01",
269+
},
270+
{
271+
context: "Swagger LintDiff",
272+
state,
273+
updated_at: "2025-01-02",
274+
},
275+
],
268276
});
277+
278+
let expectedAction;
279+
switch (state) {
280+
case CommitStatusState.ERROR:
281+
case CommitStatusState.FAILURE:
282+
expectedAction = LabelAction.Remove;
283+
break;
284+
case CommitStatusState.SUCCESS:
285+
expectedAction = LabelAction.Add;
286+
break;
287+
case CommitStatusState.PENDING:
288+
expectedAction = LabelAction.None;
289+
break;
290+
}
291+
269292
await expect(
270293
getLabelActionImpl({
271294
owner: "TestOwner",
@@ -275,7 +298,7 @@ describe("getLabelActionImpl", () => {
275298
github: github,
276299
core: core,
277300
}),
278-
).rejects.toThrow();
301+
).resolves.toEqual({ labelAction: expectedAction, headSha: "abc123", issueNumber: 123 });
279302
});
280303

281304
it("no-ops if check not found or not completed", async () => {
@@ -285,10 +308,8 @@ describe("getLabelActionImpl", () => {
285308
data: [{ name: "ARMReview" }],
286309
});
287310

288-
github.rest.checks.listForRef.mockResolvedValue({
289-
data: {
290-
check_runs: [],
291-
},
311+
github.rest.repos.listCommitStatusesForRef.mockResolvedValue({
312+
data: [],
292313
});
293314
await expect(
294315
getLabelActionImpl({
@@ -301,16 +322,8 @@ describe("getLabelActionImpl", () => {
301322
}),
302323
).resolves.toEqual({ labelAction: LabelAction.None, headSha: "abc123", issueNumber: 123 });
303324

304-
github.rest.checks.listForRef.mockResolvedValue({
305-
data: {
306-
check_runs: [
307-
{
308-
name: "Swagger LintDiff",
309-
status: "in_progress",
310-
conclusion: null,
311-
},
312-
],
313-
},
325+
github.rest.repos.listCommitStatusesForRef.mockResolvedValue({
326+
data: [{ context: "Swagger LintDiff", state: CommitStatusState.PENDING }],
314327
});
315328
await expect(
316329
getLabelActionImpl({
@@ -330,21 +343,17 @@ describe("getLabelActionImpl", () => {
330343
github.rest.issues.listLabelsOnIssue.mockResolvedValue({
331344
data: [{ name: "ARMReview" }],
332345
});
333-
github.rest.checks.listForRef.mockResolvedValue({
334-
data: {
335-
check_runs: [
336-
{
337-
name: "Swagger LintDiff",
338-
status: "completed",
339-
conclusion: "success",
340-
},
341-
{
342-
name: "Swagger Avocado",
343-
status: "completed",
344-
conclusion: "success",
345-
},
346-
],
347-
},
346+
github.rest.repos.listCommitStatusesForRef.mockResolvedValue({
347+
data: [
348+
{
349+
context: "Swagger LintDiff",
350+
state: CommitStatusState.SUCCESS,
351+
},
352+
{
353+
context: "Swagger Avocado",
354+
state: CommitStatusState.SUCCESS,
355+
},
356+
],
348357
});
349358

350359
await expect(

.github/workflows/test/mocks.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export function createMockGithub() {
3333
},
3434
repos: {
3535
createCommitStatus: vi.fn(),
36+
listCommitStatusesForRef: vi.fn().mockResolvedValue({ data: [] }),
3637
listPullRequestsAssociatedWithCommit: vi.fn().mockResolvedValue({
3738
data: [],
3839
}),

0 commit comments

Comments
 (0)