diff --git a/.gitea/workflows/cd.yaml b/.gitea/workflows/cd.yaml index 0975ba8c..fe35dd68 100644 --- a/.gitea/workflows/cd.yaml +++ b/.gitea/workflows/cd.yaml @@ -243,6 +243,10 @@ jobs: ;; docs/operations/p0-cicd-baseline-source-readiness.snapshot.json) ;; + .gitea/workflows/awoooi-onboarding-warning-step.yaml) + ;; + docs/operations/templates/awoooi-gitea-onboarding-warning-step.workflow.yaml) + ;; docs/operations/awoooi-production-deploy-readback-blocker.snapshot.json) ;; docs/operations/ai-agent-log-intelligence-runtime-sample-readback.snapshot.json) @@ -309,6 +313,8 @@ jobs: ;; apps/api/src/services/awoooi_gitea_onboarding_warning_step_template_copy_execution_plan.py) ;; + apps/api/src/services/awoooi_gitea_onboarding_warning_step_template_copy_receipt.py) + ;; apps/api/src/services/awoooi_new_product_onboarding_page_model.py) ;; apps/api/src/services/awoooi_onboarding_reminder_contract.py) @@ -552,6 +558,7 @@ jobs: src/services/awoooi_gitea_onboarding_warning_step_owner_response_preflight.py \ src/services/awoooi_gitea_onboarding_warning_step_template_copy_apply_gate.py \ src/services/awoooi_gitea_onboarding_warning_step_template_copy_execution_plan.py \ + src/services/awoooi_gitea_onboarding_warning_step_template_copy_receipt.py \ src/services/awoooi_new_product_onboarding_page_model.py \ src/services/awoooi_onboarding_reminder_contract.py \ src/services/awoooi_onboarding_source_contracts.py \ diff --git a/apps/api/src/api/v1/agents.py b/apps/api/src/api/v1/agents.py index 3281383d..7fc558dd 100644 --- a/apps/api/src/api/v1/agents.py +++ b/apps/api/src/api/v1/agents.py @@ -322,6 +322,9 @@ from src.services.awoooi_gitea_onboarding_warning_step_template_copy_apply_gate from src.services.awoooi_gitea_onboarding_warning_step_template_copy_execution_plan import ( load_latest_awoooi_gitea_onboarding_warning_step_template_copy_execution_plan, ) +from src.services.awoooi_gitea_onboarding_warning_step_template_copy_receipt import ( + load_latest_awoooi_gitea_onboarding_warning_step_template_copy_receipt, +) from src.services.awoooi_status_cleanup_dashboard import ( load_latest_awoooi_status_cleanup_dashboard, ) @@ -1162,6 +1165,36 @@ async def get_awoooi_gitea_onboarding_warning_step_template_copy_execution_plan( ) from exc +@router.get( + "/awoooi-gitea-onboarding-warning-step-template-copy-receipt", + response_model=dict[str, Any], + summary="取得 AWOOOI Gitea onboarding warning-step template copy receipt", + description=( + "讀取 P0-004 warning-step template copy 的受控 receipt;" + "此端點檢查 template 已提交、workflow 已複製且 fail-closed、" + "無 branch auto trigger、無泛用 runner label,並回傳 rollback / verifier / " + "post-copy boundary。它不觸發 workflow、不建立 repo、不同步 refs、" + "不呼叫 GitHub、不讀 secret、不操作 host / K8s。" + ), +) +async def get_awoooi_gitea_onboarding_warning_step_template_copy_receipt() -> dict[str, Any]: + """回傳 P0-004 warning-step template copy receipt 只讀讀回。""" + try: + payload = await asyncio.to_thread( + load_latest_awoooi_gitea_onboarding_warning_step_template_copy_receipt + ) + return redact_public_lan_topology(payload) + except (json.JSONDecodeError, ValueError) as exc: + logger.error( + "awoooi_gitea_onboarding_warning_step_template_copy_receipt_invalid", + error=str(exc), + ) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="P0-004 warning-step template copy receipt 無效", + ) from exc + + @router.get( "/reboot-auto-recovery-slo-scorecard", response_model=dict[str, Any], 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 new file mode 100644 index 00000000..427df023 --- /dev/null +++ b/apps/api/src/services/awoooi_gitea_onboarding_warning_step_template_copy_receipt.py @@ -0,0 +1,218 @@ +"""AWOOOI warning-step template copy receipt readback.""" + +from __future__ import annotations + +import hashlib +import re +from pathlib import Path +from typing import Any + +from src.services.awoooi_gitea_onboarding_warning_step_template_copy_apply_gate import ( + load_latest_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" +_TEMPLATE_RELATIVE_PATH = ( + "docs/operations/templates/awoooi-gitea-onboarding-warning-step.workflow.yaml" +) +_WORKFLOW_RELATIVE_PATH = ".gitea/workflows/awoooi-onboarding-warning-step.yaml" +_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), + re.compile(r"^\s*runs-on:\s*.*\bself-hosted\b", re.MULTILINE), +) + + +def load_latest_awoooi_gitea_onboarding_warning_step_template_copy_receipt( + repo_root: Path | None = None, +) -> dict[str, Any]: + """Return the fail-closed warning-step workflow copy receipt.""" + root = repo_root or resolve_repo_root(Path(__file__)) + template_path = root / _TEMPLATE_RELATIVE_PATH + workflow_path = root / _WORKFLOW_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 "" + apply_gate = load_latest_awoooi_gitea_onboarding_warning_step_template_copy_apply_gate() + gate_readback = _dict(apply_gate.get("readback")) + + active_blockers = _active_blockers( + template_path=template_path, + workflow_path=workflow_path, + template_text=template_text, + workflow_text=workflow_text, + gate_readback=gate_readback, + ) + workflow_sha = _short_content_sha(workflow_text) if workflow_text else "" + + return { + "schema_version": _SCHEMA_VERSION, + "priority": "P0-004", + "scope": "gitea_onboarding_warning_step_template_copy", + "status": ( + "controlled_template_copy_receipt_ready" + if not active_blockers + else "blocked_template_copy_receipt_invalid" + ), + "readback": { + "workplan_id": "P0-004-TEMPLATE-COPY-CONTROLLED-APPLY", + "source_apply_gate_status": apply_gate.get("status"), + "template_copy_performed": workflow_path.is_file() + and template_text == workflow_text, + "source_template_path": _TEMPLATE_RELATIVE_PATH, + "destination_workflow_path": _WORKFLOW_RELATIVE_PATH, + "workflow_content_sha256_12": workflow_sha, + "safe_next_step": ( + "open_next_gate_for_warning_step_runtime_enablement_after_pressure_guard" + ), + }, + "target_selector": { + "selector_type": "controlled_workflow_template_copy", + "source_template_path": _TEMPLATE_RELATIVE_PATH, + "destination_workflow_path": _WORKFLOW_RELATIVE_PATH, + "active_workflow_file_created": True, + }, + "source_of_truth_diff": { + "source": "controlled_apply_gate_readback", + "apply_gate_endpoint": ( + "/api/v1/agents/" + "awoooi-gitea-onboarding-warning-step-template-copy-apply-gate" + ), + "receipt_endpoint": ( + "/api/v1/agents/" + "awoooi-gitea-onboarding-warning-step-template-copy-receipt" + ), + "source_template_path": _TEMPLATE_RELATIVE_PATH, + "destination_workflow_path": _WORKFLOW_RELATIVE_PATH, + }, + "check_mode": { + "enabled": True, + "required": True, + "checks": [ + "template_file_exists", + "workflow_file_exists", + "workflow_matches_source_template", + "workflow_has_no_auto_branch_event", + "workflow_has_no_generic_runner_label", + "workflow_runtime_execution_switch_defaults_off", + "runner_pressure_guard_required", + "git_diff_check_required", + ], + }, + "rollback": { + "required": True, + "strategy": "remove_copied_workflow_and_receipt_before_commit", + "paths": [ + _WORKFLOW_RELATIVE_PATH, + "apps/api/src/services/" + "awoooi_gitea_onboarding_warning_step_template_copy_receipt.py", + ], + }, + "post_apply_verifier": { + "required": True, + "verifier_refs": [ + "python3 ops/runner/guard-gitea-runner-pressure.py --root .", + "git diff --check", + "pytest apps/api/tests/test_p0_cicd_baseline_source_readiness_api.py", + ], + }, + "rollups": { + "template_file_present": template_path.is_file(), + "workflow_file_present": workflow_path.is_file(), + "workflow_matches_template": template_text == workflow_text, + "workflow_dispatch_declared": "workflow_dispatch:" in workflow_text, + "auto_branch_event_count": len(_auto_branch_event_hits(workflow_text)), + "generic_runner_label_count": len(_generic_label_hits(workflow_text)), + "fail_closed_execution_switch_present": _fail_closed_switch_present( + workflow_text + ), + "apply_gate_ready": _gate_ready(gate_readback), + "active_blocker_count": len(active_blockers), + "active_workflow_file_created": workflow_path.is_file(), + "workflow_trigger_performed": False, + "runner_pressure_guard_required": True, + }, + "active_blockers": active_blockers, + "operation_boundaries": { + "controlled_template_copy_only": True, + "workflow_modification_allowed_by_gate": True, + "active_workflow_file_created": workflow_path.is_file(), + "workflow_dispatch_declared": "workflow_dispatch:" in workflow_text, + "workflow_trigger_performed": False, + "auto_push_or_pull_request_trigger_allowed": False, + "generic_runner_label_allowed": False, + "github_api_used": False, + "secret_value_collection_allowed": False, + "host_or_k8s_write_performed": False, + "raw_session_or_sqlite_read_allowed": False, + }, + } + + +def _active_blockers( + *, + template_path: Path, + workflow_path: Path, + template_text: str, + workflow_text: str, + gate_readback: dict[str, Any], +) -> list[str]: + blockers: list[str] = [] + if not _gate_ready(gate_readback): + 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(): + blockers.append("destination_workflow_file_missing") + if template_text != workflow_text: + blockers.append("destination_workflow_differs_from_source_template") + if _auto_branch_event_hits(workflow_text): + blockers.append("auto_branch_event_present_in_workflow") + if _generic_label_hits(workflow_text): + blockers.append("generic_runner_label_present_in_workflow") + if not _fail_closed_switch_present(workflow_text): + blockers.append("fail_closed_execution_switch_missing") + return blockers + + +def _gate_ready(gate_readback: dict[str, Any]) -> bool: + return ( + gate_readback.get("apply_allowed") is True + and gate_readback.get("controlled_apply_allowed") is True + and gate_readback.get("workflow_template_copy_authorized") is True + and gate_readback.get("workflow_trigger_authorized") is False + and gate_readback.get("auto_push_or_pull_request_trigger_authorized") is False + and gate_readback.get("generic_runner_label_authorized") is False + and gate_readback.get("runner_pressure_guard_required") is True + ) + + +def _auto_branch_event_hits(template_text: str) -> list[str]: + return [ + event + for event in _AUTO_BRANCH_EVENTS + if re.search(rf"^\s*{event}\s*:", template_text, re.MULTILINE) + or re.search(rf"\bon:\s*\[[^\]]*\b{event}\b", template_text) + ] + + +def _generic_label_hits(template_text: str) -> list[str]: + hits: list[str] = [] + for pattern in _GENERIC_LABEL_PATTERNS: + hits.extend(pattern.findall(template_text)) + return hits + + +def _short_content_sha(template_text: str) -> str: + return hashlib.sha256(template_text.encode("utf-8")).hexdigest()[:12] + + +def _fail_closed_switch_present(template_text: str) -> bool: + return ( + 'AWOOOI_ONBOARDING_WARNING_STEP_EXECUTION_ENABLED: "0"' in template_text + and "AWOOOI_ONBOARDING_WARNING_STEP_EXECUTION_ENABLED == '1'" in template_text + ) + + +def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} diff --git a/apps/api/src/services/delivery_closure_workbench.py b/apps/api/src/services/delivery_closure_workbench.py index b7f2940c..9112166e 100644 --- a/apps/api/src/services/delivery_closure_workbench.py +++ b/apps/api/src/services/delivery_closure_workbench.py @@ -12,6 +12,9 @@ from typing import Any from src.services.awoooi_gitea_onboarding_warning_step_template_copy_apply_gate import ( load_latest_awoooi_gitea_onboarding_warning_step_template_copy_apply_gate, ) +from src.services.awoooi_gitea_onboarding_warning_step_template_copy_receipt import ( + load_latest_awoooi_gitea_onboarding_warning_step_template_copy_receipt, +) from src.services.awoooi_production_deploy_readback_blocker import ( load_latest_awoooi_production_deploy_readback_blocker, ) @@ -52,6 +55,9 @@ def load_delivery_closure_workbench() -> dict[str, Any]: cicd_template_copy_apply_gate = ( load_latest_awoooi_gitea_onboarding_warning_step_template_copy_apply_gate() ) + cicd_template_copy_receipt = ( + load_latest_awoooi_gitea_onboarding_warning_step_template_copy_receipt() + ) gitea = load_latest_gitea_workflow_runner_health() runtime = load_latest_runtime_surface_inventory() backup = load_latest_backup_dr_readiness_matrix() @@ -63,6 +69,7 @@ def load_delivery_closure_workbench() -> dict[str, Any]: private_inventory=private_inventory, cicd_baseline=cicd_baseline, cicd_template_copy_apply_gate=cicd_template_copy_apply_gate, + cicd_template_copy_receipt=cicd_template_copy_receipt, gitea=gitea, runtime=runtime, backup=backup, @@ -78,6 +85,7 @@ def build_delivery_closure_workbench( private_inventory: dict[str, Any], cicd_baseline: dict[str, Any], cicd_template_copy_apply_gate: dict[str, Any], + cicd_template_copy_receipt: dict[str, Any], gitea: dict[str, Any], runtime: dict[str, Any], backup: dict[str, Any], @@ -104,6 +112,15 @@ def build_delivery_closure_workbench( cicd_apply_gate_boundaries = _dict( cicd_template_copy_apply_gate.get("operation_boundaries") ) + cicd_template_copy_receipt_readback = _dict( + cicd_template_copy_receipt.get("readback") + ) + cicd_template_copy_receipt_rollups = _dict( + cicd_template_copy_receipt.get("rollups") + ) + cicd_template_copy_receipt_boundaries = _dict( + cicd_template_copy_receipt.get("operation_boundaries") + ) production_deploy_readback = _dict(production_deploy.get("readback")) production_deploy_rollups = _dict(production_deploy.get("rollups")) gitea_status = _dict(gitea.get("program_status")) @@ -798,6 +815,64 @@ def build_delivery_closure_workbench( cicd_apply_gate_readback.get("runner_pressure_guard_required") is True ), + "template_copy_receipt_status": str( + cicd_template_copy_receipt.get("status") or "" + ), + "template_copy_receipt_ready": ( + cicd_template_copy_receipt.get("status") + == "controlled_template_copy_receipt_ready" + ), + "template_copy_receipt_active_blocker_count": _int( + cicd_template_copy_receipt_rollups.get("active_blocker_count") + ), + "source_template_path": str( + cicd_template_copy_receipt_readback.get("source_template_path") + or "" + ), + "destination_workflow_path": str( + cicd_template_copy_receipt_readback.get( + "destination_workflow_path" + ) + or "" + ), + "source_template_file_present": ( + cicd_template_copy_receipt_rollups.get("template_file_present") + is True + ), + "destination_workflow_file_present": ( + cicd_template_copy_receipt_rollups.get("workflow_file_present") + is True + ), + "destination_workflow_matches_template": ( + cicd_template_copy_receipt_rollups.get("workflow_matches_template") + is True + ), + "destination_workflow_dispatch_declared": ( + cicd_template_copy_receipt_rollups.get( + "workflow_dispatch_declared" + ) + is True + ), + "destination_workflow_auto_branch_event_count": _int( + cicd_template_copy_receipt_rollups.get("auto_branch_event_count") + ), + "destination_workflow_generic_runner_label_count": _int( + cicd_template_copy_receipt_rollups.get( + "generic_runner_label_count" + ) + ), + "destination_workflow_active_file_created": ( + cicd_template_copy_receipt_boundaries.get( + "active_workflow_file_created" + ) + is True + ), + "destination_workflow_trigger_performed": ( + cicd_template_copy_receipt_boundaries.get( + "workflow_trigger_performed" + ) + is True + ), "required_source_count": _int( cicd_baseline_rollups.get("required_source_count") ), @@ -1191,6 +1266,53 @@ def build_delivery_closure_workbench( cicd_apply_gate_readback.get("runner_pressure_guard_required") is True ), + "p0_cicd_template_copy_receipt_status": str( + cicd_template_copy_receipt.get("status") or "" + ), + "p0_cicd_template_copy_receipt_ready": ( + cicd_template_copy_receipt.get("status") + == "controlled_template_copy_receipt_ready" + ), + "p0_cicd_template_copy_receipt_active_blocker_count": _int( + cicd_template_copy_receipt_rollups.get("active_blocker_count") + ), + "p0_cicd_template_copy_source_template_path": str( + cicd_template_copy_receipt_readback.get("source_template_path") + or "" + ), + "p0_cicd_template_copy_destination_workflow_path": str( + cicd_template_copy_receipt_readback.get("destination_workflow_path") + or "" + ), + "p0_cicd_template_copy_source_template_file_present": ( + cicd_template_copy_receipt_rollups.get("template_file_present") + is True + ), + "p0_cicd_template_copy_destination_workflow_file_present": ( + cicd_template_copy_receipt_rollups.get("workflow_file_present") is True + ), + "p0_cicd_template_copy_destination_workflow_matches_template": ( + cicd_template_copy_receipt_rollups.get("workflow_matches_template") + is True + ), + "p0_cicd_template_copy_workflow_dispatch_declared": ( + cicd_template_copy_receipt_rollups.get("workflow_dispatch_declared") + is True + ), + "p0_cicd_template_copy_auto_branch_event_count": _int( + cicd_template_copy_receipt_rollups.get("auto_branch_event_count") + ), + "p0_cicd_template_copy_generic_runner_label_count": _int( + cicd_template_copy_receipt_rollups.get("generic_runner_label_count") + ), + "p0_cicd_template_copy_active_workflow_file_created": ( + cicd_template_copy_receipt_boundaries.get("active_workflow_file_created") + is True + ), + "p0_cicd_template_copy_workflow_trigger_performed": ( + cicd_template_copy_receipt_boundaries.get("workflow_trigger_performed") + is True + ), "production_deploy_status": str(production_deploy.get("status") or ""), "production_deploy_source_control_main_ready": production_deploy_rollups.get( "source_control_main_ready" diff --git a/apps/api/tests/test_delivery_closure_workbench_api.py b/apps/api/tests/test_delivery_closure_workbench_api.py index 6ceee3c1..ca3a42f6 100644 --- a/apps/api/tests/test_delivery_closure_workbench_api.py +++ b/apps/api/tests/test_delivery_closure_workbench_api.py @@ -352,6 +352,25 @@ def _assert_delivery_workbench_shape(data: dict): assert data["summary"]["p0_cicd_template_copy_auto_branch_trigger_authorized"] is False assert data["summary"]["p0_cicd_template_copy_generic_runner_label_authorized"] is False assert data["summary"]["p0_cicd_template_copy_runner_pressure_guard_required"] is True + assert data["summary"]["p0_cicd_template_copy_receipt_status"] == ( + "controlled_template_copy_receipt_ready" + ) + assert data["summary"]["p0_cicd_template_copy_receipt_ready"] is True + assert data["summary"]["p0_cicd_template_copy_receipt_active_blocker_count"] == 0 + assert data["summary"]["p0_cicd_template_copy_source_template_file_present"] is True + assert ( + data["summary"]["p0_cicd_template_copy_destination_workflow_file_present"] + is True + ) + assert ( + data["summary"]["p0_cicd_template_copy_destination_workflow_matches_template"] + is True + ) + assert data["summary"]["p0_cicd_template_copy_workflow_dispatch_declared"] is True + assert data["summary"]["p0_cicd_template_copy_auto_branch_event_count"] == 0 + assert data["summary"]["p0_cicd_template_copy_generic_runner_label_count"] == 0 + assert data["summary"]["p0_cicd_template_copy_active_workflow_file_created"] is True + assert data["summary"]["p0_cicd_template_copy_workflow_trigger_performed"] is False assert data["summary"]["production_deploy_status"] == "closure_verified" assert data["summary"]["production_deploy_image_tag_matches_main"] is True assert data["summary"]["backup_credential_escrow_intake_status"] == ( 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 c3cdf3b4..265ef25f 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 @@ -22,6 +22,9 @@ from src.services.awoooi_gitea_onboarding_warning_step_template_copy_apply_gate from src.services.awoooi_gitea_onboarding_warning_step_template_copy_execution_plan import ( load_latest_awoooi_gitea_onboarding_warning_step_template_copy_execution_plan, ) +from src.services.awoooi_gitea_onboarding_warning_step_template_copy_receipt import ( + load_latest_awoooi_gitea_onboarding_warning_step_template_copy_receipt, +) from src.services.awoooi_new_product_onboarding_page_model import ( load_latest_awoooi_new_product_onboarding_page_model, ) @@ -276,3 +279,46 @@ def test_warning_step_template_copy_is_fail_closed_and_pressure_guarded(): assert "runs-on: awoooi-non110-host" in workflow assert 'AWOOOI_ONBOARDING_WARNING_STEP_EXECUTION_ENABLED: "0"' in workflow assert "ops/runner/guard-gitea-runner-pressure.py --root ." in workflow + + +def test_template_copy_receipt_loader_confirms_template_copy(): + payload = load_latest_awoooi_gitea_onboarding_warning_step_template_copy_receipt() + + _assert_template_copy_receipt(payload) + + +def test_template_copy_receipt_endpoint_returns_controlled_receipt(): + app = FastAPI() + app.include_router(router, prefix="/api/v1") + client = TestClient(app) + + response = client.get( + "/api/v1/agents/" + "awoooi-gitea-onboarding-warning-step-template-copy-receipt" + ) + + assert response.status_code == 200 + _assert_template_copy_receipt(response.json()) + + +def _assert_template_copy_receipt(payload: dict): + assert ( + payload["schema_version"] + == "awoooi_gitea_onboarding_warning_step_template_copy_receipt_v1" + ) + assert payload["priority"] == "P0-004" + assert payload["status"] == "controlled_template_copy_receipt_ready" + assert payload["active_blockers"] == [] + assert payload["readback"]["template_copy_performed"] is True + assert payload["target_selector"]["active_workflow_file_created"] is True + assert payload["rollups"]["template_file_present"] is True + assert payload["rollups"]["auto_branch_event_count"] == 0 + assert payload["rollups"]["generic_runner_label_count"] == 0 + assert payload["rollups"]["apply_gate_ready"] is True + assert payload["rollups"]["workflow_trigger_performed"] is False + assert payload["operation_boundaries"]["controlled_template_copy_only"] is True + assert payload["operation_boundaries"]["active_workflow_file_created"] is True + assert payload["operation_boundaries"]["workflow_trigger_performed"] is False + assert payload["operation_boundaries"]["github_api_used"] is False + assert payload["operation_boundaries"]["secret_value_collection_allowed"] is False + assert payload["operation_boundaries"]["raw_session_or_sqlite_read_allowed"] is False diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index db401498..78bbc3c8 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,13 @@ +## 2026-06-30 — 00:16 P0-004 warning-step template copy receipt + +**照優先順序完成的實作**: +- 接續 production 已讀回 `controlled_template_copy_apply_gate_ready`,推進同一條 P0-004 主線的下一段 controlled apply:Gitea warning-step source template `docs/operations/templates/awoooi-gitea-onboarding-warning-step.workflow.yaml` 已複製到 `.gitea/workflows/awoooi-onboarding-warning-step.yaml`。 +- workflow 只宣告 `workflow_dispatch`,不含 `push` / `pull_request` / `pull_request_target`,runner label 為 `awoooi-non110-host`,且 `AWOOOI_ONBOARDING_WARNING_STEP_EXECUTION_ENABLED="0"` 讓 job 預設 fail-closed;本輪未觸發 workflow。 +- 新增 GET `/api/v1/agents/awoooi-gitea-onboarding-warning-step-template-copy-receipt`,由 service 讀 committed template / copied workflow 並檢查 source/destination 一致、no branch auto trigger、no generic runner label、apply gate ready、rollback 與 post-copy verifier。 +- `.gitea/workflows/cd.yaml` controlled-runtime profile 已納入 active workflow path、source template path、新 receipt service 與 focused tests,避免 template/readback 變更被送到重型 runner path。 + +**邊界**:未 workflow_dispatch、未改 runner、未操作 host / Docker / K8s / DB / firewall,未使用 GitHub / `gh` / GitHub API,未讀 secret / token / raw sessions / SQLite / `.env`。 + ## 2026-06-29 — 23:45 P0-006 final retry window readback source closure **照優先順序完成的實作**: diff --git a/ops/runner/test_cd_controlled_runtime_profile.py b/ops/runner/test_cd_controlled_runtime_profile.py index 2b4901b6..e3c8fd33 100644 --- a/ops/runner/test_cd_controlled_runtime_profile.py +++ b/ops/runner/test_cd_controlled_runtime_profile.py @@ -64,6 +64,7 @@ def test_p0_onboarding_readiness_sources_stay_on_controlled_runtime_profile() -> "awoooi_gitea_onboarding_warning_step_owner_response_preflight.py", "awoooi_gitea_onboarding_warning_step_template_copy_apply_gate.py", "awoooi_gitea_onboarding_warning_step_template_copy_execution_plan.py", + "awoooi_gitea_onboarding_warning_step_template_copy_receipt.py", "awoooi_new_product_onboarding_page_model.py", "awoooi_onboarding_reminder_contract.py", "awoooi_onboarding_source_contracts.py", @@ -72,6 +73,9 @@ def test_p0_onboarding_readiness_sources_stay_on_controlled_runtime_profile() -> for source in expected_sources: assert f"apps/api/src/services/{source})" in text 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 "tests/test_p0_cicd_baseline_source_readiness_api.py" in text def test_iwooos_security_operation_api_stays_on_controlled_runtime_profile() -> None: