Skip to content

Commit 2388575

Browse files
committed
ci: fix remaining issues with #2691
1 parent 910b12d commit 2388575

2 files changed

Lines changed: 131 additions & 12 deletions

File tree

.github/scripts/pr_labels.py

Lines changed: 78 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import subprocess
99
import sys
1010
from collections.abc import Sequence
11+
from dataclasses import dataclass
1112
from typing import Any, Final
1213

1314
ALLOWED_LABELS: Final[set[str]] = {
@@ -57,6 +58,14 @@
5758
"src/agents/models/",
5859
)
5960

61+
PR_CONTEXT_DEFAULT_PATH = ".tmp/pr-labels/pr-context.json"
62+
63+
64+
@dataclass(frozen=True)
65+
class PRContext:
66+
title: str = ""
67+
body: str = ""
68+
6069

6170
def read_file_at(commit: str | None, path: str) -> str | None:
6271
if not commit:
@@ -214,6 +223,28 @@ def load_json(path: pathlib.Path) -> Any:
214223
return json.loads(path.read_text())
215224

216225

226+
def load_pr_context(path: pathlib.Path) -> PRContext:
227+
if not path.exists():
228+
return PRContext()
229+
230+
try:
231+
payload = load_json(path)
232+
except json.JSONDecodeError:
233+
return PRContext()
234+
235+
if not isinstance(payload, dict):
236+
return PRContext()
237+
238+
title = payload.get("title", "")
239+
body = payload.get("body", "")
240+
if not isinstance(title, str):
241+
title = ""
242+
if not isinstance(body, str):
243+
body = ""
244+
245+
return PRContext(title=title, body=body)
246+
247+
217248
def load_codex_labels(path: pathlib.Path) -> tuple[list[str], bool]:
218249
if not path.exists():
219250
return [], False
@@ -248,8 +279,22 @@ def fetch_existing_labels(pr_number: str) -> set[str]:
248279
return {label for label in result.splitlines() if label}
249280

250281

