[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