Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
bump: patch
---

### Fixed

- The pr-pipeline convention names the repository's default branch, not the pull request's base, as where the merge gate reads admission's `task-list` for a size-split sub-issue.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
bump: patch
---

### Fixed

- The pr-merge gate reads admission's `task-list` for a size-split sub-issue from `admission-rules.json` at the repository's default branch, as pr-verify reads it from main's checkout, instead of at the pull request's base branch. A failed read of the default branch refuses `verify:unreadable`.
2 changes: 1 addition & 1 deletion docs/conventions/pr-pipeline/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1568,7 +1568,7 @@ head:
opens with the split header for that parent, every line after the header is one of the parent's
trusted task-list lines (a line of the parent's listed-human body or comments that starts with
admission's `task-list`, "- [ ] " with its trailing space, from
`.github/actions/resolve-config/admission-rules.json` at the base branch; a missing or empty `task-list` counts nothing), and either nobody edited it or a
`.github/actions/resolve-config/admission-rules.json` at the default branch, as resolve-pr reads it from main's checkout, whatever the pull request's base; a missing or empty `task-list` counts nothing), and either nobody edited it or a
listed human last edited it. A header whose third line is
`<!-- size-split refinement=<id> updated_at=<time> -->` also allows that refinement comment's
task-list lines, never its prose, heading or marker lines, and counts only while that comment is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1129,11 +1129,16 @@ def _lane_app_id(self) -> int | None:
return app_id if type(app_id) is int else None

def _task_list(self) -> str | None:
"""Admission's `task-list` at the branch, as resolve-pr.mjs reads it
from main's checkout, shared by the run's gate processes; None when
the rules hold no string there. A failed read raises `_Unreadable`."""
key = f"{self.repo}@{self.branch}"
"""Admission's `task-list` at the repository's default branch, as
resolve-pr.mjs reads it from main's checkout whatever the pull
request's base, shared by the run's gate processes; None when the
rules hold no string there. A failed read raises `_Unreadable`."""
if not self._task_list_read:
try:
default = merge_policy._default_branch(self.repo, shared=True)
except merge_policy.PolicyReadError as exc:
raise _Unreadable(str(exc)) from exc
key = f"{self.repo}@{default}"
shared = run_cache.get_dated("task-list", key, run_cache.MAX_AGE)
if shared is not None:
self._task_list_value, at = shared
Expand All @@ -1145,7 +1150,7 @@ def _task_list(self) -> str | None:
"api",
"-H",
"Accept: application/vnd.github.raw",
f"repos/{self.repo}/contents/{ADMISSION_RULES_PATH}?ref={self.branch}",
f"repos/{self.repo}/contents/{ADMISSION_RULES_PATH}?ref={default}",
],
"admission rules",
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,10 @@ def __init__(
self.actors = {"version": 1, "actors": actors}
self.rules = {} if task_list is None else {"task-list": task_list}
self.issue = issue
self.default_branch = "main"
self.fail = False
self.rules_fail = False
self.default_fail = False
self.calls: list[tuple[list[str], Any]] = []

def graphql_calls(self) -> int:
Expand All @@ -132,10 +134,23 @@ def rules_calls(self) -> int:
if any(ve.ADMISSION_RULES_PATH in a for a in args)
)

def rules_refs(self) -> list[str]:
return [
a.rpartition("?ref=")[2]
for args, _ in self.calls
for a in args
if ve.ADMISSION_RULES_PATH in a
]

def __call__(
self, args: list[str], **kwargs: Any
) -> subprocess.CompletedProcess[str]:
self.calls.append((args, kwargs.get("env_overrides")))
if args == ["api", f"repos/{REPO}"]:
if self.default_fail:
return subprocess.CompletedProcess(args, 1, "", "gh: boom (HTTP 502)")
answer = {"default_branch": self.default_branch}
return subprocess.CompletedProcess(args, 0, json.dumps(answer), "")
if args[:2] == ["api", "graphql"]:
assert any(a == f"query={ve.SPLIT_CHILD_QUERY}" for a in args), args
if self.fail:
Expand Down Expand Up @@ -163,6 +178,7 @@ def judge(
with (
mock.patch.dict("os.environ", {}, clear=False) as env,
mock.patch.object(postmerge, "gh_capture", side_effect=fake),
mock.patch.object(ve.merge_policy, "gh_capture", side_effect=fake),
):
env.pop("BABYSIT_RUN_CACHE", None)
found = ve.Reader(REPO, "main", agent_identity=identity, **reader)
Expand Down Expand Up @@ -275,6 +291,12 @@ def test_unreadable_rules_are_unreadable(self) -> None:
with self.assertRaises(ve._Unreadable):
judge(fake, linked(self.case["child"]), LANES_LOGIN)

def test_an_unreadable_default_branch_is_unreadable(self) -> None:
fake = self.fake()
fake.default_fail = True
with self.assertRaises(ve._Unreadable):
judge(fake, linked(self.case["child"]), LANES_LOGIN)

def test_unreadable_comments(self) -> None:
# More comments than the first 100 the query reads.
fake = self.fake()
Expand All @@ -288,6 +310,30 @@ def test_a_missing_or_empty_task_list_counts_nothing(self) -> None:
self.assertFalse(judge(fake, linked(self.case["child"]), LANES_LOGIN))


class TheRulesComeFromTheDefaultBranch(unittest.TestCase):
"""resolve-pr.mjs reads admission's rules from main's checkout, so the
gate reads them at the default branch whatever the pull request's base."""

def test_a_feature_base_reads_the_rules_at_the_default_branch(self) -> None:
data = json.loads(VECTORS.read_text(encoding="utf-8"))
case = next(c for c in data["cases"] if c["name"] == "the trusted child")
fake = FakeGitHub(
data["humans"], data["app_id"], graphql_issue(case, data["parents"])
)
fake.default_branch = "trunk"
with (
mock.patch.dict("os.environ", {}, clear=False) as env,
mock.patch.object(postmerge, "gh_capture", side_effect=fake),
mock.patch.object(ve.merge_policy, "gh_capture", side_effect=fake),
):
env.pop("BABYSIT_RUN_CACHE", None)
reader = ve.Reader(REPO, "feature-x", agent_identity=LANES_LOGIN)
self.assertTrue(
reader._trusted_spec({"linked_issues": [linked(case["child"])]})
)
self.assertEqual(fake.rules_refs(), ["trunk"])


class OneReadPerChild(unittest.TestCase):
def setUp(self) -> None:
self.data = json.loads(VECTORS.read_text(encoding="utf-8"))
Expand All @@ -303,6 +349,7 @@ def test_a_child_judged_twice_is_read_once_under_the_actions_token(self) -> None
with (
mock.patch.dict("os.environ", {}, clear=False) as env,
mock.patch.object(postmerge, "gh_capture", side_effect=self.fake),
mock.patch.object(ve.merge_policy, "gh_capture", side_effect=self.fake),
):
env.pop("BABYSIT_RUN_CACHE", None)
reader = ve.Reader(
Expand All @@ -323,6 +370,7 @@ def test_a_reused_answer_notes_when_it_was_read(self) -> None:
with (
mock.patch.dict("os.environ", {}, clear=False) as env,
mock.patch.object(postmerge, "gh_capture", side_effect=self.fake),
mock.patch.object(ve.merge_policy, "gh_capture", side_effect=self.fake),
mock.patch.object(ve.run_cache, "now", side_effect=lambda: next(ticks)),
):
env.pop("BABYSIT_RUN_CACHE", None)
Expand Down
Loading