[CI] Check target branch freshness before starting CI (#56169)
Signed-off-by: Andreas Karatzas <akaratza@amd.com> Signed-off-by: Andreas Karatzas <Andreas.Karatzas@amd.com>
Andreas Karatzas committed
Sep 14, 2026 at 16:40 UTC
bdad63c90aec1c97682b20f282ba2a959f8229c9
4 files changed
+322
-12
.github/workflows/run-ci-command.yml
+6
@@ -21,11 +21,17 @@ jobs:
21
(github.event.comment.body == '/ci run' ||
22
github.event.comment.body == '/ci run all' ||
23
github.event.comment.body == '/ci run nightly' ||
24
+ github.event.comment.body == '/ci run --allow-stale' ||
25
+ github.event.comment.body == '/ci run all --allow-stale' ||
26
+ github.event.comment.body == '/ci run nightly --allow-stale' ||
27
github.event.comment.body == '/ci retry' ||
28
github.event.comment.body == '/ci cancel' ||
29
github.event.comment.body == '/amd-ci run' ||
30
github.event.comment.body == '/amd-ci run all' ||
31
github.event.comment.body == '/amd-ci run nightly' ||
32
+ github.event.comment.body == '/amd-ci run --allow-stale' ||
33
+ github.event.comment.body == '/amd-ci run all --allow-stale' ||
34
+ github.event.comment.body == '/amd-ci run nightly --allow-stale' ||
35
github.event.comment.body == '/amd-ci retry' ||
36
github.event.comment.body == '/amd-ci cancel')
37
runs-on: [self-hosted, linux, x64, vllm-runners]
.github/workflows/scripts/run_ci_command.py
+92
-12
@@ -1,6 +1,7 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
+import http.client
5
import json
6
import os
7
import random
@@ -22,6 +23,7 @@ COMMAND_RUN_AMD_CI_ALL = "/amd-ci run all"
23
COMMAND_RUN_AMD_CI_NIGHTLY = "/amd-ci run nightly"
24
COMMAND_RETRY_AMD_FAILED = "/amd-ci retry"
25
COMMAND_CANCEL_AMD_CI = "/amd-ci cancel"
26
+ALLOW_STALE_SUFFIX = " --allow-stale"
27
RUN_CI_COMMAND_ENV = {
28
COMMAND_RUN_CI: {},
29
COMMAND_RUN_CI_ALL: {"RUN_ALL": "1"},
@@ -30,20 +32,25 @@ RUN_CI_COMMAND_ENV = {
32
COMMAND_RUN_AMD_CI_ALL: {"RUN_ALL": "1"},
33
COMMAND_RUN_AMD_CI_NIGHTLY: {"RUN_ALL": "1", "NIGHTLY": "1"},
34
}
35
+ALLOW_STALE_COMMANDS = frozenset(
36
+ f"{command}{ALLOW_STALE_SUFFIX}" for command in RUN_CI_COMMAND_ENV
37
+)
38
+RUN_CI_COMMAND_ENV.update(
39
+ {
40
+ command: RUN_CI_COMMAND_ENV[command.removesuffix(ALLOW_STALE_SUFFIX)]
41
+ for command in ALLOW_STALE_COMMANDS
42
+ }
43
+)
44
UPSTREAM_CI_COMMANDS = frozenset(
45
{
35
- COMMAND_RUN_CI,
36
- COMMAND_RUN_CI_ALL,
37
- COMMAND_RUN_CI_NIGHTLY,
46
+ *(command for command in RUN_CI_COMMAND_ENV if command.startswith("/ci ")),
47
COMMAND_RETRY_FAILED,
48
COMMAND_CANCEL_CI,
49
}
50
)
51
AMD_CI_COMMANDS = frozenset(
52
{
44
- COMMAND_RUN_AMD_CI,
45
- COMMAND_RUN_AMD_CI_ALL,
46
- COMMAND_RUN_AMD_CI_NIGHTLY,
53
+ *(command for command in RUN_CI_COMMAND_ENV if command.startswith("/amd-ci ")),
54
COMMAND_RETRY_AMD_FAILED,
55
COMMAND_CANCEL_AMD_CI,
56
}
@@ -79,6 +86,10 @@ class ApiError(RuntimeError):
86
self.status = status
87
88
89
+class CiPreparationError(RuntimeError):
90
+ pass
91
+
92
+
93
def rate_limit_jitter() -> float:
94
return random.uniform(1, 5)
95
@@ -137,6 +148,8 @@ class HttpTransport:
148
None,
149
f"API request failed: {error.reason}",
150
) from error
151
+ except (OSError, http.client.HTTPException) as error:
152
+ raise ApiError(None, f"API request failed: {error}") from error
153
154
if not response_body:
155
return None
@@ -234,6 +247,17 @@ class GitHubClient:
247
def get_pr(self, number: int) -> dict[str, Any]:
248
return self._request(self._repo_path(f"/pulls/{number}"))
249
250
+ def get_commits_behind_base(self, base_ref: str, head_sha: str) -> int:
251
+ base_ref = urllib.parse.quote(f"refs/heads/{base_ref}", safe="")
252
+ head_sha = urllib.parse.quote(head_sha, safe="")
253
+ response = self._request(
254
+ self._repo_path(f"/compare/{base_ref}...{head_sha}?per_page=1")
255
+ )
256
+ behind = response.get("behind_by") if isinstance(response, dict) else None
257
+ if type(behind) is not int or behind < 0:
258
+ raise ApiError(None, "GitHub returned an invalid commits-behind count.")
259
+ return behind
260
+
261
def list_pulls_for_commit(self, commit: str) -> list[dict[str, Any]]:
262
commit = urllib.parse.quote(commit, safe="")
263
response = self._request(self._repo_path(f"/commits/{commit}/pulls"))
@@ -747,6 +771,10 @@ def notify_authorized(
771
(
772
f"✅ @{author}, CI is now available for this PR.\n\n"
773
"- `/ci run` starts upstream CI; `/amd-ci run` starts AMD CI only.\n"
774
+ "- Your branch must contain every commit currently on its upstream "
775
+ "target branch. Merge or rebase onto the latest target branch, then "
776
+ "rerun the command. Append `--allow-stale` to a run command to test "
777
+ "an outdated branch at your own risk.\n"
778
"- `/ci retry` retries failed jobs in the CI build for the current "
779
"PR head. If the current head has no CI build, it starts a new CI "
780
"build for the current head containing only jobs that failed in "
@@ -794,6 +822,56 @@ def resolve_workflow_run_pr(
822
return find_matching_pr(github.list_pulls_for_commit(head_sha))
823
824
825
+def prepare_pr_for_ci(
826
+ github: GitHubClient,
827
+ pr: Mapping[str, Any],
828
+ command: str,
829
+) -> tuple[dict[str, Any], str]:
830
+ base_ref = pr["base"]["ref"]
831
+ no_action = "No new CI build was started."
832
+ try:
833
+ behind = github.get_commits_behind_base(base_ref, pr["head"]["sha"])
834
+ current_pr = github.get_pr(pr["number"])
835
+ except ApiError as error:
836
+ raise CiPreparationError(
837
+ f"Could not check the PR against upstream `{base_ref}`. "
838
+ f"{no_action} Comment `{command}` again. {error}"
839
+ ) from error
840
+ if (
841
+ current_pr["state"] != "open"
842
+ or current_pr["head"]["sha"] != pr["head"]["sha"]
843
+ or current_pr["base"]["ref"] != pr["base"]["ref"]
844
+ ):
845
+ raise CiPreparationError(
846
+ f"The PR changed while checking its branch. {no_action} "
847
+ f"Comment `{command}` again."
848
+ )
849
+ commits = "commit" if behind == 1 else "commits"
850
+ lag = f"This PR is {behind} {commits} behind upstream `{base_ref}`."
851
+ if command in ALLOW_STALE_COMMANDS:
852
+ warning = ""
853
+ if behind:
854
+ run_command = command.removesuffix(ALLOW_STALE_SUFFIX)
855
+ warning = (
856
+ f"⚠️ {lag} Running CI at your own risk because "
857
+ "`--allow-stale` was requested; outdated CI configuration may "
858
+ "cause failures. Before merging, merge or rebase onto the latest "
859
+ f"`{base_ref}`, then rerun `{run_command}` on the latest PR commit."
860
+ )
861
+ print(warning)
862
+ return current_pr, warning
863
+
864
+ if behind:
865
+ raise CiPreparationError(
866
+ f"{lag} Your branch must contain every commit currently on upstream "
867
+ f"`{base_ref}`. {no_action} "
868
+ f"Merge or rebase onto the latest `{base_ref}`, then rerun `{command}`."
869
+ " To test this branch at your own risk, "
870
+ f"use `{command}{ALLOW_STALE_SUFFIX}`."
871
+ )
872
+ return current_pr, ""
873
+
874
+
875
def handle_run_ci(
876
*,
877
actor: str,
@@ -828,12 +906,7 @@ def handle_run_ci(
906
f"{ci_name} is already running for this commit: {active_build['web_url']}"
907
)
908
831
- current_pr = github.get_pr(pr["number"])
832
- if current_pr["state"] != "open" or current_pr["head"]["sha"] != pr["head"]["sha"]:
833
- return (
834
- "The PR head changed while processing the command. "
835
- f"Comment `{command}` again."
836
- )
909
+ current_pr, warning = prepare_pr_for_ci(github, pr, command)
910
911
build = buildkite.create_build(
912
create_build_payload(
@@ -846,6 +919,7 @@ def handle_run_ci(
919
return (
920
f"Triggered [Buildkite {ci_name} #{build['number']}]({build['web_url']}) "
921
f"for commit `{current_pr['head']['sha'][:12]}`."
922
+ + (f"\n\n{warning}" if warning else "")
923
)
924
925
@@ -1099,6 +1173,12 @@ def run(
1173
raise ValueError(f"Unsupported CI command: {command}")
1174
add_reaction_safely(github, comment_id, "rocket")
1175
github.add_comment(issue_number, f"✅ {message}")
1176
+ except CiPreparationError as error:
1177
+ add_reaction_safely(github, comment_id, "-1")
1178
+ github.add_comment(
1179
+ issue_number,
1180
+ f"❌ {error}\n\n{command_comment_marker(comment_id)}",
1181
+ )
1182
except Exception:
1183
add_reaction_safely(github, comment_id, "confused")
1184
raise
.github/workflows/scripts/test_run_ci_command.py
+196
@@ -22,8 +22,10 @@ from run_ci_command import (
22
COMMAND_RUN_CI_ALL,
23
COMMAND_RUN_CI_NIGHTLY,
24
RETRY_STATES,
25
+ RUN_CI_COMMAND_ENV,
26
ApiError,
27
BuildkiteClient,
28
+ GitHubClient,
29
HttpTransport,
30
authorize,
31
create_build_payload,
@@ -85,6 +87,7 @@ class FakeGitHub:
87
comments: list[str] | None = None,
88
pulls_for_commit: list[dict[str, Any]] | None = None,
89
prs: dict[int, dict[str, Any]] | None = None,
90
+ behind: int = 0,
91
) -> None:
92
self.comments = comments or []
93
self.permission = permission
@@ -95,6 +98,12 @@ class FakeGitHub:
98
self.reviews = reviews or []
99
self.pulls_for_commit = pulls_for_commit or []
100
self.prs = prs or {self.pr["number"]: self.pr}
101
+ self.behind = behind
102
+ self.lag_queries: list[tuple[str, str]] = []
103
+
104
+ def get_commits_behind_base(self, base_ref: str, head_sha: str) -> int:
105
+ self.lag_queries.append((base_ref, head_sha))
106
+ return self.behind
107
108
def get_pr(self, number: int) -> dict[str, Any]:
109
try:
@@ -322,6 +331,11 @@ class RunCiCommandTest(unittest.TestCase):
331
for command in commands:
332
with self.subTest(command=command):
333
self.assertEqual(parse_command(command), command)
334
+ override = f"{command} --allow-stale"
335
+ if " run" in command:
336
+ self.assertEqual(parse_command(override), override)
337
+ else:
338
+ self.assertIsNone(parse_command(override))
339
340
self.assertIsNone(parse_command("/ci run please"))
341
self.assertIsNone(parse_command("/ci run all please"))
@@ -331,6 +345,8 @@ class RunCiCommandTest(unittest.TestCase):
345
self.assertIsNone(parse_command("/amd-ci retry "))
346
self.assertIsNone(parse_command("/AMD-CI run"))
347
self.assertIsNone(parse_command("/amdci run"))
348
+ self.assertIsNone(parse_command("/ci run --allow-stale all"))
349
+ self.assertIsNone(parse_command("/amd-ci run --allow-stale please"))
350
351
def test_commands_select_only_their_configured_pipeline(self) -> None:
352
cases = (
@@ -530,6 +546,177 @@ class RunCiCommandTest(unittest.TestCase):
546
self.assertTrue(github.comments[0].startswith("✅ "))
547
self.assertIn("Buildkite CI #123", github.comments[0])
548
549
+ def test_run_variants_require_current_base_without_changing_commit(self) -> None:
550
+ for command in RUN_CI_COMMAND_ENV:
551
+ if command.endswith(" --allow-stale"):
552
+ continue
553
+ for behind in (0, 1, 5):
554
+ with self.subTest(command=command, behind=behind):
555
+ github = FakeGitHub(
556
+ behind=behind, pr=make_pr(base={"ref": "release/1.0"})
557
+ )
558
+ buildkite = FakeBuildkite([[], []])
559
+ run(make_event(command), github, buildkite)
560
+
561
+ self.assertEqual(
562
+ github.lag_queries,
563
+ [("release/1.0", github.pr["head"]["sha"])],
564
+ )
565
+ if behind == 0:
566
+ self.assertEqual(
567
+ buildkite.created_builds[0]["commit"],
568
+ github.pr["head"]["sha"],
569
+ )
570
+ self.assertIn(
571
+ "](https://buildkite.example/builds/123)",
572
+ github.comments[0],
573
+ )
574
+ else:
575
+ self.assertEqual(buildkite.created_builds, [])
576
+ self.assertTrue(github.comments[0].startswith("❌ "))
577
+ self.assertIn(str(behind), github.comments[0])
578
+ self.assertIn("upstream `release/1.0`", github.comments[0])
579
+ self.assertIn("every commit", github.comments[0])
580
+ self.assertIn(
581
+ "No new CI build was started.", github.comments[0]
582
+ )
583
+ self.assertIn(command, github.comments[0])
584
+
585
+ def test_allow_stale_preserves_author_permissions_and_run_configuration(
586
+ self,
587
+ ) -> None:
588
+ cases = (
589
+ (COMMAND_RUN_CI, "ci", {}),
590
+ (COMMAND_RUN_CI_ALL, "ci", {"RUN_ALL": "1"}),
591
+ (COMMAND_RUN_CI_NIGHTLY, "ci", {"RUN_ALL": "1", "NIGHTLY": "1"}),
592
+ (COMMAND_RUN_AMD_CI, "amd-ci", {}),
593
+ (COMMAND_RUN_AMD_CI_ALL, "amd-ci", {"RUN_ALL": "1"}),
594
+ (
595
+ COMMAND_RUN_AMD_CI_NIGHTLY,
596
+ "amd-ci",
597
+ {"RUN_ALL": "1", "NIGHTLY": "1"},
598
+ ),
599
+ )
600
+ for original_command, pipeline, expected_env in cases:
601
+ command = f"{original_command} --allow-stale"
602
+ for ready, behind in ((False, 100), (True, 0), (True, 100)):
603
+ with self.subTest(command=command, ready=ready, behind=behind):
604
+ github = FakeGitHub(
605
+ permission="read",
606
+ behind=behind,
607
+ pr=make_pr(
608
+ base={"ref": "feature/stack"},
609
+ labels=[{"name": "ready"}] if ready else [],
610
+ ),
611
+ )
612
+ buildkite = FakeBuildkite([[], []])
613
+
614
+ run(make_event(command, "author"), github, buildkite)
615
+
616
+ self.assertEqual(pipeline_for_command(command), pipeline)
617
+ if not ready:
618
+ self.assertEqual(buildkite.created_builds, [])
619
+ self.assertEqual(buildkite.list_calls, [])
620
+ self.assertEqual(github.lag_queries, [])
621
+ self.assertTrue(github.comments[0].startswith("❌ "))
622
+ continue
623
+ self.assertEqual(len(buildkite.created_builds), 1)
624
+ payload = buildkite.created_builds[0]
625
+ self.assertEqual(payload["commit"], github.pr["head"]["sha"])
626
+ self.assertEqual(
627
+ payload["pull_request_base_branch"], "feature/stack"
628
+ )
629
+ self.assertEqual(payload["message"], f"PR #42 {command} by @author")
630
+ self.assertEqual(
631
+ payload["env"],
632
+ {
633
+ "VLLM_CI_GITHUB_COMMENT_ID": "99",
634
+ "VLLM_CI_TRIGGERED_BY": "author",
635
+ **expected_env,
636
+ },
637
+ )
638
+ self.assertEqual(
639
+ github.lag_queries,
640
+ [("feature/stack", github.pr["head"]["sha"])],
641
+ )
642
+ self.assertIn(
643
+ "Buildkite AMD CI #123"
644
+ if pipeline == "amd-ci"
645
+ else "Buildkite CI #123",
646
+ github.comments[0],
647
+ )
648
+ if behind:
649
+ self.assertIn("100 commits behind", github.comments[0])
650
+ self.assertIn("upstream `feature/stack`", github.comments[0])
651
+ self.assertIn("Before merging", github.comments[0])
652
+ self.assertIn("latest PR commit", github.comments[0])
653
+ self.assertIn(original_command, github.comments[0])
654
+
655
+ def test_base_lag_query_qualifies_branch_refs_and_validates_count(self) -> None:
656
+ for base_ref, encoded_ref in (
657
+ ("main", "main"),
658
+ ("release/1.0", "release%2F1.0"),
659
+ ("feature/stack", "feature%2Fstack"),
660
+ ):
661
+ for count in (0, 5, None, -1, True, "5"):
662
+ with self.subTest(base_ref=base_ref, count=count):
663
+ transport = FakeTransport(
664
+ {} if count is None else {"behind_by": count}
665
+ )
666
+ github = GitHubClient("token", "vllm-project/vllm", transport)
667
+ if type(count) is int and count >= 0:
668
+ self.assertEqual(
669
+ github.get_commits_behind_base(base_ref, "head-sha"), count
670
+ )
671
+ else:
672
+ with self.assertRaises(ApiError):
673
+ github.get_commits_behind_base(base_ref, "head-sha")
674
+ self.assertEqual(
675
+ transport.calls[0]["url"],
676
+ "https://api.github.com/repos/vllm-project/vllm/compare/"
677
+ f"refs%2Fheads%2F{encoded_ref}...head-sha?per_page=1",
678
+ )
679
+ self.assertEqual(transport.calls[0]["method"], "GET")
680
+
681
+ def test_unknown_lag_or_changed_pr_prevents_runs(self) -> None:
682
+ for command in (
683
+ COMMAND_RUN_CI,
684
+ COMMAND_RUN_AMD_CI,
685
+ f"{COMMAND_RUN_CI} --allow-stale",
686
+ f"{COMMAND_RUN_AMD_CI} --allow-stale",
687
+ ):
688
+ for fault in ("api", "head", "closed", "base"):
689
+ with self.subTest(command=command, fault=fault):
690
+ github = FakeGitHub()
691
+ buildkite = FakeBuildkite([[], []])
692
+
693
+ def check(
694
+ base_ref: str,
695
+ sha: str,
696
+ fault: str = fault,
697
+ github: FakeGitHub = github,
698
+ ) -> int:
699
+ if fault == "api":
700
+ raise ApiError(503, "Unavailable")
701
+ changed = make_pr()
702
+ if fault == "head":
703
+ changed["head"]["sha"] = "another-head"
704
+ elif fault == "closed":
705
+ changed["state"] = "closed"
706
+ else:
707
+ changed["base"]["ref"] = "another-base"
708
+ github.prs[42] = changed
709
+ return 0
710
+
711
+ with patch.object(
712
+ github, "get_commits_behind_base", side_effect=check
713
+ ):
714
+ run(make_event(command), github, buildkite)
715
+ self.assertEqual(buildkite.created_builds, [])
716
+ self.assertEqual(buildkite.retry_calls, [])
717
+ self.assertTrue(github.comments[0].startswith("❌ "))
718
+ self.assertNotIn("Triggered", github.comments[0])
719
+
720
def test_amd_run_ignores_blocked_builds(self) -> None:
721
for metadata in ({}, {"github-comment-id": "98"}):
722
with self.subTest(metadata=metadata):
@@ -627,6 +814,7 @@ class RunCiCommandTest(unittest.TestCase):
814
run(make_event(COMMAND_RUN_CI, "author"), github, buildkite)
815
816
self.assertEqual(buildkite.list_calls, [])
817
+ self.assertEqual(github.lag_queries, [])
818
self.assertEqual(github.reactions, ["eyes"])
819
self.assertTrue(github.comments[0].startswith("❌ "))
820
self.assertIn("approve the PR", github.comments[0])
@@ -848,6 +1036,7 @@ class RunCiCommandTest(unittest.TestCase):
1036
1037
def test_ci_retry_retries_failed_jobs_while_build_is_running(self) -> None:
1038
github = FakeGitHub(
1039
+ behind=100,
1040
permission="read",
1041
pr=make_pr(labels=[{"name": "ready"}]),
1042
)
@@ -867,10 +1056,12 @@ class RunCiCommandTest(unittest.TestCase):
1056
run(make_event(COMMAND_RETRY_FAILED, "author"), github, buildkite)
1057
1058
self.assertEqual(buildkite.retry_calls, [(123, RETRY_STATES)])
1059
+ self.assertEqual(github.lag_queries, [])
1060
self.assertIn("Queued 3 failed job", github.comments[0])
1061
1062
def test_amd_ci_retry_retries_only_the_current_head_build(self) -> None:
1063
github = FakeGitHub(
1064
+ behind=100,
1065
permission="read",
1066
pr=make_pr(labels=[{"name": "ready"}]),
1067
)
@@ -891,6 +1082,7 @@ class RunCiCommandTest(unittest.TestCase):
1082
run(make_event(COMMAND_RETRY_AMD_FAILED, "author"), github, buildkite)
1083
1084
self.assertEqual(buildkite.retry_calls, [(321, RETRY_STATES)])
1085
+ self.assertEqual(github.lag_queries, [])
1086
self.assertIn("Buildkite AMD CI #321", github.comments[0])
1087
1088
def test_amd_ci_retry_requires_a_build_for_the_current_head(self) -> None:
@@ -912,6 +1104,7 @@ class RunCiCommandTest(unittest.TestCase):
1104
1105
def test_ci_retry_creates_filtered_build_for_new_head(self) -> None:
1106
github = FakeGitHub(
1107
+ behind=100,
1108
permission="read",
1109
pr=make_pr(labels=[{"name": "ready"}]),
1110
)
@@ -953,6 +1146,7 @@ class RunCiCommandTest(unittest.TestCase):
1146
self.assertEqual(len(buildkite.created_builds), 1)
1147
payload = buildkite.created_builds[0]
1148
self.assertEqual(payload["commit"], "0123456789abcdef")
1149
+ self.assertEqual(github.lag_queries, [])
1150
self.assertEqual(payload["message"], "PR #42 /ci retry by @author")
1151
self.assertEqual(
1152
json.loads(payload["env"]["VLLM_CI_ONLY_STEP_KEYS"]),
@@ -1104,6 +1298,7 @@ class RunCiCommandTest(unittest.TestCase):
1298
run(make_event(COMMAND_CANCEL_CI), github, buildkite)
1299
1300
self.assertEqual(buildkite.cancel_calls, [123, 124, 126])
1301
+ self.assertEqual(github.lag_queries, [])
1302
self.assertEqual(
1303
[request["branch"] for request in buildkite.list_requests],
1304
["contributor:feature", "feature"],
@@ -1152,6 +1347,7 @@ class RunCiCommandTest(unittest.TestCase):
1347
run(make_event(COMMAND_CANCEL_AMD_CI), github, buildkite)
1348
1349
self.assertEqual(buildkite.cancel_calls, [322, 321])
1350
+ self.assertEqual(github.lag_queries, [])
1351
self.assertEqual(
1352
[request["branch"] for request in buildkite.list_requests],
1353
["contributor:feature", "feature"],
docs/contributing/README.md
+28
@@ -315,6 +315,34 @@ review process:
315
the `ready` label, the PR author can use `/ci run`, `/ci retry`, `/ci cancel`,
316
or the corresponding `/amd-ci` variants. New commits do not start upstream
317
CI automatically.
318
+- Before creating a Buildkite build, `/ci run` and `/amd-ci run`, including
319
+ `all` and `nightly`, check the requested PR commit against its target branch
320
+ in the upstream repository. Your branch must contain every commit currently
321
+ on that branch: **zero commits behind**. This also applies to PRs targeting
322
+ release branches or another PR's branch.
323
+- If the PR is behind, the bot reports the count without creating a new build.
324
+ Merge or rebase onto the latest target branch, then rerun the command. If the
325
+ target branch advances in the meantime, update your branch and try again.
326
+- To run CI on an outdated branch at your own risk, append `--allow-stale` to
327
+ any run command, for example `/ci run --allow-stale` or
328
+ `/amd-ci run all --allow-stale`. The same authorization requirements apply.
329
+ The bot reports the lag and warns that outdated CI configuration may cause
330
+ failures. Before merging, merge or rebase onto the latest target branch and
331
+ rerun CI without `--allow-stale` on the latest PR commit.
332
+- Retrying or cancelling builds does not check branch freshness.
333
+- These commands do not modify your branch or enforce merge requirements.
334
+ Direct Buildkite launches are outside this workflow. No additional token is
335
+ needed.
336
+
337
+Maintainers must configure required Buildkite status checks on the latest PR
338
+commit and enable GitHub's
339
+[Require branches to be up to date before merging](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches#require-status-checks-before-merging)
340
+protection on the relevant target branches to enforce the merge policy. Select
341
+statuses that confirm the required tests completed, using Buildkite as their
342
+expected source. An aggregate status that reports "passed and blocked" does not
343
+by itself guarantee those tests ran; verify the pipeline's required and optional
344
+blocked steps before choosing statuses or changing blocked-build reporting.
345
+The CI command workflow does not configure these repository or pipeline settings.
346
347
### Pull Request Limits and Escalation
348