From 3c06ae867b28f25d0fa9d2c60542825e64b7547f Mon Sep 17 00:00:00 2001 From: jenkins Date: Mon, 24 Aug 2026 01:19:58 -0300 Subject: [PATCH] fix(hermes): reject invalid release revisions early --- .../scripts/jenkins_image_build_trigger.py | 45 +++++++++++++- .../test_hermes_image_builder_coverage.py | 60 ++++++++++++++++++- .../test_hermes_release_followthrough.py | 22 +++++++ 3 files changed, 124 insertions(+), 3 deletions(-) diff --git a/services/hermes/scripts/jenkins_image_build_trigger.py b/services/hermes/scripts/jenkins_image_build_trigger.py index 9fffb3ec..b83a13a7 100755 --- a/services/hermes/scripts/jenkins_image_build_trigger.py +++ b/services/hermes/scripts/jenkins_image_build_trigger.py @@ -14,6 +14,10 @@ from pathlib import Path JENKINS_ORIGIN = "https://ci.bstein.dev" JENKINS_BUILD_URL = f"{JENKINS_ORIGIN}/buildByToken/buildWithParameters" +SCM_BROKER_ORIGIN = "http://hermes-scm-broker.hermes-scm.svc.cluster.local:9081" +SCM_METADATA_URL = f"{SCM_BROKER_ORIGIN}/v1/metadata" +SCM_REPOSITORY = "titan-iac" +MAX_SCM_RESPONSE_BYTES = 512 * 1024 JOBS = { "agent": { "job": "hermes-agent-image", @@ -58,12 +62,45 @@ def _open_without_redirect(request: urllib.request.Request, timeout: int): raise +def _verify_reviewed_revision_exists(revision: str) -> None: + """Fail before queuing when the exact reviewed commit is absent from SCM.""" + path = f"/api/v1/repos/atlas/{SCM_REPOSITORY}/git/commits/{revision}" + payload = json.dumps({"path": path}, separators=(",", ":")).encode("utf-8") + request = urllib.request.Request( + SCM_METADATA_URL, + data=payload, + headers={ + "Accept": "application/json", + "Content-Type": "application/json", + "User-Agent": "hermes-image-release/1", + }, + method="POST", + ) + opener = urllib.request.build_opener(_NoRedirect()) + with opener.open(request, timeout=20) as response: + if int(response.status) != 200: + raise RuntimeError("SCM did not confirm the reviewed revision") + content_type = response.headers.get_content_type() + if content_type != "application/json": + raise RuntimeError("SCM returned an unexpected revision response") + body = response.read(MAX_SCM_RESPONSE_BYTES + 1) + if len(body) > MAX_SCM_RESPONSE_BYTES: + raise RuntimeError("SCM revision response exceeded the safe size limit") + try: + result = json.loads(body) + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + raise RuntimeError("SCM returned malformed revision evidence") from exc + if not isinstance(result, dict) or result.get("sha") != revision: + raise RuntimeError("SCM did not confirm the exact reviewed revision") + + def trigger_build( revision: str, *, component: str = "agent", token_file: Path = TOKEN_FILE, opener=_open_without_redirect, + revision_verifier=None, ) -> dict[str, str | int]: """Post one allow-listed job's fixed parameters using the release token.""" revision = revision.strip() @@ -75,6 +112,8 @@ def trigger_build( token = token_file.read_text(encoding="utf-8").strip() if not token: raise RuntimeError("Jenkins image-build token is empty") + if revision_verifier is not None: + revision_verifier(revision) fields = { "job": job["job"], "token": token, @@ -132,7 +171,11 @@ def main() -> int: parser.add_argument("revision", help="reviewed full commit contained by main") args = parser.parse_args() try: - result = trigger_build(args.revision, component=args.component) + result = trigger_build( + args.revision, + component=args.component, + revision_verifier=_verify_reviewed_revision_exists, + ) except (OSError, ValueError, RuntimeError, urllib.error.URLError) as exc: print(json.dumps({"error": str(exc)}, sort_keys=True)) return 1 diff --git a/testing/tests/test_hermes_image_builder_coverage.py b/testing/tests/test_hermes_image_builder_coverage.py index f5a6c3a6..1900ef54 100644 --- a/testing/tests/test_hermes_image_builder_coverage.py +++ b/testing/tests/test_hermes_image_builder_coverage.py @@ -190,6 +190,60 @@ def test_trigger_redirect_wrapper_accepts_only_plugin_303( module._open_without_redirect(request, 9) +def test_trigger_confirms_exact_revision_through_credential_isolated_scm( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Revision preflight is bounded, fixed-origin, and exact-SHA fail closed.""" + module = _load(TRIGGER, "trigger_revision_coverage") + revision = "7" * 40 + captured = {} + + class Headers(dict): + def get_content_type(self): + return self.get("Content-Type", "application/octet-stream") + + class Opener: + def __init__(self, status=200, body=b"", content_type="application/json"): + self.status = status + self.body = body + self.content_type = content_type + + def open(self, request, timeout): + captured["request"] = request + captured["timeout"] = timeout + response = Response( + self.status, + self.body, + Headers({"Content-Type": self.content_type}), + ) + return response + + opener = Opener(body=json.dumps({"sha": revision}).encode()) + monkeypatch.setattr(module.urllib.request, "build_opener", lambda *_args: opener) + module._verify_reviewed_revision_exists(revision) + request = captured["request"] + assert request.full_url == module.SCM_METADATA_URL + assert captured["timeout"] == 20 + assert json.loads(request.data)["path"].endswith(f"/git/commits/{revision}") + + cases = ( + (Opener(status=503, body=b"{}"), "did not confirm"), + (Opener(body=b"{}", content_type="text/plain"), "unexpected"), + (Opener(body=b"x" * (module.MAX_SCM_RESPONSE_BYTES + 1)), "safe size"), + (Opener(body=b"not-json"), "malformed"), + (Opener(body=json.dumps({"sha": "8" * 40}).encode()), "exact"), + (Opener(body=b"[]"), "exact"), + ) + for failing_opener, message in cases: + monkeypatch.setattr( + module.urllib.request, + "build_opener", + lambda *_args, value=failing_opener: value, + ) + with pytest.raises(RuntimeError, match=message): + module._verify_reviewed_revision_exists(revision) + + def test_trigger_main_reports_safe_success_and_errors( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: @@ -200,7 +254,7 @@ def test_trigger_main_reports_safe_success_and_errors( monkeypatch.setattr( module, "trigger_build", - lambda value, component="agent": { + lambda value, component="agent", **_kwargs: { "component": component, "job": module.JOBS[component]["job"], "source_revision": value, @@ -212,7 +266,9 @@ def test_trigger_main_reports_safe_success_and_errors( monkeypatch.setattr( module, "trigger_build", - lambda _value, component="agent": (_ for _ in ()).throw(OSError("closed")), + lambda _value, component="agent", **_kwargs: ( + _ for _ in () + ).throw(OSError("closed")), ) assert module.main() == 1 assert json.loads(capsys.readouterr().out) == {"error": "closed"} diff --git a/testing/tests/test_hermes_release_followthrough.py b/testing/tests/test_hermes_release_followthrough.py index b4378525..333180eb 100644 --- a/testing/tests/test_hermes_release_followthrough.py +++ b/testing/tests/test_hermes_release_followthrough.py @@ -322,6 +322,28 @@ def test_trigger_returns_a_token_free_exact_follow_command(tmp_path: Path) -> No assert "never-print-me" not in json.dumps(result) +def test_trigger_cli_verifies_the_exact_revision_before_queueing( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + """The CLI never spends a Jenkins build slot on a nonexistent commit.""" + module = _load(TRIGGER, "release_status_trigger_revision") + revision = "8" * 40 + seen = [] + + def trigger(value, *, component, revision_verifier): + revision_verifier(value) + seen.append((value, component)) + return {"source_revision": value, "component": component} + + monkeypatch.setattr(module, "trigger_build", trigger) + monkeypatch.setattr(module, "_verify_reviewed_revision_exists", seen.append) + monkeypatch.setattr(sys, "argv", ["trigger", "--component", "stt", revision]) + + assert module.main() == 0 + assert seen == [revision, (revision, "stt")] + assert json.loads(capsys.readouterr().out)["source_revision"] == revision + + def test_runtime_bundle_and_guidance_require_live_convergence() -> None: """The helper ships to Worker and merged code alone is explicitly insufficient.""" kustomization = (ROOT / "services/hermes/kustomization.yaml").read_text()