Skip to content

Commit 9a19364

Browse files
authored
ci: stop duplicate automatic x86 E2E executors (#7279)
Non-e2e labels from bots retriggered pull_request_target, and each dispatcher run used a unique generation/requestKey. That started several identical automatic executors for the same PR HEAD. Only dispatch automatic coverage on opened/reopened/synchronize or e2e:* label changes, share a pr+headSHA concurrency group, cancel in-progress automatic executors for that HEAD, and reuse the same executor concurrency key. Signed-off-by: Zujian Zhang <zhangzujian.7@gmail.com>
1 parent e0d20f1 commit 9a19364

3 files changed

Lines changed: 145 additions & 2 deletions

File tree

.github/workflows/x86-e2e-dispatcher.yaml

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,18 @@ jobs:
198198
if: >-
199199
always() &&
200200
github.event_name == 'pull_request_target' &&
201-
github.event.action != 'closed'
201+
(
202+
github.event.action == 'opened' ||
203+
github.event.action == 'reopened' ||
204+
github.event.action == 'synchronize' ||
205+
(
206+
(github.event.action == 'labeled' || github.event.action == 'unlabeled') &&
207+
startsWith(github.event.label.name, 'e2e:')
208+
)
209+
)
210+
concurrency:
211+
group: x86-e2e-automatic-${{ github.event.pull_request.number }}-${{ github.event.pull_request.head.sha }}
212+
cancel-in-progress: true
202213
permissions:
203214
actions: write
204215
contents: write
@@ -260,7 +271,28 @@ jobs:
260271
'.controlledLabels | if length == 0 then "-" else join(",") end' \
261272
automatic-context.json)
262273
approvalGeneration=1
263-
requestKey="automatic-$DISPATCH_GENERATION"
274+
requestKey="automatic-$PR_NUMBER-$headSHA"
275+
gh api --paginate --slurp \
276+
"repos/$GITHUB_REPOSITORY/actions/workflows/build-x86-image.yaml/runs?event=workflow_dispatch&per_page=100" \
277+
| jq '{workflow_runs: [.[].workflow_runs[]]}' > automatic-executor-runs.json
278+
python3 - "$PR_NUMBER" "$headSHA" <<'PY' > stale-automatic-runs.txt
279+
import json
280+
import sys
281+
from pathlib import Path
282+
import e2e_control as e2eControl
283+
284+
runs = json.loads(Path("automatic-executor-runs.json").read_text())["workflow_runs"]
285+
for runId in e2eControl.inProgressAutomaticExecutorRunIds(
286+
runs, int(sys.argv[1]), sys.argv[2]
287+
):
288+
print(runId)
289+
PY
290+
while IFS= read -r runId; do
291+
[ -n "$runId" ] || continue
292+
if ! gh api --method POST "repos/$GITHUB_REPOSITORY/actions/runs/$runId/cancel"; then
293+
echo "Automatic executor run $runId finished before cancellation." >&2
294+
fi
295+
done < stale-automatic-runs.txt
264296
executorRef=$(python3 - "$PR_NUMBER" "$approvalGeneration" "$DISPATCH_GENERATION" <<'PY'
265297
import sys
266298
import e2e_control as e2eControl

hack/e2e_control.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -952,6 +952,34 @@ def latestExecutorRun(
952952
return matchingRuns[0][1]
953953

954954

955+
def inProgressAutomaticExecutorRunIds(runs, prNumber, headSHA):
956+
if not re.fullmatch(headPattern, headSHA or ""):
957+
raise ValueError("invalid pull request HEAD for automatic executor cancellation")
958+
prNumber = int(prNumber)
959+
matched = []
960+
for run in runs:
961+
if run.get("status") not in {"queued", "in_progress"}:
962+
continue
963+
if run.get("actor", {}).get("login") != "github-actions[bot]":
964+
continue
965+
path = run.get("path")
966+
if path not in (None, "", ".github/workflows/build-x86-image.yaml"):
967+
continue
968+
try:
969+
metadata = parseExecutorRunName(run.get("display_title") or "")
970+
except ValueError:
971+
continue
972+
if (
973+
metadata["prNumber"] == prNumber
974+
and metadata["headSHA"] == headSHA
975+
and metadata["automatic"]
976+
):
977+
runId = run.get("id")
978+
if runId is not None:
979+
matched.append(runId)
980+
return matched
981+
982+
955983
def parseArgs():
956984
parser = argparse.ArgumentParser(description="Control comment-gated x86 E2E workflows")
957985
subparsers = parser.add_subparsers(dest="command", required=True)

hack/test_e2e_control.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -976,6 +976,67 @@ def testLatestExecutorRunPrefersApprovedCoverageOverAutomaticProbe(self):
976976

977977
self.assertEqual(latest["id"], 8)
978978

979+
def testInProgressAutomaticExecutorsForTheSameHeadAreCancelled(self):
980+
head = "a" * 40
981+
otherHead = "b" * 40
982+
runs = [
983+
{
984+
"id": 11,
985+
"path": ".github/workflows/build-x86-image.yaml",
986+
"actor": {"login": "github-actions[bot]"},
987+
"status": "in_progress",
988+
"display_title": (
989+
"x86-e2e pr=7278 head=" + head
990+
+ " approval=1 generation=1001 mode=automatic groups=- labels=- full=0"
991+
),
992+
},
993+
{
994+
"id": 12,
995+
"path": ".github/workflows/build-x86-image.yaml",
996+
"actor": {"login": "github-actions[bot]"},
997+
"status": "queued",
998+
"display_title": (
999+
"x86-e2e pr=7278 head=" + head
1000+
+ " approval=1 generation=1002 mode=automatic groups=- labels=- full=0"
1001+
),
1002+
},
1003+
{
1004+
"id": 13,
1005+
"path": ".github/workflows/build-x86-image.yaml",
1006+
"actor": {"login": "github-actions[bot]"},
1007+
"status": "in_progress",
1008+
"display_title": (
1009+
"x86-e2e pr=7278 head=" + head
1010+
+ " approval=1 generation=1003 mode=approved groups=core labels=- full=0"
1011+
),
1012+
},
1013+
{
1014+
"id": 14,
1015+
"path": ".github/workflows/build-x86-image.yaml",
1016+
"actor": {"login": "github-actions[bot]"},
1017+
"status": "in_progress",
1018+
"display_title": (
1019+
"x86-e2e pr=7278 head=" + otherHead
1020+
+ " approval=1 generation=1004 mode=automatic groups=- labels=- full=0"
1021+
),
1022+
},
1023+
{
1024+
"id": 15,
1025+
"path": ".github/workflows/build-x86-image.yaml",
1026+
"actor": {"login": "github-actions[bot]"},
1027+
"status": "completed",
1028+
"display_title": (
1029+
"x86-e2e pr=7278 head=" + head
1030+
+ " approval=1 generation=1005 mode=automatic groups=- labels=- full=0"
1031+
),
1032+
},
1033+
]
1034+
1035+
self.assertEqual(
1036+
e2eControl.inProgressAutomaticExecutorRunIds(runs, 7278, head),
1037+
[11, 12],
1038+
)
1039+
9791040
def testDispatchCliWritesDecisionJson(self):
9801041
with tempfile.TemporaryDirectory() as directory:
9811042
directory = Path(directory)
@@ -1131,6 +1192,28 @@ def testDispatcherWorkflowUsesOnlyTrustedWritePermissions(self):
11311192
self.assertIn('-f ref="$executorRef"', workflow)
11321193
self.assertNotIn("ref: ${{ inputs.headSHA || github.sha }}", workflow)
11331194

1195+
def testAutomaticCoverageIgnoresUncontrolledLabelEvents(self):
1196+
workflow = (repoRoot / ".github/workflows/x86-e2e-dispatcher.yaml").read_text()
1197+
automatic = e2eSelector.workflowJobBlocks(workflow)["automatic"]
1198+
1199+
self.assertIn("github.event.action == 'opened'", automatic)
1200+
self.assertIn("github.event.action == 'reopened'", automatic)
1201+
self.assertIn("github.event.action == 'synchronize'", automatic)
1202+
self.assertIn("github.event.action == 'labeled'", automatic)
1203+
self.assertIn("github.event.action == 'unlabeled'", automatic)
1204+
self.assertIn("startsWith(github.event.label.name, 'e2e:')", automatic)
1205+
self.assertNotIn("github.event.action != 'closed'", automatic)
1206+
self.assertIn(
1207+
"group: x86-e2e-automatic-${{ github.event.pull_request.number }}-"
1208+
"${{ github.event.pull_request.head.sha }}",
1209+
automatic,
1210+
)
1211+
self.assertIn("cancel-in-progress: true", automatic)
1212+
self.assertIn("inProgressAutomaticExecutorRunIds", automatic)
1213+
self.assertIn("actions/runs/$runId/cancel", automatic)
1214+
self.assertIn('requestKey="automatic-$PR_NUMBER-$headSHA"', automatic)
1215+
self.assertNotIn('requestKey="automatic-$DISPATCH_GENERATION"', automatic)
1216+
11341217
def testGateWorkflowCanOnlyReadRunsAndWriteChecks(self):
11351218
workflow = (repoRoot / ".github/workflows/x86-e2e-gate.yaml").read_text()
11361219

0 commit comments

Comments
 (0)