From 7e69bbb5802946ef2a3ac9cf33eaba3d6af8c369 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 30 Jun 2026 00:46:03 +0800 Subject: [PATCH] fix(api): support runtime template copy receipt --- .gitea/workflows/cd.yaml | 2 + ...ding_warning_step_template_copy_receipt.py | 205 ++++++++++++++---- ...t_p0_cicd_baseline_source_readiness_api.py | 19 ++ docs/LOGBOOK.md | 9 + ...g-step-template-copy-receipt.snapshot.json | 26 +++ .../test_cd_controlled_runtime_profile.py | 4 + 6 files changed, 217 insertions(+), 48 deletions(-) create mode 100644 docs/operations/awoooi-gitea-onboarding-warning-step-template-copy-receipt.snapshot.json diff --git a/.gitea/workflows/cd.yaml b/.gitea/workflows/cd.yaml index fe35dd68..0e20dc0f 100644 --- a/.gitea/workflows/cd.yaml +++ b/.gitea/workflows/cd.yaml @@ -243,6 +243,8 @@ jobs: ;; docs/operations/p0-cicd-baseline-source-readiness.snapshot.json) ;; + docs/operations/awoooi-gitea-onboarding-warning-step-template-copy-receipt.snapshot.json) + ;; .gitea/workflows/awoooi-onboarding-warning-step.yaml) ;; docs/operations/templates/awoooi-gitea-onboarding-warning-step.workflow.yaml) diff --git a/apps/api/src/services/awoooi_gitea_onboarding_warning_step_template_copy_receipt.py b/apps/api/src/services/awoooi_gitea_onboarding_warning_step_template_copy_receipt.py index 37162777..3080e433 100644 --- a/apps/api/src/services/awoooi_gitea_onboarding_warning_step_template_copy_receipt.py +++ b/apps/api/src/services/awoooi_gitea_onboarding_warning_step_template_copy_receipt.py @@ -3,6 +3,7 @@ from __future__ import annotations import hashlib +import json import re from pathlib import Path from typing import Any @@ -13,11 +14,18 @@ from src.services.awoooi_gitea_onboarding_warning_step_template_copy_apply_gate from src.services.snapshot_paths import resolve_repo_root _SCHEMA_VERSION = "awoooi_gitea_onboarding_warning_step_template_copy_receipt_v1" +_RECEIPT_SCHEMA_VERSION = ( + "awoooi_gitea_onboarding_warning_step_template_copy_receipt_snapshot_v1" +) +_EXPECTED_WORKFLOW_SHA256_12 = "70ecac9e4b59" _TEMPLATE_RELATIVE_PATH = ( "docs/operations/templates/awoooi-gitea-onboarding-warning-step.workflow.yaml" ) _WORKFLOW_RELATIVE_PATH = ".gitea/workflows/awoooi-onboarding-warning-step.yaml" -_EXPECTED_WORKFLOW_SHA256_12 = "70ecac9e4b59" +_RECEIPT_RELATIVE_PATH = ( + "docs/operations/awoooi-gitea-onboarding-warning-step-template-copy-receipt." + "snapshot.json" +) _AUTO_BRANCH_EVENTS = ("push", "pull_request", "pull_request_target") _GENERIC_LABEL_PATTERNS = ( re.compile(r"^\s*runs-on:\s*.*\bubuntu-[A-Za-z0-9_.-]+\b", re.MULTILINE), @@ -32,36 +40,45 @@ def load_latest_awoooi_gitea_onboarding_warning_step_template_copy_receipt( root = repo_root or resolve_repo_root(Path(__file__)) template_path = root / _TEMPLATE_RELATIVE_PATH workflow_path = root / _WORKFLOW_RELATIVE_PATH + receipt_path = root / _RECEIPT_RELATIVE_PATH + template_text = template_path.read_text(encoding="utf-8") if template_path.exists() else "" workflow_text = workflow_path.read_text(encoding="utf-8") if workflow_path.exists() else "" + receipt = _load_receipt_snapshot(receipt_path) effective_workflow_text = workflow_text or template_text workflow_content_source = ( - "repo_workflow_file" - if workflow_text - else "packaged_template_digest_fallback" + "repo_workflow_file" if workflow_text else "packaged_template_digest_fallback" + ) + template_sha = _short_content_sha(template_text) if template_text else "" + workflow_sha = _short_content_sha(workflow_text) if workflow_text else "" + effective_workflow_sha = ( + _short_content_sha(effective_workflow_text) if effective_workflow_text else "" + ) + template_copy_recorded = _template_copy_recorded( + receipt=receipt, + template_sha=template_sha, + workflow_sha=workflow_sha, + workflow_visible=workflow_path.is_file(), + template_text=template_text, + workflow_text=workflow_text, + ) + workflow_matches_template = _workflow_matches_template( + receipt=receipt, + template_sha=template_sha, + workflow_sha=workflow_sha, + workflow_visible=workflow_path.is_file(), + template_text=template_text, + workflow_text=workflow_text, ) apply_gate = load_latest_awoooi_gitea_onboarding_warning_step_template_copy_apply_gate() gate_readback = _dict(apply_gate.get("readback")) - workflow_sha = ( - _short_content_sha(effective_workflow_text) if effective_workflow_text else "" - ) - committed_workflow_file_created = ( - template_path.is_file() and workflow_sha == _EXPECTED_WORKFLOW_SHA256_12 - ) - workflow_matches_template = ( - bool(effective_workflow_text) - and template_text == effective_workflow_text - and workflow_sha == _EXPECTED_WORKFLOW_SHA256_12 - ) active_blockers = _active_blockers( template_path=template_path, - workflow_path=workflow_path, - template_text=template_text, - workflow_text=workflow_text, - effective_workflow_text=effective_workflow_text, - committed_workflow_file_created=committed_workflow_file_created, + receipt=receipt, + template_copy_recorded=template_copy_recorded, workflow_matches_template=workflow_matches_template, + effective_workflow_text=effective_workflow_text, gate_readback=gate_readback, ) @@ -77,11 +94,15 @@ def load_latest_awoooi_gitea_onboarding_warning_step_template_copy_receipt( "readback": { "workplan_id": "P0-004-TEMPLATE-COPY-CONTROLLED-APPLY", "source_apply_gate_status": apply_gate.get("status"), - "template_copy_performed": committed_workflow_file_created - and workflow_matches_template, + "template_copy_performed": template_copy_recorded, + "receipt_snapshot_path": _RECEIPT_RELATIVE_PATH, "source_template_path": _TEMPLATE_RELATIVE_PATH, "destination_workflow_path": _WORKFLOW_RELATIVE_PATH, - "workflow_content_sha256_12": workflow_sha, + "source_template_content_sha256_12": template_sha, + "destination_workflow_content_sha256_12": _destination_workflow_sha( + receipt, workflow_sha + ), + "workflow_content_sha256_12": effective_workflow_sha, "workflow_content_source": workflow_content_source, "safe_next_step": ( "open_next_gate_for_warning_step_runtime_enablement_after_pressure_guard" @@ -111,8 +132,9 @@ def load_latest_awoooi_gitea_onboarding_warning_step_template_copy_receipt( "required": True, "checks": [ "template_file_exists", - "workflow_file_exists", - "workflow_matches_source_template", + "receipt_snapshot_exists", + "workflow_file_exists_or_receipt_records_copy", + "workflow_matches_source_template_or_receipt_hash_matches", "workflow_has_no_auto_branch_event", "workflow_has_no_generic_runner_label", "workflow_runtime_execution_switch_defaults_off", @@ -125,6 +147,7 @@ def load_latest_awoooi_gitea_onboarding_warning_step_template_copy_receipt( "strategy": "remove_copied_workflow_and_receipt_before_commit", "paths": [ _WORKFLOW_RELATIVE_PATH, + _RECEIPT_RELATIVE_PATH, "apps/api/src/services/" "awoooi_gitea_onboarding_warning_step_template_copy_receipt.py", ], @@ -139,24 +162,29 @@ def load_latest_awoooi_gitea_onboarding_warning_step_template_copy_receipt( }, "rollups": { "template_file_present": template_path.is_file(), - "workflow_file_present": committed_workflow_file_created, + "receipt_snapshot_present": bool(receipt), + "workflow_file_present": template_copy_recorded, "runtime_image_workflow_file_present": workflow_path.is_file(), + "workflow_file_visible_in_runtime_image": workflow_path.is_file(), "workflow_matches_template": workflow_matches_template, "workflow_content_source": workflow_content_source, "expected_workflow_sha256_12": _EXPECTED_WORKFLOW_SHA256_12, - "workflow_dispatch_declared": "workflow_dispatch:" in effective_workflow_text, - "auto_branch_event_count": len( - _auto_branch_event_hits(effective_workflow_text) + "workflow_dispatch_declared": _workflow_dispatch_declared( + receipt, effective_workflow_text ), - "generic_runner_label_count": len( - _generic_label_hits(effective_workflow_text) + "auto_branch_event_count": _auto_branch_event_count( + receipt, effective_workflow_text + ), + "generic_runner_label_count": _generic_runner_label_count( + receipt, effective_workflow_text ), "fail_closed_execution_switch_present": _fail_closed_switch_present( effective_workflow_text - ), + ) + or receipt.get("fail_closed_execution_switch_present") is True, "apply_gate_ready": _gate_ready(gate_readback), "active_blocker_count": len(active_blockers), - "active_workflow_file_created": committed_workflow_file_created, + "active_workflow_file_created": template_copy_recorded, "workflow_trigger_performed": False, "runner_pressure_guard_required": True, }, @@ -164,9 +192,12 @@ def load_latest_awoooi_gitea_onboarding_warning_step_template_copy_receipt( "operation_boundaries": { "controlled_template_copy_only": True, "workflow_modification_allowed_by_gate": True, - "active_workflow_file_created": committed_workflow_file_created, + "active_workflow_file_created": template_copy_recorded, "runtime_image_workflow_file_present": workflow_path.is_file(), - "workflow_dispatch_declared": "workflow_dispatch:" in effective_workflow_text, + "active_workflow_file_visible_in_runtime_image": workflow_path.is_file(), + "workflow_dispatch_declared": _workflow_dispatch_declared( + receipt, effective_workflow_text + ), "workflow_trigger_performed": False, "auto_push_or_pull_request_trigger_allowed": False, "generic_runner_label_allowed": False, @@ -181,12 +212,10 @@ def load_latest_awoooi_gitea_onboarding_warning_step_template_copy_receipt( def _active_blockers( *, template_path: Path, - workflow_path: Path, - template_text: str, - workflow_text: str, - effective_workflow_text: str, - committed_workflow_file_created: bool, + receipt: dict[str, Any], + template_copy_recorded: bool, workflow_matches_template: bool, + effective_workflow_text: str, gate_readback: dict[str, Any], ) -> list[str]: blockers: list[str] = [] @@ -194,17 +223,20 @@ def _active_blockers( blockers.append("template_copy_apply_gate_not_ready") if not template_path.is_file(): blockers.append("source_template_file_missing") - if not workflow_path.is_file() and not committed_workflow_file_created: - blockers.append("destination_workflow_file_missing") - if workflow_path.is_file() and template_text != workflow_text: - blockers.append("destination_workflow_differs_from_source_template") + if not receipt: + blockers.append("template_copy_receipt_snapshot_missing") + if not template_copy_recorded: + blockers.append("template_copy_not_recorded") if not workflow_matches_template: - blockers.append("destination_workflow_digest_not_confirmed") - if _auto_branch_event_hits(effective_workflow_text): + blockers.append("destination_workflow_differs_from_source_template") + if _auto_branch_event_count(receipt, effective_workflow_text) > 0: blockers.append("auto_branch_event_present_in_workflow") - if _generic_label_hits(effective_workflow_text): + if _generic_runner_label_count(receipt, effective_workflow_text) > 0: blockers.append("generic_runner_label_present_in_workflow") - if not _fail_closed_switch_present(effective_workflow_text): + if not ( + _fail_closed_switch_present(effective_workflow_text) + or receipt.get("fail_closed_execution_switch_present") is True + ): blockers.append("fail_closed_execution_switch_missing") return blockers @@ -221,6 +253,69 @@ def _gate_ready(gate_readback: dict[str, Any]) -> bool: ) +def _load_receipt_snapshot(path: Path) -> dict[str, Any]: + if not path.exists(): + return {} + with path.open(encoding="utf-8") as handle: + payload = json.load(handle) + if not isinstance(payload, dict): + raise ValueError(f"{path}: receipt snapshot must be an object") + if payload.get("schema_version") != _RECEIPT_SCHEMA_VERSION: + raise ValueError(f"{path}: unexpected receipt snapshot schema") + return payload + + +def _template_copy_recorded( + *, + receipt: dict[str, Any], + template_sha: str, + workflow_sha: str, + workflow_visible: bool, + template_text: str, + workflow_text: str, +) -> bool: + if workflow_visible: + return bool(template_text) and template_text == workflow_text + return ( + receipt.get("template_copy_recorded") is True + and str(receipt.get("source_template_content_sha256_12") or "") == template_sha + and str(receipt.get("destination_workflow_content_sha256_12") or "") + == template_sha + and receipt.get("runtime_image_excludes_dot_gitea") is True + and not workflow_sha + ) + + +def _workflow_matches_template( + *, + receipt: dict[str, Any], + template_sha: str, + workflow_sha: str, + workflow_visible: bool, + template_text: str, + workflow_text: str, +) -> bool: + if workflow_visible: + return bool(template_text) and template_text == workflow_text + return ( + bool(template_sha) + and str(receipt.get("source_template_content_sha256_12") or "") == template_sha + and str(receipt.get("destination_workflow_content_sha256_12") or "") + == template_sha + and not workflow_sha + ) + + +def _destination_workflow_sha(receipt: dict[str, Any], workflow_sha: str) -> str: + return workflow_sha or str(receipt.get("destination_workflow_content_sha256_12") or "") + + +def _workflow_dispatch_declared(receipt: dict[str, Any], workflow_text: str) -> bool: + return "workflow_dispatch:" in workflow_text or ( + receipt.get("workflow_dispatch_declared") is True + ) + + def _auto_branch_event_hits(template_text: str) -> list[str]: return [ event @@ -230,6 +325,13 @@ def _auto_branch_event_hits(template_text: str) -> list[str]: ] +def _auto_branch_event_count(receipt: dict[str, Any], workflow_text: str) -> int: + if workflow_text: + return len(_auto_branch_event_hits(workflow_text)) + value = receipt.get("auto_branch_event_count") + return value if isinstance(value, int) else 0 + + def _generic_label_hits(template_text: str) -> list[str]: hits: list[str] = [] for pattern in _GENERIC_LABEL_PATTERNS: @@ -237,6 +339,13 @@ def _generic_label_hits(template_text: str) -> list[str]: return hits +def _generic_runner_label_count(receipt: dict[str, Any], workflow_text: str) -> int: + if workflow_text: + return len(_generic_label_hits(workflow_text)) + value = receipt.get("generic_runner_label_count") + return value if isinstance(value, int) else 0 + + def _short_content_sha(template_text: str) -> str: return hashlib.sha256(template_text.encode("utf-8")).hexdigest()[:12] diff --git a/apps/api/tests/test_p0_cicd_baseline_source_readiness_api.py b/apps/api/tests/test_p0_cicd_baseline_source_readiness_api.py index 3960f1df..07cc082f 100644 --- a/apps/api/tests/test_p0_cicd_baseline_source_readiness_api.py +++ b/apps/api/tests/test_p0_cicd_baseline_source_readiness_api.py @@ -45,6 +45,12 @@ _SNAPSHOT_PATH = ( / "operations" / "p0-cicd-baseline-source-readiness.snapshot.json" ) +_RECEIPT_SNAPSHOT_PATH = ( + _REPO_ROOT + / "docs" + / "operations" + / "awoooi-gitea-onboarding-warning-step-template-copy-receipt.snapshot.json" +) _WARNING_STEP_TEMPLATE_PATH = ( _REPO_ROOT / "docs" @@ -286,6 +292,7 @@ def test_template_copy_receipt_loader_confirms_template_copy(): _assert_template_copy_receipt(payload) assert payload["rollups"]["runtime_image_workflow_file_present"] is True + assert payload["rollups"]["workflow_file_visible_in_runtime_image"] is True def test_template_copy_receipt_endpoint_returns_controlled_receipt(): @@ -315,6 +322,11 @@ def test_template_copy_receipt_supports_runtime_image_layout(tmp_path): _WARNING_STEP_TEMPLATE_PATH.read_text(encoding="utf-8"), encoding="utf-8", ) + receipt_path = tmp_path / "docs" / "operations" / _RECEIPT_SNAPSHOT_PATH.name + receipt_path.write_text( + _RECEIPT_SNAPSHOT_PATH.read_text(encoding="utf-8"), + encoding="utf-8", + ) payload = load_latest_awoooi_gitea_onboarding_warning_step_template_copy_receipt( tmp_path @@ -325,7 +337,12 @@ def test_template_copy_receipt_supports_runtime_image_layout(tmp_path): "packaged_template_digest_fallback" ) assert payload["rollups"]["runtime_image_workflow_file_present"] is False + assert payload["rollups"]["workflow_file_visible_in_runtime_image"] is False assert payload["operation_boundaries"]["runtime_image_workflow_file_present"] is False + assert ( + payload["operation_boundaries"]["active_workflow_file_visible_in_runtime_image"] + is False + ) def _assert_template_copy_receipt(payload: dict): @@ -340,7 +357,9 @@ def _assert_template_copy_receipt(payload: dict): assert payload["readback"]["workflow_content_sha256_12"] == "70ecac9e4b59" assert payload["target_selector"]["active_workflow_file_created"] is True assert payload["rollups"]["template_file_present"] is True + assert payload["rollups"]["receipt_snapshot_present"] is True assert payload["rollups"]["workflow_file_present"] is True + assert payload["rollups"]["workflow_matches_template"] is True assert payload["rollups"]["auto_branch_event_count"] == 0 assert payload["rollups"]["generic_runner_label_count"] == 0 assert payload["rollups"]["expected_workflow_sha256_12"] == "70ecac9e4b59" diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 2eb697dd..2c3f2162 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,12 @@ +## 2026-06-30 — 00:41 P0-004 template copy receipt runtime-image readback 修正 + +**照優先順序完成的實作**: +- production `/api/v1/agents/awoooi-gitea-onboarding-warning-step-template-copy-receipt` 已部署但回 `blocked_template_copy_receipt_invalid`,原因是 API image 依 `.dockerignore` 不包含 `.gitea`;runtime 看得到 source template,卻看不到 copied workflow path,造成 source-control receipt 假紅。 +- 新增 `docs/operations/awoooi-gitea-onboarding-warning-step-template-copy-receipt.snapshot.json`,把 source template / destination workflow / hash / fail-closed switch / no branch auto event / no generic runner label 收斂成會進 API image 的 committed receipt。 +- receipt service 現在支援兩種 layout:repo/local 直接比對 `.gitea/workflows/awoooi-onboarding-warning-step.yaml`;production image 若 `.gitea` 不可見,則以 docs receipt + source template hash 驗證 copy 已記錄且仍 fail-closed。 + +**邊界**:未 workflow_dispatch,未改 runner,未操作 host / Docker / K8s / DB / firewall,未使用 GitHub / `gh` / GitHub API,未讀 secret / token / raw sessions / SQLite / `.env`。 + ## 2026-06-30 — 00:34 P1-LOG executor queue 納入 autonomous runtime-control **照主線完成的實作**: diff --git a/docs/operations/awoooi-gitea-onboarding-warning-step-template-copy-receipt.snapshot.json b/docs/operations/awoooi-gitea-onboarding-warning-step-template-copy-receipt.snapshot.json new file mode 100644 index 00000000..ff24513b --- /dev/null +++ b/docs/operations/awoooi-gitea-onboarding-warning-step-template-copy-receipt.snapshot.json @@ -0,0 +1,26 @@ +{ + "schema_version": "awoooi_gitea_onboarding_warning_step_template_copy_receipt_snapshot_v1", + "generated_at": "2026-06-30T00:42:00+08:00", + "workplan_id": "P0-004-TEMPLATE-COPY-CONTROLLED-APPLY", + "source_template_path": "docs/operations/templates/awoooi-gitea-onboarding-warning-step.workflow.yaml", + "destination_workflow_path": ".gitea/workflows/awoooi-onboarding-warning-step.yaml", + "source_template_content_sha256_12": "70ecac9e4b59", + "destination_workflow_content_sha256_12": "70ecac9e4b59", + "template_copy_recorded": true, + "destination_workflow_runtime_visible_required": false, + "runtime_image_excludes_dot_gitea": true, + "workflow_dispatch_declared": true, + "fail_closed_execution_switch_present": true, + "workflow_trigger_performed": false, + "auto_branch_event_count": 0, + "generic_runner_label_count": 0, + "runner_pressure_guard_required": true, + "hard_blockers_preserved": [ + "no_push_or_pull_request_trigger_to_awoooi_runner", + "no_generic_runner_label", + "no_workflow_dispatch_from_this_receipt", + "no_github_api_or_gh", + "no_secret_or_token_read", + "no_host_or_k8s_write" + ] +} diff --git a/ops/runner/test_cd_controlled_runtime_profile.py b/ops/runner/test_cd_controlled_runtime_profile.py index e3c8fd33..f4aab86e 100644 --- a/ops/runner/test_cd_controlled_runtime_profile.py +++ b/ops/runner/test_cd_controlled_runtime_profile.py @@ -75,6 +75,10 @@ def test_p0_onboarding_readiness_sources_stay_on_controlled_runtime_profile() -> assert f"src/services/{source}" in text assert ".gitea/workflows/awoooi-onboarding-warning-step.yaml)" in text assert "docs/operations/templates/awoooi-gitea-onboarding-warning-step.workflow.yaml)" in text + assert ( + "docs/operations/awoooi-gitea-onboarding-warning-step-template-copy-receipt.snapshot.json)" + in text + ) assert "tests/test_p0_cicd_baseline_source_readiness_api.py" in text