282+
def infer_title_intent_labels(pr_context: PRContext) -> set[str]:
283+
normalized_title = pr_context.title.strip().lower()
284+
285+
bug_prefixes = ("fix:", "fix(", "bug:", "bugfix:", "hotfix:", "regression:")
286+
enhancement_prefixes = ("feat:", "feat(", "feature:", "enhancement:")
287+
288+
if normalized_title.startswith(bug_prefixes):
289+
return {"bug"}
290+
if normalized_title.startswith(enhancement_prefixes):
291+
return {"enhancement"}
292+
return set()
293+
294+
251295
def compute_desired_labels(
252296
*,
297+
pr_context: PRContext,
253298
changed_files: Sequence[str],
254299
diff_text: str,
255300
codex_ran: bool,
@@ -259,6 +304,11 @@ def compute_desired_labels(
259304
head_sha: str | None,
260305
) -> set[str]:
261306
desired: set[str] = set()
307+
codex_label_set = {label for label in codex_labels if label in ALLOWED_LABELS}
308+
codex_feature_labels = codex_label_set & FEATURE_LABELS
309+
codex_model_only_labels = codex_label_set & MODEL_ONLY_LABELS
310+
fallback_feature_labels = infer_fallback_labels(changed_files)
311+
title_intent_labels = infer_title_intent_labels(pr_context)
262312

263313
if "pyproject.toml" in changed_files:
264314
desired.add("project")
@@ -274,21 +324,30 @@ def compute_desired_labels(
274324
if dependencies_allowed:
275325
desired.add("dependencies")
276326

277-
if codex_ran and codex_output_valid:
278-
for label in codex_labels:
279-
if label == "dependencies" and not dependencies_allowed:
280-
continue
281-
if label in ALLOWED_LABELS:
282-
desired.add(label)
283-
return desired
327+
if codex_ran and codex_output_valid and codex_feature_labels:
328+
desired.update(codex_feature_labels)
329+
else:
330+
desired.update(fallback_feature_labels)
331+
332+
if title_intent_labels:
333+
desired.update(title_intent_labels)
334+
elif codex_ran and codex_output_valid:
335+
desired.update(codex_model_only_labels)
284336

285-
desired.update(infer_fallback_labels(changed_files))
286337
return desired
287338

288339

289-
def compute_managed_labels(*, codex_ran: bool, codex_output_valid: bool) -> set[str]:
340+
def compute_managed_labels(
341+
*,
342+
pr_context: PRContext,
343+
codex_ran: bool,
344+
codex_output_valid: bool,
345+
codex_labels: Sequence[str],
346+
) -> set[str]:
290347
managed = DETERMINISTIC_LABELS | FEATURE_LABELS
291-
if codex_ran and codex_output_valid:
348+
title_intent_labels = infer_title_intent_labels(pr_context)
349+
codex_label_set = {label for label in codex_labels if label in MODEL_ONLY_LABELS}
350+
if title_intent_labels or (codex_ran and codex_output_valid and codex_label_set):
292351
managed |= MODEL_ONLY_LABELS
293352
return managed
294353

@@ -303,6 +362,10 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
303362
default=os.environ.get("CODEX_OUTPUT_PATH", ".tmp/codex/outputs/pr-labels.json"),
304363
)
305364
parser.add_argument("--codex-conclusion", default=os.environ.get("CODEX_CONCLUSION", ""))
365+
parser.add_argument(
366+
"--pr-context-path",
367+
default=os.environ.get("PR_CONTEXT_PATH", PR_CONTEXT_DEFAULT_PATH),
368+
)
306369
parser.add_argument(
307370
"--changed-files-path",
308371
default=os.environ.get("CHANGED_FILES_PATH", ".tmp/pr-labels/changed-files.txt"),
@@ -322,8 +385,10 @@ def main(argv: Sequence[str] | None = None) -> int:
322385
changed_files_path = pathlib.Path(args.changed_files_path)
323386
changes_diff_path = pathlib.Path(args.changes_diff_path)
324387
codex_output_path = pathlib.Path(args.codex_output_path)
388+
pr_context_path = pathlib.Path(args.pr_context_path)
325389
codex_conclusion = args.codex_conclusion.strip().lower()
326390
codex_ran = bool(codex_conclusion) and codex_conclusion != "skipped"
391+
pr_context = load_pr_context(pr_context_path)
327392

328393
changed_files = []
329394
if changed_files_path.exists():
@@ -339,6 +404,7 @@ def main(argv: Sequence[str] | None = None) -> int:
339404
"model-only labels."
340405
)
341406
desired = compute_desired_labels(
407+
pr_context=pr_context,
342408
changed_files=changed_files,
343409
diff_text=diff_text,
344410
codex_ran=codex_ran,
@@ -350,8 +416,10 @@ def main(argv: Sequence[str] | None = None) -> int:
350416

351417
existing = fetch_existing_labels(args.pr_number)
352418
managed_labels = compute_managed_labels(
419+
pr_context=pr_context,
353420
codex_ran=codex_ran,
354421
codex_output_valid=codex_output_valid,
422+
codex_labels=codex_labels,
355423
)
356424
to_add = sorted(desired - existing)
357425
to_remove = sorted((existing & managed_labels) - desired)

tests/test_pr_labels.py

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import sys
34
from importlib.util import module_from_spec, spec_from_file_location
45
from pathlib import Path
56
from types import ModuleType
@@ -13,6 +14,7 @@ def load_pr_labels_module() -> Any:
1314
assert spec.loader is not None
1415
module = module_from_spec(spec)
1516
assert isinstance(module, ModuleType)
17+
sys.modules[spec.name] = module
1618
spec.loader.exec_module(module)
1719
return cast(Any, module)
1820

@@ -40,6 +42,7 @@ def test_infer_fallback_labels_marks_core_for_runtime_changes() -> None:
4042

4143
def test_compute_desired_labels_removes_stale_fallback_labels() -> None:
4244
desired = pr_labels.compute_desired_labels(
45+
pr_context=pr_labels.PRContext(),
4346
changed_files=["src/agents/models/chatcmpl_converter.py"],
4447
diff_text="",
4548
codex_ran=False,
@@ -54,6 +57,7 @@ def test_compute_desired_labels_removes_stale_fallback_labels() -> None:
5457

5558
def test_compute_desired_labels_falls_back_when_codex_output_is_invalid() -> None:
5659
desired = pr_labels.compute_desired_labels(
60+
pr_context=pr_labels.PRContext(),
5761
changed_files=["src/agents/run_internal/approvals.py"],
5862
diff_text="",
5963
codex_ran=True,
@@ -66,9 +70,56 @@ def test_compute_desired_labels_falls_back_when_codex_output_is_invalid() -> Non
6670
assert desired == {"feature:core"}
6771

6872

69-
def test_compute_managed_labels_preserves_model_only_labels_without_valid_codex_output() -> None:
70-
managed = pr_labels.compute_managed_labels(codex_ran=True, codex_output_valid=False)
73+
def test_compute_desired_labels_uses_fallback_feature_labels_when_codex_valid_but_empty() -> None:
74+
desired = pr_labels.compute_desired_labels(
75+
pr_context=pr_labels.PRContext(),
76+
changed_files=["src/agents/run_internal/approvals.py"],
77+
diff_text="",
78+
codex_ran=True,
79+
codex_output_valid=True,
80+
codex_labels=[],
81+
base_sha=None,
82+
head_sha=None,
83+
)
84+
85+
assert desired == {"feature:core"}
86+
87+
88+
def test_compute_desired_labels_infers_bug_from_fix_title() -> None:
89+
desired = pr_labels.compute_desired_labels(
90+
pr_context=pr_labels.PRContext(title="fix: stop streamed tool execution"),
91+
changed_files=["src/agents/run_internal/approvals.py"],
92+
diff_text="",
93+
codex_ran=True,
94+
codex_output_valid=True,
95+
codex_labels=[],
96+
base_sha=None,
97+
head_sha=None,
98+
)
99+
100+
assert desired == {"bug", "feature:core"}
101+
102+
103+
def test_compute_managed_labels_preserves_model_only_labels_without_signal() -> None:
104+
managed = pr_labels.compute_managed_labels(
105+
pr_context=pr_labels.PRContext(),
106+
codex_ran=True,
107+
codex_output_valid=True,
108+
codex_labels=[],
109+
)
71110

72111
assert "bug" not in managed
73112
assert "enhancement" not in managed
74113
assert "feature:core" in managed
114+
115+
116+
def test_compute_managed_labels_manages_model_only_labels_with_fix_title() -> None:
117+
managed = pr_labels.compute_managed_labels(
118+
pr_context=pr_labels.PRContext(title="fix: stop streamed tool execution"),
119+
codex_ran=True,
120+
codex_output_valid=True,
121+
codex_labels=[],
122+
)
123+
124+
assert "bug" in managed
125+
assert "enhancement" in managed

0 commit comments

Comments
 (0)