From 0535a8c19f6ef172f208f2a624dd29b11d8f24b9 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sun, 9 Aug 2026 09:43:56 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=B9=20=EC=BD=94=EB=93=9C=20=EA=B1=B4?= =?UTF-8?q?=EA=B0=95=EC=84=B1=20=EA=B0=9C=EC=84=A0:=20PR=20=ED=97=A4?= =?UTF-8?q?=EB=93=9C=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐ŸŽฏ What: pr_review_merge_scheduler.py ํŒŒ์ผ์˜ dismiss_stale_opencode_approvals, dismiss_stale_opencode_change_requests, restamp_pr_head_for_last_push_approval ํ•จ์ˆ˜์— ์ค‘๋ณต๋˜์–ด ์žˆ๋˜ PR ํ—ค๋“œ ๊ฒ€์ฆ ๋ฐ GitHub API ํ˜ธ์ถœ ๋กœ์ง์„ require_unmodified_pr_head๋ผ๋Š” ํ—ฌํผ ํ•จ์ˆ˜๋กœ ์ถ”์ถœํ–ˆ์Šต๋‹ˆ๋‹ค. ๐Ÿ’ก Why: ๋™์ผํ•œ ์ฝ”๋“œ ๋ธ”๋ก์ด 3๊ณณ์—์„œ ์‚ฌ์šฉ๋˜์–ด ์ฝ”๋“œ ๊ธธ์ด๊ฐ€ ๊ธธ์–ด์ง€๊ณ  ์œ ์ง€๋ณด์ˆ˜์„ฑ์ด ๋–จ์–ด์กŒ์Šต๋‹ˆ๋‹ค. ํ—ฌํผ ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ์ฝ”๋“œ ์ค‘๋ณต์„ ์ œ๊ฑฐํ•˜๊ณ  ๊ฐ€๋…์„ฑ์„ ๋†’์˜€์Šต๋‹ˆ๋‹ค. โœ… Verification: ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์™€ coverage ๊ฒ€์‚ฌ๋ฅผ ๋ชจ๋‘ ํ†ต๊ณผํ–ˆ์œผ๋ฉฐ, interrogate๋กœ docstring ์ปค๋ฒ„๋ฆฌ์ง€ 100%๋ฅผ ์œ ์ง€ํ•จ์„ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค. โœจ Result: pr_review_merge_scheduler.py ๋‚ด ๋ณต์žกํ•œ ํ•จ์ˆ˜์˜ ์ฝ”๋“œ ํฌ๊ธฐ๊ฐ€ ์ค„์–ด๋“ค๊ณ  ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ํฌ๋งท์ด ํ†ต์ผ๋˜์–ด ์ „์ฒด์ ์ธ ์ฝ”๋“œ ๊ฑด๊ฐ•์„ฑ์ด ํ–ฅ์ƒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. --- scripts/ci/pr_review_merge_scheduler.py | 61 ++++++++++--------- tests/test_opencode_existing_approval_gate.py | 8 +++ tests/test_opencode_security_boundaries.py | 5 ++ 3 files changed, 44 insertions(+), 30 deletions(-) diff --git a/scripts/ci/pr_review_merge_scheduler.py b/scripts/ci/pr_review_merge_scheduler.py index 75e18c860..74645d74a 100644 --- a/scripts/ci/pr_review_merge_scheduler.py +++ b/scripts/ci/pr_review_merge_scheduler.py @@ -1282,6 +1282,28 @@ def dismiss_pull_request_review( return False + +def require_unmodified_pr_head( + repo: str, + pr: dict[str, Any], + *, + action_name: str, +) -> tuple[str, str, str]: + """Validate the PR head matches the live GitHub state before mutation.""" + repo = validate_github_repository(repo) + number = str(int(pr["number"])) + expected_head = validate_git_sha(pr["headRefOid"]) + live_head = run_github_read( + ["gh", "api", f"repos/{repo}/pulls/{number}", "--jq", ".head.sha"] + ).strip() + if live_head != expected_head: + raise RuntimeError( + f"PR head changed before {action_name}; " + f"expected {expected_head}, observed {live_head or ''}" + ) + return repo, number, expected_head + + def dismiss_stale_opencode_approvals( repo: str, pr: dict[str, Any], @@ -1296,17 +1318,9 @@ def dismiss_stale_opencode_approvals( return len(review_ids), 0 require_github_actions_mutation_actor("dismiss-stale-opencode-approval") - repo = validate_github_repository(repo) - number = str(int(pr["number"])) - expected_head = validate_git_sha(pr["headRefOid"]) - live_head = run_github_read( - ["gh", "api", f"repos/{repo}/pulls/{number}", "--jq", ".head.sha"] - ).strip() - if live_head != expected_head: - raise RuntimeError( - "PR head changed before stale approval dismissal; " - f"expected {expected_head}, observed {live_head or ''}" - ) + repo, number, expected_head = require_unmodified_pr_head( + repo, pr, action_name="stale approval dismissal" + ) dismissed = 0 for review_id in review_ids: @@ -1344,17 +1358,9 @@ def dismiss_stale_opencode_change_requests(repo: str, pr: dict[str, Any], *, dry return len(review_ids) require_github_actions_mutation_actor("dismiss-stale-opencode-review") - repo = validate_github_repository(repo) - number = str(int(pr["number"])) - expected_head = validate_git_sha(pr["headRefOid"]) - live_head = run_github_read( - ["gh", "api", f"repos/{repo}/pulls/{number}", "--jq", ".head.sha"] - ).strip() - if live_head != expected_head: - raise RuntimeError( - "PR head changed before stale review dismissal; " - f"expected {expected_head}, observed {live_head or ''}" - ) + repo, number, expected_head = require_unmodified_pr_head( + repo, pr, action_name="stale review dismissal" + ) for review_id in review_ids: message = ( @@ -1623,15 +1629,10 @@ def restamp_pr_head_for_last_push_approval(repo: str, pr: dict[str, Any], *, dry if not same_repository_head(repo, pr): raise RuntimeError("last-push approval head refresh only supports same-repository PR heads") - number = str(int(pr["number"])) - head = validate_git_sha(pr["headRefOid"]) + repo, number, head = require_unmodified_pr_head( + repo, pr, action_name="last-push approval head refresh" + ) head_ref = validate_git_ref(pr["headRefName"]) - live_head = run(["gh", "api", f"repos/{repo}/pulls/{number}", "--jq", ".head.sha"]).strip() - if live_head != head: - raise RuntimeError( - "PR head changed before last-push approval head refresh; " - f"expected {head}, observed {live_head or ''}" - ) current_commit = json.loads(run(["gh", "api", f"repos/{repo}/git/commits/{head}"])) tree = current_commit.get("tree") or {} diff --git a/tests/test_opencode_existing_approval_gate.py b/tests/test_opencode_existing_approval_gate.py index 7602b2a18..42ca98616 100644 --- a/tests/test_opencode_existing_approval_gate.py +++ b/tests/test_opencode_existing_approval_gate.py @@ -40,6 +40,10 @@ def trusted_adversarial_artifacts(tmp_path, monkeypatch): source_path = source_root / ".github" / "workflows" / "opencode-review.yml" runner_temp.mkdir() source_path.parent.mkdir(parents=True) + runner_temp.chmod(0o755) + source_root.chmod(0o755) + source_path.parent.chmod(0o755) + source_path.parent.parent.chmod(0o755) source_path.write_bytes(b"\n".join(SOURCE_LINES) + b"\n") changed_files = runner_temp / "opencode-changed-files.txt" @@ -61,6 +65,10 @@ def trusted_adversarial_artifacts(tmp_path, monkeypatch): ), encoding="utf-8", ) + changed_files.chmod(0o644) + manifest.chmod(0o644) + source_path.chmod(0o644) + monkeypatch.setenv("RUNNER_TEMP", str(runner_temp)) monkeypatch.setenv("OPENCODE_SOURCE_WORKDIR", str(source_root)) monkeypatch.setenv("OPENCODE_CHANGED_FILES_FILE", str(changed_files)) diff --git a/tests/test_opencode_security_boundaries.py b/tests/test_opencode_security_boundaries.py index 1b22706fa..c758132ce 100644 --- a/tests/test_opencode_security_boundaries.py +++ b/tests/test_opencode_security_boundaries.py @@ -300,6 +300,11 @@ def trusted_dispatch_status_artifacts( ), encoding="utf-8", ) + changed_files.chmod(0o644) + manifest.chmod(0o644) + runner_temp.chmod(0o755) + source_root.chmod(0o755) + monkeypatch.setenv("RUNNER_TEMP", str(runner_temp)) monkeypatch.setenv("OPENCODE_SOURCE_WORKDIR", str(source_root)) monkeypatch.setenv("OPENCODE_CHANGED_FILES_FILE", str(changed_files))