diff --git a/.github/actions/noema-review/two_phase.py b/.github/actions/noema-review/two_phase.py old mode 100644 new mode 100755 index 1cab5aa411..4137556a96 --- a/.github/actions/noema-review/two_phase.py +++ b/.github/actions/noema-review/two_phase.py @@ -28,6 +28,8 @@ ENVELOPE_SCHEMA_VERSION = 1 MAX_ENVELOPE_BYTES = 2 * 1024 * 1024 +CANONICAL_APP_TOKEN_SOURCE = "noema-review-github-app" +REFRESHED_APP_TOKEN_SOURCE = "noema-review-github-app-refresh" def _canonical_head(value: str) -> str: @@ -46,9 +48,25 @@ def _canonical_base(pull_request: dict[str, Any]) -> str: return base -def _reviewer_actor() -> str: +def _current_actor(*, allow_refreshed_app: bool) -> str: + """Validate the refresh marker through the existing canonical App gate.""" + token_source = os.environ.get("NOEMA_REVIEW_TOKEN_SOURCE") + if not ( + allow_refreshed_app + and token_source == REFRESHED_APP_TOKEN_SOURCE + ): + return gate.current_actor() + + os.environ["NOEMA_REVIEW_TOKEN_SOURCE"] = CANONICAL_APP_TOKEN_SOURCE + try: + return gate.current_actor() + finally: + os.environ["NOEMA_REVIEW_TOKEN_SOURCE"] = token_source + + +def _reviewer_actor(*, allow_refreshed_app: bool = False) -> str: """Return a verified independent reviewer actor for the active token.""" - actor = gate.current_actor() + actor = _current_actor(allow_refreshed_app=allow_refreshed_app) if not actor: raise RuntimeError("Noema reviewer identity could not be verified") if actor in gate.PRIMARY_REVIEW_AUTHORS: @@ -219,7 +237,7 @@ def publish_verdict(repo: str, number: int, expected_head: str, path: Path) -> i if _canonical_base(current_pull_request) != expected_base: print("Pull request base advanced after model review; stale prepared verdict was not published.") return 0 - actor = _reviewer_actor() + actor = _reviewer_actor(allow_refreshed_app=True) if current_pull_request.get("isDraft"): print("PR became draft after model review; prepared verdict was not published.") return 0 diff --git a/.github/workflows/noema-token-lifetime-quality-ci.yml b/.github/workflows/noema-token-lifetime-quality-ci.yml index 3de8f18ab3..cfcd722ee4 100644 --- a/.github/workflows/noema-token-lifetime-quality-ci.yml +++ b/.github/workflows/noema-token-lifetime-quality-ci.yml @@ -7,6 +7,7 @@ on: - .github/actions/noema-review/two_phase.py - tests/test_noema_reviewer_token_lifetime.py - tests/test_noema_two_phase_handoff.py + - tests/test_noema_refreshed_app_identity.py - docs/doctoring/noema-review-token-lifetime.md - docs/product-technical-gap-baseline.md - CHANGELOG.md @@ -27,10 +28,18 @@ jobs: persist-credentials: false - name: Install pinned review CI dependencies run: >- - python3 -m pip install --disable-pip-version-check --require-hashes --only-binary=:all: -r requirements-opencode-review-ci-hashes.txt + python3 -m pip install --disable-pip-version-check --require-hashes + --only-binary=:all: -r requirements-opencode-review-ci-hashes.txt - name: Verify token-lifetime handoff contracts run: | set -euo pipefail - PYTHONPATH=. python3 -m pytest -q tests/test_noema_reviewer_token_lifetime.py tests/test_noema_two_phase_handoff.py - python3 -m compileall -q .github/actions/noema-review/two_phase.py tests/test_noema_reviewer_token_lifetime.py tests/test_noema_two_phase_handoff.py + PYTHONPATH=. python3 -m pytest -q \ + tests/test_noema_reviewer_token_lifetime.py \ + tests/test_noema_two_phase_handoff.py \ + tests/test_noema_refreshed_app_identity.py + python3 -m compileall -q \ + .github/actions/noema-review/two_phase.py \ + tests/test_noema_reviewer_token_lifetime.py \ + tests/test_noema_two_phase_handoff.py \ + tests/test_noema_refreshed_app_identity.py git diff --check diff --git a/tests/test_noema_refreshed_app_identity.py b/tests/test_noema_refreshed_app_identity.py new file mode 100644 index 0000000000..2339fdba88 --- /dev/null +++ b/tests/test_noema_refreshed_app_identity.py @@ -0,0 +1,67 @@ +"""Regression coverage for refreshed Noema GitHub App credentials.""" + +from __future__ import annotations + +import importlib.util +import os +from pathlib import Path +from types import ModuleType + +import pytest + + +ROOT = Path(__file__).resolve().parents[1] +MODULE_PATH = ROOT / ".github" / "actions" / "noema-review" / "two_phase.py" + + +def _load_module() -> ModuleType: + """Load the trusted two-phase helper from its workflow action path.""" + spec = importlib.util.spec_from_file_location( + "noema_two_phase_refreshed_identity_under_test", + MODULE_PATH, + ) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_publication_validates_refreshed_token_as_the_same_bound_app(monkeypatch: pytest.MonkeyPatch) -> None: + """Token renewal changes lifetime, not the independently bound App identity.""" + module = _load_module() + monkeypatch.setenv("NOEMA_REVIEW_ACTOR", "cwl-noema-review[bot]") + monkeypatch.setenv("NOEMA_REVIEW_INSTALLATION_ID", "146401636") + monkeypatch.setenv( + "NOEMA_REVIEW_TOKEN_SOURCE", + module.REFRESHED_APP_TOKEN_SOURCE, + ) + + assert module._reviewer_actor(allow_refreshed_app=True) == "cwl-noema-review[bot]" + assert os.environ["NOEMA_REVIEW_TOKEN_SOURCE"] == module.REFRESHED_APP_TOKEN_SOURCE + + +def test_refreshed_token_path_retains_bot_identity_fail_closed(monkeypatch: pytest.MonkeyPatch) -> None: + """Publication must not turn the refresh alias into a general identity bypass.""" + module = _load_module() + monkeypatch.setenv("NOEMA_REVIEW_ACTOR", "seonghobae") + monkeypatch.setenv("NOEMA_REVIEW_INSTALLATION_ID", "146401636") + monkeypatch.setenv( + "NOEMA_REVIEW_TOKEN_SOURCE", + module.REFRESHED_APP_TOKEN_SOURCE, + ) + + with pytest.raises(RuntimeError, match="Noema GitHub App identity binding is invalid"): + module._reviewer_actor(allow_refreshed_app=True) + assert os.environ["NOEMA_REVIEW_TOKEN_SOURCE"] == module.REFRESHED_APP_TOKEN_SOURCE + + +def test_unrecognized_source_is_not_normalized(monkeypatch: pytest.MonkeyPatch) -> None: + """Only the workflow-owned refresh marker may reuse canonical App validation.""" + module = _load_module() + monkeypatch.setenv("NOEMA_REVIEW_ACTOR", "cwl-noema-review[bot]") + monkeypatch.setenv("NOEMA_REVIEW_INSTALLATION_ID", "146401636") + monkeypatch.setenv("NOEMA_REVIEW_TOKEN_SOURCE", "untrusted-app-alias") + + with pytest.raises(RuntimeError, match="Noema GitHub App identity binding is invalid"): + module._reviewer_actor(allow_refreshed_app=True) + assert os.environ["NOEMA_REVIEW_TOKEN_SOURCE"] == "untrusted-app-alias"