feat(gitea): add capability and backup receipt readbacks

This commit is contained in:
ogt
2026-07-09 23:21:54 +08:00
parent 7ee0a17ab7
commit 391a1577d0
15 changed files with 3587 additions and 114 deletions

View File

@@ -389,6 +389,21 @@ from src.services.docker_build_surface_inventory import (
from src.services.gitea_authenticated_inventory_payload_validation import (
validate_gitea_authenticated_inventory_payload,
)
from src.services.gitea_actions_secret_variable_name_inventory import (
load_latest_gitea_actions_secret_variable_name_inventory,
)
from src.services.gitea_branch_protection_required_checks_readback import (
load_latest_gitea_branch_protection_required_checks_readback,
)
from src.services.gitea_capability_ci_cd_optimization_readback import (
load_latest_gitea_capability_ci_cd_optimization_readback,
)
from src.services.gitea_cicd_alert_receipt_chain_readback import (
load_latest_gitea_cicd_alert_receipt_chain_readback,
)
from src.services.gitea_full_server_backup_dr_receipts import (
load_latest_gitea_full_server_backup_dr_receipts,
)
from src.services.gitea_owner_coverage_attestation_validation import (
validate_gitea_owner_coverage_attestation,
)
@@ -4767,6 +4782,151 @@ async def get_runtime_surface_inventory() -> dict[str, Any]:
) from exc
@router.get(
"/gitea-cicd-alert-receipt-chain-readback",
response_model=dict[str, Any],
summary="取得 Gitea CI/CD / backup alert receipt chain readback",
description=(
"整合 Gitea workflow notify bridge、backup notification policy、"
"Gitea full-server DR receipts 與 Telegram monitoring coverage"
"回傳 receipt chain 缺口。此端點只讀 metadata不送 Telegram、"
"不呼叫 Bot API、不改 receiver/Alertmanager、不執行 backup/restore、"
"不觸發 workflow、不讀 secret、不保存 raw payload、不碰 GitHub 或 host/K8s。"
),
)
async def get_gitea_cicd_alert_receipt_chain_readback() -> dict[str, Any]:
"""Return Gitea CI/CD, backup, and alert receipt chain readback."""
try:
payload = await load_latest_gitea_cicd_alert_receipt_chain_readback()
return redact_public_lan_topology(payload)
except (json.JSONDecodeError, ValueError) as exc:
logger.error("gitea_cicd_alert_receipt_chain_readback_invalid", error=str(exc))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Gitea CI/CD alert receipt chain readback 無效",
) from exc
@router.get(
"/gitea-full-server-backup-dr-receipts",
response_model=dict[str, Any],
summary="取得 Gitea full-server backup / DR receipts",
description=(
"分層讀回 Gitea repository bundle、server backup target、Google Drive/offsite、"
"restore drill gate、DB/settings/LFS/packages/attachments/hooks component receipts。"
"此端點只做 read-only receipt readback不執行 backup/restore/offsite sync、"
"不讀 credential/secret、不寫 repo、不讀 raw session/SQLite、不碰 GitHub 或 host/K8s。"
),
)
async def get_gitea_full_server_backup_dr_receipts() -> dict[str, Any]:
"""Return layered Gitea full-server backup / DR receipt readback."""
try:
return await asyncio.to_thread(load_latest_gitea_full_server_backup_dr_receipts)
except (json.JSONDecodeError, ValueError) as exc:
logger.error("gitea_full_server_backup_dr_receipts_invalid", error=str(exc))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Gitea full-server backup / DR receipts 無效",
) from exc
@router.get(
"/gitea-branch-protection-required-checks-readback",
response_model=dict[str, Any],
summary="取得 Gitea branch protection / required checks source policy",
description=(
"從 Gitea dev/prod repo truth 與 `.gitea/workflows` 產生 main/dev "
"branch protection required-check source policy。此端點只回傳 source policy "
"與 redacted metadata 驗證下一步;不修改 Gitea branch protection、不觸發 workflow、"
"不讀 secret、不改 repo visibility、不讀 raw session/SQLite、不碰 GitHub 或 host/K8s。"
),
)
async def get_gitea_branch_protection_required_checks_readback() -> dict[str, Any]:
"""Return source-derived Gitea branch protection / required checks policy."""
try:
return await asyncio.to_thread(
load_latest_gitea_branch_protection_required_checks_readback
)
except FileNotFoundError as exc:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=str(exc),
) from exc
except (json.JSONDecodeError, ValueError) as exc:
logger.error(
"gitea_branch_protection_required_checks_readback_invalid",
error=str(exc),
)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Gitea branch protection required checks 讀回無效",
) from exc
@router.get(
"/gitea-actions-secret-variable-name-inventory",
response_model=dict[str, Any],
summary="取得 Gitea Actions Secret / Variable 名稱盤點",
description=(
"只讀取已提交 `.gitea/workflows` 中的 `${{ secrets.NAME }}` / "
"`${{ vars.NAME }}` 名稱引用,回傳 workflow 對應、風險分類與後續 "
"redacted metadata 驗證欄位。此端點不讀 Secret/Variable 值、不呼叫 "
"Gitea write API、不觸發 workflow、不改 repo 設定、不讀 raw session/SQLite、"
"不碰 GitHub 或 host/K8s。"
),
)
async def get_gitea_actions_secret_variable_name_inventory() -> dict[str, Any]:
"""Return value-safe Gitea Actions secret / variable name inventory."""
try:
return await asyncio.to_thread(
load_latest_gitea_actions_secret_variable_name_inventory
)
except (json.JSONDecodeError, ValueError) as exc:
logger.error(
"gitea_actions_secret_variable_name_inventory_invalid",
error=str(exc),
)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Gitea Actions Secret / Variable 名稱盤點無效",
) from exc
@router.get(
"/gitea-capability-ci-cd-optimization-readback",
response_model=dict[str, Any],
summary="取得 Gitea 完整功能覆蓋與 CI/CD 專業化優化讀回",
description=(
"整合 Gitea repo/dev-prod branch、Actions workflow/runner、"
"secret/variable source references、backup/DR、notification policy 與 "
"CI/CD optimization rows。此端點只做 read-only governance readback"
"不呼叫 GitHub、不觸發 workflow、不建立 repo、不同步 refs、不改 visibility、"
"不重啟 runner、不讀 secret value、不讀 raw session/SQLite、不操作 host/K8s。"
),
)
async def get_gitea_capability_ci_cd_optimization_readback() -> dict[str, Any]:
"""Return the Gitea capability and CI/CD optimization governance readback."""
try:
payload = await asyncio.to_thread(
load_latest_gitea_capability_ci_cd_optimization_readback
)
return redact_public_lan_topology(payload)
except FileNotFoundError as exc:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=str(exc),
) from exc
except (json.JSONDecodeError, ValueError) as exc:
logger.error(
"gitea_capability_ci_cd_optimization_readback_invalid",
error=str(exc),
)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Gitea 完整功能覆蓋與 CI/CD 優化讀回無效",
) from exc
@router.get(
"/gitea-workflow-runner-health",
response_model=dict[str, Any],

View File

@@ -0,0 +1,316 @@
"""Gitea Actions secret / variable name inventory.
This readback inventories only committed workflow references such as
`${{ secrets.NAME }}` and `${{ vars.NAME }}`. It never calls Gitea for Secret
values, never reads plaintext credentials, and never writes repository settings.
"""
from __future__ import annotations
from datetime import datetime
from pathlib import Path
from typing import Any
from zoneinfo import ZoneInfo
from src.services.gitea_capability_ci_cd_optimization_readback import (
build_gitea_workflow_source_inventory,
)
from src.services.snapshot_paths import resolve_repo_root
SCHEMA_VERSION = "gitea_actions_secret_variable_name_inventory_v1"
TZ_TAIPEI = ZoneInfo("Asia/Taipei")
def load_latest_gitea_actions_secret_variable_name_inventory(
repo_root: Path | None = None,
) -> dict[str, Any]:
"""Build the latest committed Gitea Actions secret-name inventory."""
root = repo_root or resolve_repo_root(Path(__file__))
return build_gitea_actions_secret_variable_name_inventory(
workflow_source_inventory=build_gitea_workflow_source_inventory(root)
)
def build_gitea_actions_secret_variable_name_inventory(
*,
workflow_source_inventory: dict[str, Any],
generated_at: str | None = None,
) -> dict[str, Any]:
"""Return a value-safe inventory of workflow secret and variable names."""
now = generated_at or datetime.now(TZ_TAIPEI).isoformat(timespec="seconds")
workflow_rows = _workflow_rows(workflow_source_inventory.get("workflow_rows"))
secret_rollups = _dict(workflow_source_inventory.get("secret_variable_rollups"))
secret_names = _strings(secret_rollups.get("referenced_secret_names"))
variable_names = _strings(secret_rollups.get("referenced_variable_names"))
secret_rows = _secret_rows(secret_names, workflow_rows)
variable_rows = _variable_rows(variable_names, workflow_rows)
workflow_usage_rows = _workflow_usage_rows(workflow_rows)
payload = {
"schema_version": SCHEMA_VERSION,
"generated_at": now,
"source_control_authority": "gitea",
"scope": "gitea_actions_secret_variable_name_inventory",
"status": "gitea_actions_secret_variable_name_inventory_ready",
"ready": True,
"operation_boundaries": {
"read_only_api_allowed": True,
"workflow_source_read_allowed": True,
"secret_name_inventory_allowed": True,
"variable_name_inventory_allowed": True,
"secret_value_read_allowed": False,
"variable_value_read_allowed": False,
"gitea_api_write_allowed": False,
"gitea_secret_update_allowed": False,
"gitea_variable_update_allowed": False,
"workflow_trigger_allowed": False,
"repo_visibility_change_allowed": False,
"raw_session_or_sqlite_read_allowed": False,
"github_api_allowed": False,
"host_or_k8s_write_allowed": False,
},
"summary": {
"workflow_file_count": _int(
workflow_source_inventory.get("workflow_file_count")
),
"workflows_using_secret_context_count": _int(
secret_rollups.get("workflows_using_secret_context_count")
),
"secret_reference_count": _int(secret_rollups.get("secret_reference_count")),
"referenced_secret_name_count": len(secret_rows),
"workflows_using_variable_context_count": _int(
secret_rollups.get("workflows_using_vars_context_count")
),
"variable_reference_count": _int(secret_rollups.get("vars_reference_count")),
"referenced_variable_name_count": len(variable_rows),
"secret_values_collected": False,
"variable_values_collected": False,
"gitea_configured_secret_metadata_verified": False,
"gitea_configured_variable_metadata_verified": False,
"name_inventory_complete_from_committed_workflows": True,
},
"secret_name_rows": secret_rows,
"variable_name_rows": variable_rows,
"workflow_usage_rows": workflow_usage_rows,
"metadata_verification_next_step": {
"status": "redacted_configuration_metadata_required",
"allowed_fields": [
"name",
"scope",
"repo",
"environment",
"updated_at",
"created_at",
"present",
],
"forbidden_fields": [
"value",
"plaintext",
"token",
"authorization_header",
"private_key",
"cookie",
"session",
],
"safe_next_action": (
"verify Gitea configured secret/variable names with redacted "
"metadata only; never export values"
),
},
}
_require_payload(payload, "gitea_actions_secret_variable_name_inventory")
return payload
def _secret_rows(
secret_names: list[str],
workflow_rows: list[dict[str, Any]],
) -> list[dict[str, Any]]:
return [
{
"name": name,
"kind": "secret",
"risk_category": _risk_category(name),
"workflow_refs": _refs_for_name(name, workflow_rows, "referenced_secret_names"),
"reference_count": _reference_count(
name,
workflow_rows,
"referenced_secret_names",
"secret_reference_count",
),
"value_read_status": "not_read_by_design",
"configuration_metadata_status": "redacted_metadata_required",
}
for name in secret_names
]
def _variable_rows(
variable_names: list[str],
workflow_rows: list[dict[str, Any]],
) -> list[dict[str, Any]]:
return [
{
"name": name,
"kind": "variable",
"risk_category": _risk_category(name),
"workflow_refs": _refs_for_name(name, workflow_rows, "referenced_variable_names"),
"reference_count": _reference_count(
name,
workflow_rows,
"referenced_variable_names",
"vars_reference_count",
),
"value_read_status": "not_read_by_design",
"configuration_metadata_status": "redacted_metadata_required",
}
for name in variable_names
]
def _workflow_usage_rows(workflow_rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
rows: list[dict[str, Any]] = []
for row in workflow_rows:
secret_names = _strings(row.get("referenced_secret_names"))
variable_names = _strings(row.get("referenced_variable_names"))
if not secret_names and not variable_names:
continue
rows.append(
{
"workflow_id": str(row.get("workflow_id") or ""),
"workflow_file": str(row.get("workflow_file") or ""),
"referenced_secret_names": secret_names,
"referenced_variable_names": variable_names,
"secret_reference_count": _int(row.get("secret_reference_count")),
"variable_reference_count": _int(row.get("vars_reference_count")),
"value_read_status": "not_read_by_design",
}
)
return rows
def _refs_for_name(
name: str,
workflow_rows: list[dict[str, Any]],
field: str,
) -> list[str]:
return sorted(
str(row.get("workflow_file") or row.get("workflow_id") or "")
for row in workflow_rows
if name in _strings(row.get(field))
)
def _reference_count(
name: str,
workflow_rows: list[dict[str, Any]],
names_field: str,
count_field: str,
) -> int:
# Per-workflow source rows expose unique names and total context references.
# Count the workflow-level total when this name appears in that workflow.
return sum(
_int(row.get(count_field))
for row in workflow_rows
if name in _strings(row.get(names_field))
)
def _risk_category(name: str) -> str:
lowered = name.lower()
if "private_key" in lowered or lowered.endswith("_key"):
return "credential_key"
if "token" in lowered:
return "token"
if "password" in lowered:
return "password"
if lowered.endswith("_url") or lowered in {"database_url", "redis_url"}:
return "connection_url"
if "secret" in lowered:
return "secret"
return "sensitive_config"
def _require_payload(payload: dict[str, Any], label: str) -> None:
if payload.get("schema_version") != SCHEMA_VERSION:
raise ValueError(f"{label}: invalid schema_version")
if payload.get("source_control_authority") != "gitea":
raise ValueError(f"{label}: source_control_authority must be gitea")
boundaries = _dict(payload.get("operation_boundaries"))
if boundaries.get("read_only_api_allowed") is not True:
raise ValueError(f"{label}: read_only_api_allowed must be true")
blocked_flags = {
"secret_value_read_allowed",
"variable_value_read_allowed",
"gitea_api_write_allowed",
"gitea_secret_update_allowed",
"gitea_variable_update_allowed",
"workflow_trigger_allowed",
"repo_visibility_change_allowed",
"raw_session_or_sqlite_read_allowed",
"github_api_allowed",
"host_or_k8s_write_allowed",
}
unexpected = sorted(flag for flag in blocked_flags if boundaries.get(flag) is not False)
if unexpected:
raise ValueError(f"{label}: blocked operation flags must be false: {unexpected}")
summary = _dict(payload.get("summary"))
secret_rows = _list(payload.get("secret_name_rows"))
variable_rows = _list(payload.get("variable_name_rows"))
if summary.get("referenced_secret_name_count") != len(secret_rows):
raise ValueError(f"{label}: referenced_secret_name_count mismatch")
if summary.get("referenced_variable_name_count") != len(variable_rows):
raise ValueError(f"{label}: referenced_variable_name_count mismatch")
if summary.get("secret_values_collected") is not False:
raise ValueError(f"{label}: secret_values_collected must be false")
if summary.get("variable_values_collected") is not False:
raise ValueError(f"{label}: variable_values_collected must be false")
_require_no_plaintext_like_keys(payload, label)
def _require_no_plaintext_like_keys(value: Any, label: str, path: str = "$") -> None:
forbidden = {
"value",
"plaintext",
"token_value",
"secret_value",
"authorization_header",
"private_key_value",
"cookie",
"session",
}
if isinstance(value, dict):
for key, child in value.items():
lowered = str(key).lower()
if lowered in forbidden or lowered.endswith("_value"):
if lowered not in {"value_read_status"}:
raise ValueError(f"{label}: forbidden key at {path}.{key}")
_require_no_plaintext_like_keys(child, label, f"{path}.{key}")
elif isinstance(value, list):
for index, child in enumerate(value):
_require_no_plaintext_like_keys(child, label, f"{path}[{index}]")
def _workflow_rows(value: Any) -> list[dict[str, Any]]:
return [row for row in value if isinstance(row, dict)] if isinstance(value, list) else []
def _dict(value: Any) -> dict[str, Any]:
return value if isinstance(value, dict) else {}
def _list(value: Any) -> list[Any]:
return value if isinstance(value, list) else []
def _strings(value: Any) -> list[str]:
return [str(item) for item in value] if isinstance(value, list) else []
def _int(value: Any) -> int:
try:
return int(value)
except (TypeError, ValueError):
return 0

View File

@@ -0,0 +1,283 @@
"""Gitea branch protection / required checks source policy readback.
This module defines the desired, source-derived branch protection contract for
all product repositories. It does not call Gitea settings APIs, modify branch
protection, trigger workflows, or read credentials.
"""
from __future__ import annotations
import json
from datetime import datetime
from pathlib import Path
from typing import Any
from zoneinfo import ZoneInfo
from src.services.gitea_capability_ci_cd_optimization_readback import (
build_gitea_workflow_source_inventory,
)
from src.services.snapshot_paths import default_operations_dir, resolve_repo_root
SCHEMA_VERSION = "gitea_branch_protection_required_checks_readback_v1"
TZ_TAIPEI = ZoneInfo("Asia/Taipei")
_DEV_PROD_REPO_READBACK_FILE = "gitea-all-product-dev-prod-repo-readback.snapshot.json"
def load_latest_gitea_branch_protection_required_checks_readback(
*,
repo_root: Path | None = None,
operations_dir: Path | None = None,
) -> dict[str, Any]:
"""Build the latest source policy for Gitea branch protection."""
root = repo_root or resolve_repo_root(Path(__file__))
operations = operations_dir or default_operations_dir(Path(__file__))
dev_prod = _load_json(operations / _DEV_PROD_REPO_READBACK_FILE)
return build_gitea_branch_protection_required_checks_readback(
dev_prod_repo_readback=dev_prod,
workflow_source_inventory=build_gitea_workflow_source_inventory(root),
)
def build_gitea_branch_protection_required_checks_readback(
*,
dev_prod_repo_readback: dict[str, Any],
workflow_source_inventory: dict[str, Any],
generated_at: str | None = None,
) -> dict[str, Any]:
"""Build a desired branch protection policy from committed source truth."""
now = generated_at or datetime.now(TZ_TAIPEI).isoformat(timespec="seconds")
products = _product_rows(dev_prod_repo_readback.get("products"))
workflow_ids = set(_strings(workflow_source_inventory.get("workflow_ids")))
main_checks = _present(
workflow_ids,
["code-review", "cd", "type-sync-check"],
)
dev_checks = _present(
workflow_ids,
["cd-dev", "type-sync-check"],
)
protection_rows = _protection_rows(products, main_checks, dev_checks)
excluded = _present(
workflow_ids,
[
"run-migration",
"harbor-110-local-repair",
"awoooi-onboarding-warning-step",
],
)
missing_required_sources = sorted(
set(["cd", "cd-dev", "code-review", "type-sync-check"]) - workflow_ids
)
status_value = (
"gitea_branch_protection_required_checks_source_policy_ready"
if not missing_required_sources and protection_rows
else "blocked_gitea_branch_protection_required_check_source_missing"
)
payload = {
"schema_version": SCHEMA_VERSION,
"generated_at": now,
"source_control_authority": "gitea",
"scope": "all_product_repositories_branch_protection_required_checks",
"status": status_value,
"ready": status_value.endswith("_ready"),
"operation_boundaries": {
"read_only_api_allowed": True,
"source_policy_generation_allowed": True,
"gitea_branch_protection_write_allowed": False,
"gitea_required_check_write_allowed": False,
"repo_visibility_change_allowed": False,
"workflow_trigger_allowed": False,
"secret_value_read_allowed": False,
"raw_session_or_sqlite_read_allowed": False,
"github_api_allowed": False,
"host_or_k8s_write_allowed": False,
},
"summary": {
"expected_product_count": len(products),
"protected_repo_count": len({row["gitea_repo"] for row in protection_rows}),
"branch_policy_row_count": len(protection_rows),
"main_branch_policy_count": _count_branch(protection_rows, "main"),
"dev_branch_policy_count": _count_branch(protection_rows, "dev"),
"source_required_check_workflow_count": len(
sorted(set(main_checks + dev_checks))
),
"missing_required_check_source_count": len(missing_required_sources),
"live_branch_protection_verified": False,
"live_required_checks_verified": False,
"controlled_apply_required": True,
},
"required_check_policy": {
"main_branch": {
"branch": "main",
"required_workflow_ids": main_checks,
"minimum_required_approvals": 1,
"required_status": "success",
"rationale": "main is the production branch and must pass review/source checks plus CD closure.",
},
"dev_branch": {
"branch": "dev",
"required_workflow_ids": dev_checks,
"minimum_required_approvals": 0,
"required_status": "success",
"rationale": "dev is the development branch and must pass dev deploy/source checks.",
},
"excluded_manual_or_high_risk_workflows": excluded,
},
"protection_rows": protection_rows,
"missing_required_check_sources": missing_required_sources,
"metadata_verification_next_step": {
"status": "redacted_branch_settings_metadata_required",
"allowed_fields": [
"repo",
"branch",
"protected",
"required_status_checks",
"required_approvals",
"push_restrictions_present",
"updated_at",
],
"forbidden_fields": [
"token",
"authorization_header",
"private_key",
"cookie",
"session",
"secret_value",
],
"safe_next_action": (
"compare Gitea branch settings metadata against this source "
"policy before any controlled apply"
),
},
}
_require_payload(payload, "gitea_branch_protection_required_checks_readback")
return payload
def _protection_rows(
products: list[dict[str, Any]],
main_checks: list[str],
dev_checks: list[str],
) -> list[dict[str, Any]]:
rows: list[dict[str, Any]] = []
for product in products:
repo = str(product.get("gitea_repo") or "")
if not repo:
continue
if product.get("prod_branch_present") is True:
rows.append(
{
"product_id": str(product.get("product_id") or ""),
"gitea_repo": repo,
"branch": "main",
"environment": "production",
"required_workflow_ids": main_checks,
"required_approval_count": 1,
"policy_source_status": "source_ready",
"live_settings_status": "redacted_metadata_required",
}
)
if product.get("dev_branch_present") is True:
rows.append(
{
"product_id": str(product.get("product_id") or ""),
"gitea_repo": repo,
"branch": "dev",
"environment": "development",
"required_workflow_ids": dev_checks,
"required_approval_count": 0,
"policy_source_status": "source_ready",
"live_settings_status": "redacted_metadata_required",
}
)
return rows
def _require_payload(payload: dict[str, Any], label: str) -> None:
if payload.get("schema_version") != SCHEMA_VERSION:
raise ValueError(f"{label}: invalid schema_version")
if payload.get("source_control_authority") != "gitea":
raise ValueError(f"{label}: source_control_authority must be gitea")
boundaries = _dict(payload.get("operation_boundaries"))
if boundaries.get("read_only_api_allowed") is not True:
raise ValueError(f"{label}: read_only_api_allowed must be true")
blocked_flags = {
"gitea_branch_protection_write_allowed",
"gitea_required_check_write_allowed",
"repo_visibility_change_allowed",
"workflow_trigger_allowed",
"secret_value_read_allowed",
"raw_session_or_sqlite_read_allowed",
"github_api_allowed",
"host_or_k8s_write_allowed",
}
unexpected = sorted(flag for flag in blocked_flags if boundaries.get(flag) is not False)
if unexpected:
raise ValueError(f"{label}: blocked operation flags must be false: {unexpected}")
summary = _dict(payload.get("summary"))
rows = _list(payload.get("protection_rows"))
if summary.get("branch_policy_row_count") != len(rows):
raise ValueError(f"{label}: branch_policy_row_count mismatch")
if summary.get("missing_required_check_source_count") != len(
_list(payload.get("missing_required_check_sources"))
):
raise ValueError(f"{label}: missing_required_check_source_count mismatch")
if summary.get("live_branch_protection_verified") is not False:
raise ValueError(f"{label}: live_branch_protection_verified must be false")
if summary.get("live_required_checks_verified") is not False:
raise ValueError(f"{label}: live_required_checks_verified must be false")
_require_no_plaintext_like_keys(payload, label)
def _require_no_plaintext_like_keys(value: Any, label: str, path: str = "$") -> None:
forbidden = {
"secret_value",
"token_value",
"authorization_header",
"private_key",
"cookie",
"session",
}
if isinstance(value, dict):
for key, child in value.items():
lowered = str(key).lower()
if lowered in forbidden or lowered.endswith("_secret_value"):
raise ValueError(f"{label}: forbidden key at {path}.{key}")
_require_no_plaintext_like_keys(child, label, f"{path}.{key}")
elif isinstance(value, list):
for index, child in enumerate(value):
_require_no_plaintext_like_keys(child, label, f"{path}[{index}]")
def _present(workflow_ids: set[str], candidates: list[str]) -> list[str]:
return [candidate for candidate in candidates if candidate in workflow_ids]
def _count_branch(rows: list[dict[str, Any]], branch: str) -> int:
return sum(1 for row in rows if row.get("branch") == branch)
def _load_json(path: Path) -> dict[str, Any]:
with path.open(encoding="utf-8") as handle:
payload = json.load(handle)
if not isinstance(payload, dict):
raise ValueError(f"{path}: expected JSON object")
return payload
def _product_rows(value: Any) -> list[dict[str, Any]]:
return [row for row in value if isinstance(row, dict)] if isinstance(value, list) else []
def _dict(value: Any) -> dict[str, Any]:
return value if isinstance(value, dict) else {}
def _list(value: Any) -> list[Any]:
return value if isinstance(value, list) else []
def _strings(value: Any) -> list[str]:
return [str(item) for item in value] if isinstance(value, list) else []

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,246 @@
"""Gitea CI/CD, backup, and alert receipt chain readback."""
from __future__ import annotations
import asyncio
from datetime import datetime
from typing import Any
from zoneinfo import ZoneInfo
from src.services.backup_notification_policy import (
load_latest_backup_notification_policy,
)
from src.services.gitea_full_server_backup_dr_receipts import (
load_latest_gitea_full_server_backup_dr_receipts,
)
from src.services.gitea_workflow_runner_health import (
load_latest_gitea_workflow_runner_health,
)
from src.services.telegram_alert_monitoring_coverage_readback import (
load_latest_telegram_alert_monitoring_coverage_readback,
)
SCHEMA_VERSION = "gitea_cicd_alert_receipt_chain_readback_v1"
TZ_TAIPEI = ZoneInfo("Asia/Taipei")
async def load_latest_gitea_cicd_alert_receipt_chain_readback(
*,
project_id: str = "awoooi",
) -> dict[str, Any]:
"""Load live/source alert receipt chain readback without sending messages."""
telegram_coverage, backup_policy, workflow_health, full_dr = await asyncio.gather(
load_latest_telegram_alert_monitoring_coverage_readback(project_id=project_id),
asyncio.to_thread(load_latest_backup_notification_policy),
asyncio.to_thread(load_latest_gitea_workflow_runner_health),
asyncio.to_thread(load_latest_gitea_full_server_backup_dr_receipts),
)
return build_gitea_cicd_alert_receipt_chain_readback(
telegram_coverage=telegram_coverage,
backup_notification_policy=backup_policy,
workflow_runner_health=workflow_health,
full_server_backup_dr_receipts=full_dr,
)
def build_gitea_cicd_alert_receipt_chain_readback(
*,
telegram_coverage: dict[str, Any],
backup_notification_policy: dict[str, Any],
workflow_runner_health: dict[str, Any],
full_server_backup_dr_receipts: dict[str, Any],
generated_at: str | None = None,
) -> dict[str, Any]:
"""Compose CI/CD and backup alert receipt chain coverage."""
now = generated_at or datetime.now(TZ_TAIPEI).isoformat(timespec="seconds")
coverage_summary = _dict(telegram_coverage.get("summary"))
backup_rollups = _dict(backup_notification_policy.get("rollups"))
workflow_rollups = _dict(workflow_runner_health.get("rollups"))
full_dr_summary = _dict(full_server_backup_dr_receipts.get("summary"))
monitoring_gap_count = _int(coverage_summary.get("monitoring_live_receipt_gap_count"))
full_dr_gap_count = _int(full_server_backup_dr_receipts.get("active_gap_count"))
quiet_contract_count = len(_list(backup_rollups.get("suppressed_success_rule_ids")))
immediate_escalation_count = len(
_list(backup_rollups.get("immediate_escalation_rule_ids"))
)
notify_workflow_count = _int(workflow_rollups.get("workflows_with_notify_bridge"))
receipt_rows = [
{
"chain_id": "gitea_cd_pipeline_notifications",
"domain": "cicd",
"status": "source_ready" if notify_workflow_count > 0 else "action_required",
"receipt_state": "notify_bridge_source_present",
"evidence_refs": [
"GET /api/v1/agents/gitea-workflow-runner-health",
".gitea/workflows/cd.yaml",
],
"next_action": "bind CD notify bridge delivery receipts to deployed marker readback",
},
{
"chain_id": "backup_notification_policy",
"domain": "backup",
"status": "policy_ready"
if quiet_contract_count > 0 and immediate_escalation_count > 0
else "action_required",
"receipt_state": "failure_or_action_required_policy_present",
"evidence_refs": ["GET /api/v1/agents/backup-notification-policy"],
"next_action": "bind backup policy decisions to live receipt stage evidence",
"metrics": {
"suppressed_success_rule_count": quiet_contract_count,
"immediate_escalation_rule_count": immediate_escalation_count,
},
},
{
"chain_id": "gitea_full_server_dr_receipts",
"domain": "backup_dr",
"status": "action_required" if full_dr_gap_count else "verified_ready",
"receipt_state": "component_receipts_pending"
if full_dr_gap_count
else "component_receipts_ready",
"evidence_refs": ["GET /api/v1/agents/gitea-full-server-backup-dr-receipts"],
"next_action": "close full-server component receipt rows before DR complete",
"metrics": {
"full_server_active_gap_count": full_dr_gap_count,
"full_server_verified_ready_component_count": _int(
full_dr_summary.get("verified_ready_component_count")
),
},
},
{
"chain_id": "telegram_monitoring_live_receipts",
"domain": "observability",
"status": "action_required" if monitoring_gap_count else "verified_ready",
"receipt_state": "monitoring_live_receipts_pending"
if monitoring_gap_count
else "monitoring_live_receipts_ready",
"evidence_refs": [
"GET /api/v1/agents/telegram-alert-monitoring-coverage-readback"
],
"next_action": "write metadata-only live receipt batches for monitoring surfaces",
"metrics": {
"monitoring_live_receipt_gap_count": monitoring_gap_count,
"alert_receipt_pipeline_ready_count": _int(
coverage_summary.get("alert_receipt_pipeline_ready_count")
),
"alert_receipt_pipeline_stage_count": _int(
coverage_summary.get("alert_receipt_pipeline_stage_count")
),
},
},
{
"chain_id": "ai_agent_context_receipts",
"domain": "ai_context",
"status": "source_ready"
if _int(coverage_summary.get("verified_ai_agent_context_receipt_count")) > 0
else "action_required",
"receipt_state": "ai_context_receipts_projected",
"evidence_refs": [
"GET /api/v1/agents/telegram-alert-monitoring-coverage-readback"
],
"next_action": "keep KM/RAG/MCP/PlayBook context receipts metadata-only",
},
]
active_gaps = [
row["chain_id"]
for row in receipt_rows
if row["status"] == "action_required"
]
payload = {
"schema_version": SCHEMA_VERSION,
"generated_at": now,
"source_control_authority": "gitea",
"scope": "gitea_cicd_backup_alert_receipt_chain",
"status": (
"gitea_cicd_alert_receipt_chain_ready"
if not active_gaps
else "gitea_cicd_alert_receipt_chain_gaps_present"
),
"ready": not active_gaps,
"active_gap_ids": active_gaps,
"active_gap_count": len(active_gaps),
"operation_boundaries": {
"read_only_api_allowed": True,
"metadata_read_allowed": True,
"telegram_send_allowed": False,
"bot_api_call_allowed": False,
"receiver_route_change_allowed": False,
"alertmanager_reload_allowed": False,
"backup_execution_allowed": False,
"restore_execution_allowed": False,
"workflow_trigger_allowed": False,
"secret_value_read_allowed": False,
"raw_payload_storage_allowed": False,
"github_api_allowed": False,
"host_or_k8s_write_allowed": False,
},
"summary": {
"receipt_chain_count": len(receipt_rows),
"source_or_policy_ready_chain_count": sum(
1
for row in receipt_rows
if row["status"] in {"source_ready", "policy_ready", "verified_ready"}
),
"action_required_chain_count": len(active_gaps),
"monitoring_live_receipt_gap_count": monitoring_gap_count,
"full_server_dr_active_gap_count": full_dr_gap_count,
"workflow_notify_bridge_count": notify_workflow_count,
"backup_quiet_success_contract_count": quiet_contract_count,
"backup_immediate_escalation_rule_count": immediate_escalation_count,
},
"receipt_chain_rows": receipt_rows,
"safe_next_actions": [
"write metadata-only live receipt batches for monitoring surfaces",
"bind backup/CD notification outcomes to receipt-stage evidence",
"keep Telegram sends disabled from this readback endpoint",
],
}
_require_payload(payload, "gitea_cicd_alert_receipt_chain_readback")
return payload
def _require_payload(payload: dict[str, Any], label: str) -> None:
if payload.get("schema_version") != SCHEMA_VERSION:
raise ValueError(f"{label}: invalid schema_version")
boundaries = _dict(payload.get("operation_boundaries"))
if boundaries.get("read_only_api_allowed") is not True:
raise ValueError(f"{label}: read_only_api_allowed must be true")
blocked_flags = {
"telegram_send_allowed",
"bot_api_call_allowed",
"receiver_route_change_allowed",
"alertmanager_reload_allowed",
"backup_execution_allowed",
"restore_execution_allowed",
"workflow_trigger_allowed",
"secret_value_read_allowed",
"raw_payload_storage_allowed",
"github_api_allowed",
"host_or_k8s_write_allowed",
}
unexpected = sorted(flag for flag in blocked_flags if boundaries.get(flag) is not False)
if unexpected:
raise ValueError(f"{label}: blocked operation flags must be false: {unexpected}")
rows = _list(payload.get("receipt_chain_rows"))
summary = _dict(payload.get("summary"))
if summary.get("receipt_chain_count") != len(rows):
raise ValueError(f"{label}: receipt_chain_count mismatch")
if payload.get("active_gap_count") != len(_list(payload.get("active_gap_ids"))):
raise ValueError(f"{label}: active_gap_count mismatch")
def _dict(value: Any) -> dict[str, Any]:
return value if isinstance(value, dict) else {}
def _list(value: Any) -> list[Any]:
return value if isinstance(value, list) else []
def _int(value: Any) -> int:
try:
return int(value)
except (TypeError, ValueError):
return 0

View File

@@ -0,0 +1,553 @@
"""Gitea full-server backup / DR receipt readback.
Repository bundle backups are necessary but not the whole Gitea DR story. This
readback separates repository bundles, the Gitea server backup target, offsite
mirror evidence, restore-drill gates, and component receipts for database,
settings, LFS, packages, attachments, and hooks.
"""
from __future__ import annotations
import json
from datetime import datetime
from pathlib import Path
from typing import Any
from urllib.parse import urlencode
from urllib.request import Request, urlopen
from zoneinfo import ZoneInfo
from src.core.config import settings
from src.services.backup_dr_readiness_matrix import (
load_latest_backup_dr_readiness_matrix,
)
from src.services.backup_dr_target_inventory import (
load_latest_backup_dr_target_inventory,
)
from src.services.gitea_repo_bundle_backup_readback import (
load_latest_gitea_repo_bundle_backup_readback,
)
from src.services.snapshot_paths import resolve_repo_root
SCHEMA_VERSION = "gitea_full_server_backup_dr_receipts_v1"
TZ_TAIPEI = ZoneInfo("Asia/Taipei")
DEFAULT_HOST = "110"
PROMETHEUS_TIMEOUT_SECONDS = 4
_GITEA_COMPONENT_IDS = {
"database": "gitea_database_receipt",
"app_settings": "gitea_app_settings_receipt",
"lfs_objects": "gitea_lfs_receipt",
"package_registry": "gitea_packages_receipt",
"attachments": "gitea_attachments_receipt",
"hooks_custom_assets": "gitea_hooks_custom_assets_receipt",
}
def load_latest_gitea_full_server_backup_dr_receipts(
*,
host: str = DEFAULT_HOST,
prometheus_url: str | None = None,
repo_root: Path | None = None,
) -> dict[str, Any]:
"""Build the latest Gitea full-server backup / DR receipt readback."""
samples: list[dict[str, Any]] = []
errors: list[str] = []
url = (prometheus_url or settings.PROMETHEUS_URL).rstrip("/")
for query in _prometheus_queries(host):
try:
samples.extend(_query_prometheus_vector(url, query))
except Exception as exc: # noqa: BLE001 - fail closed with redacted error class
errors.append(f"{query.split('{', 1)[0]}:{exc.__class__.__name__}")
return build_gitea_full_server_backup_dr_receipts(
repo_bundle_backup_readback=load_latest_gitea_repo_bundle_backup_readback(),
backup_dr_readiness_matrix=load_latest_backup_dr_readiness_matrix(),
backup_dr_target_inventory=load_latest_backup_dr_target_inventory(),
metric_samples=samples,
metrics_error=",".join(errors),
source_contract=build_gitea_full_server_backup_source_contract(
repo_root or resolve_repo_root(Path(__file__))
),
host=host,
)
def build_gitea_full_server_backup_source_contract(repo_root: Path) -> dict[str, Any]:
"""Inspect committed Gitea backup/offsite source without reading secrets."""
backup_script = repo_root / "scripts" / "backup" / "backup-gitea.sh"
sync_script = repo_root / "scripts" / "backup" / "sync-offsite-backups.sh"
verify_script = repo_root / "scripts" / "backup" / "verify-offsite-full-sync.sh"
backup_text = _read_text(backup_script)
sync_text = _read_text(sync_script)
verify_text = _read_text(verify_script)
metric_writer_present = (
"awoooi_gitea_full_backup_component_receipt" in backup_text
and "secret_value_collected" in backup_text
and "production_restore_performed" in backup_text
)
gitea_dump_present = "gitea dump" in backup_text and "/tmp/gitea-dump.zip" in backup_text
restic_target_present = "restic -r" in backup_text and (
"/backup" in backup_text or "BACKUP_BASE" in backup_text
)
offsite_source_ready = (
"OFFSITE_RCLONE_REMOTE" in sync_text
and "gdrive" in sync_text
and "awoooi_backup_offsite_remote_verify_ok" in verify_text
)
component_rows = [
{
"component_id": component_id,
"metric_component": metric_component,
"source_status": "source_ready"
if metric_writer_present and gitea_dump_present and restic_target_present
else "source_missing",
"source_refs": ["scripts/backup/backup-gitea.sh"],
"metric_name": "awoooi_gitea_full_backup_component_receipt",
}
for metric_component, component_id in _GITEA_COMPONENT_IDS.items()
]
return {
"schema_version": "gitea_full_server_backup_source_contract_v1",
"source_refs": [
"scripts/backup/backup-gitea.sh",
"scripts/backup/sync-offsite-backups.sh",
"scripts/backup/verify-offsite-full-sync.sh",
],
"gitea_dump_source_ready": gitea_dump_present,
"restic_target_source_ready": restic_target_present,
"component_metric_writer_source_ready": metric_writer_present,
"google_drive_offsite_source_ready": offsite_source_ready,
"component_rows": component_rows,
"source_ready_component_count": sum(
1 for row in component_rows if row["source_status"] == "source_ready"
),
"secret_values_collected_by_source_inspection": False,
}
def build_gitea_full_server_backup_dr_receipts(
*,
repo_bundle_backup_readback: dict[str, Any],
backup_dr_readiness_matrix: dict[str, Any],
backup_dr_target_inventory: dict[str, Any],
metric_samples: list[dict[str, Any]] | None = None,
metrics_error: str = "",
source_contract: dict[str, Any] | None = None,
host: str = DEFAULT_HOST,
generated_at: str | None = None,
) -> dict[str, Any]:
"""Compose full-server Gitea backup evidence without executing backups."""
now = generated_at or datetime.now(TZ_TAIPEI).isoformat(timespec="seconds")
readiness_rows = _rows(backup_dr_readiness_matrix.get("readiness_rows"))
target_rows = _rows(backup_dr_target_inventory.get("backup_targets"))
gitea_readiness = _find(readiness_rows, "target_id", "gitea")
offsite_readiness = _find(readiness_rows, "target_id", "offsite_rclone_full_sync")
gitea_target = _find(target_rows, "target_id", "gitea")
repo_bundle_ready = repo_bundle_backup_readback.get("ready") is True
repo_bundle_summary = _dict(repo_bundle_backup_readback.get("summary"))
contract = _dict(source_contract) or {
"schema_version": "gitea_full_server_backup_source_contract_v1",
"source_refs": [],
"component_rows": [],
"source_ready_component_count": 0,
"secret_values_collected_by_source_inspection": False,
}
receipt_metrics = _receipt_metric_rollup(metric_samples or [], host)
component_rows = [
{
"component_id": "repository_bundles",
"display_name": "Gitea repository bundles",
"status": "verified_ready" if repo_bundle_ready else "action_required",
"evidence_refs": ["GET /api/v1/agents/gitea-repo-bundle-backup-readback"],
"receipt_state": "fresh_bundle_and_sample_restore"
if repo_bundle_ready
else "bundle_readback_gap",
"restore_scope": "sample_restore_dry_run_only",
"production_restore_performed": False,
"metrics": {
"expected_repo_count": _int(repo_bundle_summary.get("expected_repo_count")),
"repo_row_count": _int(repo_bundle_summary.get("repo_row_count")),
"sample_restore_dry_run_ok": repo_bundle_summary.get(
"sample_restore_dry_run_ok"
)
is True,
},
},
_target_component(
"gitea_server_backup_target",
"Gitea server backup target",
gitea_readiness,
gitea_target,
"full_server_backup_target_ready",
),
_target_component(
"google_drive_offsite_mirror",
"Google Drive / rclone offsite mirror",
offsite_readiness,
_find(target_rows, "target_id", "offsite_rclone_full_sync"),
"offsite_mirror_verified",
),
{
"component_id": "restore_drill_approval_gate",
"display_name": "Restore drill approval gate",
"status": "verified_ready"
if receipt_metrics["restore_drill_performed"]
else "approval_required",
"evidence_refs": ["GET /api/v1/agents/backup-dr-readiness-matrix"],
"receipt_state": "restore_drill_receipt_present"
if receipt_metrics["restore_drill_performed"]
else "restore_drill_not_executed_by_readback",
"restore_scope": "restore requires controlled drill approval",
"production_restore_performed": False,
"metrics": {
"gitea_restore_drill_status": str(
gitea_readiness.get("restore_drill_status") or ""
),
"restore_execution_allowed": False,
"restore_drill_performed_metric": receipt_metrics[
"restore_drill_performed"
],
},
},
*_component_receipt_rows(contract, receipt_metrics),
]
active_gaps = [
row["component_id"]
for row in component_rows
if row["status"] in {"action_required", "receipt_required", "approval_required"}
]
status_value = (
"gitea_full_server_backup_dr_receipts_ready"
if not active_gaps
else "gitea_full_server_backup_dr_receipts_gaps_present"
)
payload = {
"schema_version": SCHEMA_VERSION,
"generated_at": now,
"source_control_authority": "gitea",
"scope": "gitea_full_server_backup_dr_receipts",
"host": host,
"status": status_value,
"ready": not active_gaps,
"active_gap_ids": active_gaps,
"active_gap_count": len(active_gaps),
"operation_boundaries": {
"read_only_api_allowed": True,
"backup_execution_allowed": False,
"restore_execution_allowed": False,
"production_restore_allowed": False,
"offsite_sync_execution_allowed": False,
"credential_read_allowed": False,
"secret_value_read_allowed": False,
"gitea_repo_write_allowed": False,
"raw_session_or_sqlite_read_allowed": False,
"github_api_allowed": False,
"host_or_k8s_write_allowed": False,
},
"summary": {
"component_count": len(component_rows),
"verified_ready_component_count": _count_status(
component_rows,
"verified_ready",
),
"source_ready_component_count": _count_status(
component_rows,
"source_ready",
),
"receipt_required_component_count": _count_status(
component_rows,
"receipt_required",
),
"approval_required_component_count": _count_status(
component_rows,
"approval_required",
),
"repo_bundle_ready": repo_bundle_ready,
"gitea_target_overall_readiness": str(
gitea_readiness.get("overall_readiness") or ""
),
"gitea_target_offsite_status": str(
gitea_readiness.get("offsite_status") or ""
),
"offsite_mirror_overall_readiness": str(
offsite_readiness.get("overall_readiness") or ""
),
"gitea_full_backup_metrics_present": receipt_metrics["metrics_present"],
"gitea_full_backup_component_receipt_count": receipt_metrics[
"component_receipt_count"
],
"gitea_full_backup_metric_error": metrics_error,
"production_restore_performed": False,
},
"source_contract": contract,
"receipt_metric_rollup": receipt_metrics,
"component_rows": component_rows,
"safe_next_actions": [
"deploy backup-gitea.sh receipt metric writer and rerun the normal Gitea backup cadence",
"keep production restore disabled; use controlled restore-drill approval only",
"verify Google Drive/offsite freshness with redacted non-secret receipts",
],
}
_require_payload(payload, "gitea_full_server_backup_dr_receipts")
return payload
def _target_component(
component_id: str,
display_name: str,
readiness: dict[str, Any],
target: dict[str, Any],
ready_receipt_state: str,
) -> dict[str, Any]:
ready = (
readiness.get("overall_readiness") == "ready"
and readiness.get("offsite_status") == "verified"
)
return {
"component_id": component_id,
"display_name": display_name,
"status": "verified_ready" if ready else "action_required",
"evidence_refs": _strings(readiness.get("evidence_refs"))
or _strings(target.get("evidence_refs")),
"receipt_state": ready_receipt_state if ready else "target_readiness_gap",
"restore_scope": str(readiness.get("gate_status") or target.get("restore_gate_status") or ""),
"production_restore_performed": False,
"metrics": {
"overall_readiness": str(readiness.get("overall_readiness") or ""),
"offsite_status": str(readiness.get("offsite_status") or ""),
"storage_ref": str(target.get("storage_ref") or ""),
},
}
def _component_receipt_rows(
source_contract: dict[str, Any],
receipt_metrics: dict[str, Any],
) -> list[dict[str, Any]]:
component_specs = [
("gitea_database_receipt", "database", "Gitea database dump / restore receipt"),
("gitea_app_settings_receipt", "app_settings", "Gitea app.ini and settings receipt"),
("gitea_lfs_receipt", "lfs_objects", "Gitea LFS object receipt"),
("gitea_packages_receipt", "package_registry", "Gitea package registry receipt"),
("gitea_attachments_receipt", "attachments", "Gitea issue/wiki attachment receipt"),
("gitea_hooks_custom_assets_receipt", "hooks_custom_assets", "Gitea hooks/custom assets receipt"),
]
source_by_id = {
str(row.get("component_id") or ""): row
for row in _rows(source_contract.get("component_rows"))
}
metric_components = set(_strings(receipt_metrics.get("component_receipt_components")))
rows: list[dict[str, Any]] = []
for component_id, metric_component, display_name in component_specs:
source_ready = (
_dict(source_by_id.get(component_id)).get("source_status") == "source_ready"
)
metric_ready = metric_component in metric_components
if metric_ready:
status = "verified_ready"
receipt_state = "live_component_receipt_bound"
elif source_ready:
status = "source_ready"
receipt_state = "source_contract_ready_live_receipt_pending"
else:
status = "receipt_required"
receipt_state = "component_receipt_not_bound_to_readback"
rows.append(
{
"component_id": component_id,
"display_name": display_name,
"status": status,
"evidence_refs": [
"scripts/backup/backup-gitea.sh",
"GET /api/v1/agents/backup-dr-readiness-matrix",
],
"receipt_state": receipt_state,
"restore_scope": "restore drill approval required before execution",
"production_restore_performed": False,
"metrics": {
"metric_component": metric_component,
"component_metric_present": metric_ready,
"source_contract_present": source_ready,
"secret_value_collection_allowed": False,
"componentized_receipt_required": True,
},
}
)
return rows
def _require_payload(payload: dict[str, Any], label: str) -> None:
if payload.get("schema_version") != SCHEMA_VERSION:
raise ValueError(f"{label}: invalid schema_version")
boundaries = _dict(payload.get("operation_boundaries"))
if boundaries.get("read_only_api_allowed") is not True:
raise ValueError(f"{label}: read_only_api_allowed must be true")
blocked_flags = {
"backup_execution_allowed",
"restore_execution_allowed",
"production_restore_allowed",
"offsite_sync_execution_allowed",
"credential_read_allowed",
"secret_value_read_allowed",
"gitea_repo_write_allowed",
"raw_session_or_sqlite_read_allowed",
"github_api_allowed",
"host_or_k8s_write_allowed",
}
unexpected = sorted(flag for flag in blocked_flags if boundaries.get(flag) is not False)
if unexpected:
raise ValueError(f"{label}: blocked operation flags must be false: {unexpected}")
rows = _rows(payload.get("component_rows"))
summary = _dict(payload.get("summary"))
if summary.get("component_count") != len(rows):
raise ValueError(f"{label}: component_count mismatch")
if payload.get("active_gap_count") != len(_strings(payload.get("active_gap_ids"))):
raise ValueError(f"{label}: active_gap_count mismatch")
if summary.get("production_restore_performed") is not False:
raise ValueError(f"{label}: production_restore_performed must be false")
if _dict(payload.get("source_contract")).get(
"secret_values_collected_by_source_inspection"
) is not False:
raise ValueError(f"{label}: source inspection must not collect secrets")
receipt_metrics = _dict(payload.get("receipt_metric_rollup"))
if receipt_metrics.get("secret_value_collected") is True:
raise ValueError(f"{label}: receipt metrics must not collect secrets")
if receipt_metrics.get("production_restore_performed") is True:
raise ValueError(f"{label}: receipt metrics must not restore production")
def _prometheus_queries(host: str) -> list[str]:
return [
f"awoooi_gitea_full_backup_component_receipt{{host=\"{host}\"}}",
f"awoooi_gitea_full_backup_last_run_failed{{host=\"{host}\"}}",
f"awoooi_gitea_full_backup_restore_drill_performed{{host=\"{host}\"}}",
f"awoooi_gitea_full_backup_secret_value_collected{{host=\"{host}\"}}",
f"awoooi_gitea_full_backup_production_restore_performed{{host=\"{host}\"}}",
f"awoooi_backup_offsite_remote_verify_ok{{host=\"{host}\",provider=\"rclone\"}}",
f"awoooi_backup_offsite_full_verify_fresh{{host=\"{host}\",provider=\"rclone\"}}",
]
def _query_prometheus_vector(prometheus_url: str, query: str) -> list[dict[str, Any]]:
params = urlencode({"query": query})
request = Request(
f"{prometheus_url}/api/v1/query?{params}",
headers={"User-Agent": "awoooi-gitea-full-backup-dr-readback"},
)
with urlopen(request, timeout=PROMETHEUS_TIMEOUT_SECONDS) as response: # noqa: S310
payload = json.loads(response.read().decode("utf-8", errors="replace"))
if payload.get("status") != "success":
raise ValueError("prometheus_status_not_success")
rows = payload.get("data", {}).get("result") or []
results: list[dict[str, Any]] = []
for row in rows:
if not isinstance(row, dict):
continue
labels = dict(row.get("metric") or {})
name = str(labels.pop("__name__", query.split("{", 1)[0]))
value = row.get("value") or [None, "0"]
try:
scalar = float(value[1])
except (IndexError, TypeError, ValueError):
continue
results.append({"name": name, "labels": labels, "value": scalar})
return results
def _receipt_metric_rollup(samples: list[dict[str, Any]], host: str) -> dict[str, Any]:
filtered = [
sample
for sample in samples
if str(_dict(sample.get("labels")).get("host", host)) == host
]
component_receipts = sorted(
{
str(_dict(sample.get("labels")).get("component") or "")
for sample in filtered
if sample.get("name") == "awoooi_gitea_full_backup_component_receipt"
and _float(sample.get("value")) == 1
and _dict(sample.get("labels")).get("component")
}
)
scalar = {
str(sample.get("name")): _float(sample.get("value"))
for sample in filtered
if sample.get("name") != "awoooi_gitea_full_backup_component_receipt"
}
secret_value_collected = (
scalar.get("awoooi_gitea_full_backup_secret_value_collected", 0.0) > 0
)
production_restore_performed = (
scalar.get("awoooi_gitea_full_backup_production_restore_performed", 0.0) > 0
)
return {
"metrics_present": bool(filtered),
"component_receipt_components": component_receipts,
"component_receipt_count": len(component_receipts),
"last_run_failed": scalar.get(
"awoooi_gitea_full_backup_last_run_failed",
0.0,
)
> 0,
"restore_drill_performed": scalar.get(
"awoooi_gitea_full_backup_restore_drill_performed",
0.0,
)
> 0,
"secret_value_collected": secret_value_collected,
"production_restore_performed": production_restore_performed,
"offsite_remote_verify_ok": scalar.get(
"awoooi_backup_offsite_remote_verify_ok",
0.0,
)
== 1,
"offsite_full_verify_fresh": scalar.get(
"awoooi_backup_offsite_full_verify_fresh",
0.0,
)
== 1,
}
def _count_status(rows: list[dict[str, Any]], status: str) -> int:
return sum(1 for row in rows if row.get("status") == status)
def _find(rows: list[dict[str, Any]], key: str, value: str) -> dict[str, Any]:
for row in rows:
if row.get(key) == value:
return row
return {}
def _rows(value: Any) -> list[dict[str, Any]]:
return [row for row in value if isinstance(row, dict)] if isinstance(value, list) else []
def _dict(value: Any) -> dict[str, Any]:
return value if isinstance(value, dict) else {}
def _strings(value: Any) -> list[str]:
return [str(item) for item in value] if isinstance(value, list) else []
def _int(value: Any) -> int:
try:
return int(value)
except (TypeError, ValueError):
return 0
def _float(value: Any) -> float:
try:
return float(value)
except (TypeError, ValueError):
return 0.0
def _read_text(path: Path) -> str:
try:
return path.read_text(encoding="utf-8", errors="replace")
except OSError:
return ""

View File

@@ -0,0 +1,49 @@
from __future__ import annotations
from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.api.v1.agents import router
from src.services.gitea_actions_secret_variable_name_inventory import (
load_latest_gitea_actions_secret_variable_name_inventory,
)
def test_gitea_actions_secret_variable_name_inventory_loader_is_value_safe() -> None:
payload = load_latest_gitea_actions_secret_variable_name_inventory()
assert payload["schema_version"] == "gitea_actions_secret_variable_name_inventory_v1"
assert payload["status"] == "gitea_actions_secret_variable_name_inventory_ready"
assert payload["ready"] is True
assert payload["summary"]["workflow_file_count"] == 12
assert payload["summary"]["referenced_secret_name_count"] == 26
assert payload["summary"]["referenced_variable_name_count"] == 0
assert payload["summary"]["secret_values_collected"] is False
assert payload["summary"]["variable_values_collected"] is False
assert payload["operation_boundaries"]["secret_value_read_allowed"] is False
assert payload["operation_boundaries"]["variable_value_read_allowed"] is False
assert payload["operation_boundaries"]["gitea_api_write_allowed"] is False
assert payload["operation_boundaries"]["workflow_trigger_allowed"] is False
names = {row["name"] for row in payload["secret_name_rows"]}
assert "TELEGRAM_BOT_TOKEN" in names
assert "AWOOOI_GITEA_API_TOKEN" in names
assert "DATABASE_URL" in names
assert all(row["value_read_status"] == "not_read_by_design" for row in payload["secret_name_rows"])
assert payload["metadata_verification_next_step"]["status"] == (
"redacted_configuration_metadata_required"
)
def test_gitea_actions_secret_variable_name_inventory_endpoint_returns_payload() -> None:
app = FastAPI()
app.include_router(router, prefix="/api/v1")
client = TestClient(app)
response = client.get("/api/v1/agents/gitea-actions-secret-variable-name-inventory")
assert response.status_code == 200
data = response.json()
assert data["schema_version"] == "gitea_actions_secret_variable_name_inventory_v1"
assert data["summary"]["referenced_secret_name_count"] == len(data["secret_name_rows"])
assert data["summary"]["secret_values_collected"] is False

View File

@@ -0,0 +1,64 @@
from __future__ import annotations
from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.api.v1.agents import router
from src.services.gitea_branch_protection_required_checks_readback import (
load_latest_gitea_branch_protection_required_checks_readback,
)
def test_gitea_branch_protection_required_checks_loader_builds_source_policy() -> None:
payload = load_latest_gitea_branch_protection_required_checks_readback()
assert (
payload["schema_version"]
== "gitea_branch_protection_required_checks_readback_v1"
)
assert payload["status"] == (
"gitea_branch_protection_required_checks_source_policy_ready"
)
assert payload["ready"] is True
assert payload["summary"]["expected_product_count"] == 12
assert payload["summary"]["protected_repo_count"] == 12
assert payload["summary"]["branch_policy_row_count"] == 24
assert payload["summary"]["main_branch_policy_count"] == 12
assert payload["summary"]["dev_branch_policy_count"] == 12
assert payload["summary"]["missing_required_check_source_count"] == 0
assert payload["summary"]["live_branch_protection_verified"] is False
assert payload["summary"]["live_required_checks_verified"] is False
assert payload["operation_boundaries"]["gitea_branch_protection_write_allowed"] is False
assert payload["operation_boundaries"]["gitea_required_check_write_allowed"] is False
assert payload["operation_boundaries"]["workflow_trigger_allowed"] is False
assert payload["operation_boundaries"]["secret_value_read_allowed"] is False
assert payload["required_check_policy"]["main_branch"]["required_workflow_ids"] == [
"code-review",
"cd",
"type-sync-check",
]
assert payload["required_check_policy"]["dev_branch"]["required_workflow_ids"] == [
"cd-dev",
"type-sync-check",
]
assert "run-migration" in payload["required_check_policy"][
"excluded_manual_or_high_risk_workflows"
]
def test_gitea_branch_protection_required_checks_endpoint_returns_payload() -> None:
app = FastAPI()
app.include_router(router, prefix="/api/v1")
client = TestClient(app)
response = client.get(
"/api/v1/agents/gitea-branch-protection-required-checks-readback"
)
assert response.status_code == 200
data = response.json()
assert data["schema_version"] == (
"gitea_branch_protection_required_checks_readback_v1"
)
assert data["summary"]["branch_policy_row_count"] == len(data["protection_rows"])

View File

@@ -0,0 +1,239 @@
from __future__ import annotations
import json
import os
from pathlib import Path
from fastapi import FastAPI
from fastapi.testclient import TestClient
os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test")
from src.api.v1 import agents
from src.api.v1.agents import router
from src.services.backup_dr_readiness_matrix import (
load_latest_backup_dr_readiness_matrix,
)
from src.services.backup_notification_policy import (
load_latest_backup_notification_policy,
)
from src.services.gitea_capability_ci_cd_optimization_readback import (
build_gitea_capability_ci_cd_optimization_readback,
build_gitea_workflow_source_inventory,
)
from src.services.gitea_private_inventory_p0_scorecard import (
load_latest_gitea_private_inventory_p0_scorecard,
)
from src.services.gitea_repo_bundle_backup_readback import (
build_gitea_repo_bundle_backup_readback,
)
from src.services.gitea_workflow_runner_health import (
load_latest_gitea_workflow_runner_health,
)
from src.services.p0_cicd_baseline_source_readiness import (
load_latest_p0_cicd_baseline_source_readiness,
)
_REPO_ROOT = Path(__file__).resolve().parents[3]
_DEV_PROD_SNAPSHOT = (
_REPO_ROOT
/ "docs"
/ "operations"
/ "gitea-all-product-dev-prod-repo-readback.snapshot.json"
)
def _sample(name: str, value: float, **labels: str) -> dict:
merged = {"host": "188", **labels}
return {"name": name, "labels": merged, "value": value}
def _repo_refs() -> list[str]:
data = json.loads(_DEV_PROD_SNAPSHOT.read_text(encoding="utf-8"))
return sorted(row["gitea_repo"] for row in data["products"])
def _repo_bundle_payload() -> dict:
refs = _repo_refs()
samples = [
_sample("awoooi_gitea_bundle_expected_repo_count", len(refs)),
_sample("awoooi_gitea_bundle_expected_repo_missing_count", 0),
_sample("awoooi_gitea_bundle_failed_repo_count", 0),
_sample("awoooi_gitea_bundle_checksum_missing_count", 0),
_sample("awoooi_gitea_bundle_age_seconds", 600),
_sample("awoooi_gitea_bundle_fresh", 1),
_sample("awoooi_gitea_bundle_all_expected_ok", 1),
_sample("awoooi_gitea_bundle_sample_restore_dry_run_ok", 1),
]
for repo in refs:
samples.extend(
[
_sample("awoooi_gitea_bundle_repo_present", 1, repo=repo),
_sample("awoooi_gitea_bundle_repo_ok", 1, repo=repo),
_sample("awoooi_gitea_bundle_repo_head_count", 2, repo=repo),
_sample(
"awoooi_gitea_bundle_checksum_digest_present",
1,
repo=repo,
),
]
)
return build_gitea_repo_bundle_backup_readback(
metric_samples=samples,
generated_at="2026-07-09T23:00:00+08:00",
dev_prod_repo_refs=refs,
dev_prod_repo_truth_ref=(
"docs/operations/gitea-all-product-dev-prod-repo-readback.snapshot.json"
),
)
def _full_server_dr_payload() -> dict:
return {
"schema_version": "gitea_full_server_backup_dr_receipts_v1",
"status": "gitea_full_server_backup_dr_receipts_gaps_present",
"ready": False,
"active_gap_ids": ["restore_drill_approval_gate"],
"active_gap_count": 1,
"summary": {
"source_ready_component_count": 6,
"verified_ready_component_count": 3,
"gitea_full_backup_component_receipt_count": 0,
},
}
def _readback_payload() -> dict:
return build_gitea_capability_ci_cd_optimization_readback(
dev_prod_repo_readback=json.loads(
_DEV_PROD_SNAPSHOT.read_text(encoding="utf-8")
),
private_inventory=load_latest_gitea_private_inventory_p0_scorecard(),
workflow_runner_health=load_latest_gitea_workflow_runner_health(),
workflow_source_inventory=build_gitea_workflow_source_inventory(_REPO_ROOT),
cicd_source_readiness=load_latest_p0_cicd_baseline_source_readiness(),
backup_dr_readiness=load_latest_backup_dr_readiness_matrix(),
backup_notification_policy=load_latest_backup_notification_policy(),
repo_bundle_backup_readback=_repo_bundle_payload(),
full_server_backup_dr_receipts=_full_server_dr_payload(),
generated_at="2026-07-09T23:01:00+08:00",
)
def test_gitea_capability_readback_rolls_up_complete_feature_surface() -> None:
payload = _readback_payload()
assert (
payload["schema_version"]
== "gitea_capability_ci_cd_optimization_readback_v1"
)
assert payload["source_control_authority"] == "gitea"
assert payload["status"] == "gitea_capability_ci_cd_optimization_gaps_present"
assert payload["rollups"]["expected_product_count"] == 12
assert payload["rollups"]["ssh_verified_product_repo_count"] == 12
assert payload["rollups"]["dev_prod_environment_split_ready_count"] == 12
assert payload["workflow_source_inventory"]["workflow_file_count"] == 12
assert payload["workflow_source_inventory"]["secret_variable_rollups"][
"secret_values_collected"
] is False
assert payload["workflow_source_inventory"]["secret_variable_rollups"][
"secret_reference_count"
] > 0
assert (
len(
payload["workflow_source_inventory"]["secret_variable_rollups"][
"referenced_secret_names"
]
)
== 26
)
capability_by_id = {row["id"]: row for row in payload["capability_rows"]}
assert capability_by_id["code_git_source_control"]["status"] == "verified_ready"
assert (
capability_by_id["dev_prod_environment_split"]["status"]
== "verified_ready"
)
assert capability_by_id["gitea_actions_workflows"]["status"] == "verified_ready"
assert capability_by_id["gitea_actions_workflows"]["metrics"] == {
"workflow_source_file_count": 12,
"workflow_contract_count": 12,
}
assert capability_by_id["gitea_actions_runners"]["status"] == "verified_ready"
assert capability_by_id["branch_protection_required_checks"]["status"] == "source_ready"
assert capability_by_id["backup_dr_restore"]["status"] == "source_ready"
assert capability_by_id["backup_dr_restore"]["metrics"][
"gitea_full_dr_source_ready_component_count"
] == 6
assert capability_by_id["observability_alert_receipts"]["status"] == "action_required"
assert payload["operation_boundaries"]["read_only_api_allowed"] is True
assert payload["operation_boundaries"]["github_api_allowed"] is False
assert payload["operation_boundaries"]["secret_value_read_allowed"] is False
assert payload["operation_boundaries"]["workflow_trigger_allowed"] is False
def test_gitea_capability_readback_prioritizes_professional_cicd_gaps() -> None:
payload = _readback_payload()
optimization_by_id = {
row["id"]: row for row in payload["ci_cd_optimization_rows"]
}
assert (
optimization_by_id["gitea_actions_source_authority"]["status"]
== "implemented"
)
assert (
optimization_by_id["dev_prod_branch_promotion_contract"]["status"]
== "implemented"
)
assert (
optimization_by_id["workflow_contract_freshness"]["status"]
== "implemented"
)
assert (
optimization_by_id["runner_label_isolation_attestation"]["status"]
== "implemented"
)
assert (
optimization_by_id["dedupe_cancel_stale_run_guard"]["status"]
== "source_ready"
)
assert (
optimization_by_id["branch_protection_required_checks"]["status"]
== "source_ready"
)
assert (
optimization_by_id["full_gitea_server_offsite_dr_receipts"]["status"]
== "source_ready"
)
assert payload["rollups"]["ci_cd_optimization_count"] == len(
payload["ci_cd_optimization_rows"]
)
assert payload["rollups"]["p0_optimization_gap_count"] == 0
assert payload["active_p0_gap_ids"] == ["observability_alert_receipts"]
assert any(
action.startswith("observability_alert_receipts:")
for action in payload["readback"]["safe_next_actions"]
)
def test_gitea_capability_readback_endpoint_returns_payload(monkeypatch) -> None:
monkeypatch.setattr(
agents,
"load_latest_gitea_capability_ci_cd_optimization_readback",
_readback_payload,
)
app = FastAPI()
app.include_router(router, prefix="/api/v1")
client = TestClient(app)
response = client.get(
"/api/v1/agents/gitea-capability-ci-cd-optimization-readback"
)
assert response.status_code == 200
data = response.json()
assert data["schema_version"] == "gitea_capability_ci_cd_optimization_readback_v1"
assert data["rollups"]["capability_count"] == len(data["capability_rows"])
assert data["rollups"]["workflow_contract_fresh"] is True

View File

@@ -0,0 +1,98 @@
from __future__ import annotations
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.api.v1 import agents
from src.api.v1.agents import router
from src.services.backup_notification_policy import (
load_latest_backup_notification_policy,
)
from src.services.gitea_cicd_alert_receipt_chain_readback import (
build_gitea_cicd_alert_receipt_chain_readback,
)
from src.services.gitea_workflow_runner_health import (
load_latest_gitea_workflow_runner_health,
)
def _telegram_coverage() -> dict:
return {
"schema_version": "telegram_alert_monitoring_coverage_readback_v1",
"status": "blocked_telegram_alert_monitoring_coverage_gaps_present",
"summary": {
"monitoring_live_receipt_gap_count": 60,
"alert_receipt_pipeline_ready_count": 6,
"alert_receipt_pipeline_stage_count": 7,
"verified_ai_agent_context_receipt_count": 6,
},
}
def _full_dr() -> dict:
return {
"schema_version": "gitea_full_server_backup_dr_receipts_v1",
"status": "gitea_full_server_backup_dr_receipts_gaps_present",
"active_gap_count": 7,
"summary": {
"verified_ready_component_count": 3,
},
}
def _payload() -> dict:
return build_gitea_cicd_alert_receipt_chain_readback(
telegram_coverage=_telegram_coverage(),
backup_notification_policy=load_latest_backup_notification_policy(),
workflow_runner_health=load_latest_gitea_workflow_runner_health(),
full_server_backup_dr_receipts=_full_dr(),
generated_at="2026-07-10T00:20:00+08:00",
)
def test_gitea_cicd_alert_receipt_chain_readback_surfaces_receipt_gaps() -> None:
payload = _payload()
assert payload["schema_version"] == "gitea_cicd_alert_receipt_chain_readback_v1"
assert payload["status"] == "gitea_cicd_alert_receipt_chain_gaps_present"
assert payload["ready"] is False
assert payload["summary"]["receipt_chain_count"] == 5
assert payload["summary"]["action_required_chain_count"] == 2
assert payload["summary"]["monitoring_live_receipt_gap_count"] == 60
assert payload["summary"]["full_server_dr_active_gap_count"] == 7
assert payload["operation_boundaries"]["telegram_send_allowed"] is False
assert payload["operation_boundaries"]["bot_api_call_allowed"] is False
assert payload["operation_boundaries"]["backup_execution_allowed"] is False
assert payload["operation_boundaries"]["restore_execution_allowed"] is False
assert payload["operation_boundaries"]["workflow_trigger_allowed"] is False
assert payload["operation_boundaries"]["secret_value_read_allowed"] is False
by_id = {row["chain_id"]: row for row in payload["receipt_chain_rows"]}
assert by_id["gitea_cd_pipeline_notifications"]["status"] == "source_ready"
assert by_id["backup_notification_policy"]["status"] == "policy_ready"
assert by_id["gitea_full_server_dr_receipts"]["status"] == "action_required"
assert by_id["telegram_monitoring_live_receipts"]["status"] == "action_required"
assert by_id["ai_agent_context_receipts"]["status"] == "source_ready"
@pytest.mark.asyncio
async def test_gitea_cicd_alert_receipt_chain_endpoint_returns_payload(monkeypatch) -> None:
async def fake_loader() -> dict:
return _payload()
monkeypatch.setattr(
agents,
"load_latest_gitea_cicd_alert_receipt_chain_readback",
fake_loader,
)
app = FastAPI()
app.include_router(router, prefix="/api/v1")
client = TestClient(app)
response = client.get("/api/v1/agents/gitea-cicd-alert-receipt-chain-readback")
assert response.status_code == 200
data = response.json()
assert data["schema_version"] == "gitea_cicd_alert_receipt_chain_readback_v1"
assert data["active_gap_count"] == len(data["active_gap_ids"])

View File

@@ -0,0 +1,173 @@
from __future__ import annotations
from pathlib import Path
from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.api.v1 import agents
from src.api.v1.agents import router
from src.services.backup_dr_readiness_matrix import (
load_latest_backup_dr_readiness_matrix,
)
from src.services.backup_dr_target_inventory import (
load_latest_backup_dr_target_inventory,
)
from src.services.gitea_full_server_backup_dr_receipts import (
build_gitea_full_server_backup_source_contract,
build_gitea_full_server_backup_dr_receipts,
)
from src.services.gitea_repo_bundle_backup_readback import (
build_gitea_repo_bundle_backup_readback,
)
def _sample(name: str, value: float, **labels: str) -> dict:
merged = {"host": "188", **labels}
return {"name": name, "labels": merged, "value": value}
def _repo_bundle_payload() -> dict:
repos = ["wooo/awoooi", "wooo/ewoooc"]
samples = [
_sample("awoooi_gitea_bundle_expected_repo_count", len(repos)),
_sample("awoooi_gitea_bundle_expected_repo_missing_count", 0),
_sample("awoooi_gitea_bundle_failed_repo_count", 0),
_sample("awoooi_gitea_bundle_checksum_missing_count", 0),
_sample("awoooi_gitea_bundle_age_seconds", 600),
_sample("awoooi_gitea_bundle_fresh", 1),
_sample("awoooi_gitea_bundle_all_expected_ok", 1),
_sample("awoooi_gitea_bundle_sample_restore_dry_run_ok", 1),
]
for repo in repos:
samples.extend(
[
_sample("awoooi_gitea_bundle_repo_present", 1, repo=repo),
_sample("awoooi_gitea_bundle_repo_ok", 1, repo=repo),
_sample("awoooi_gitea_bundle_repo_head_count", 2, repo=repo),
_sample("awoooi_gitea_bundle_checksum_digest_present", 1, repo=repo),
]
)
return build_gitea_repo_bundle_backup_readback(
metric_samples=samples,
generated_at="2026-07-10T00:00:00+08:00",
)
def _payload() -> dict:
repo_root = Path(__file__).resolve().parents[3]
return build_gitea_full_server_backup_dr_receipts(
repo_bundle_backup_readback=_repo_bundle_payload(),
backup_dr_readiness_matrix=load_latest_backup_dr_readiness_matrix(),
backup_dr_target_inventory=load_latest_backup_dr_target_inventory(),
source_contract=build_gitea_full_server_backup_source_contract(repo_root),
generated_at="2026-07-10T00:01:00+08:00",
)
def _full_backup_metric_samples() -> list[dict]:
def sample(name: str, value: float, **labels: str) -> dict:
return {"name": name, "labels": {"host": "110", **labels}, "value": value}
samples = [
sample("awoooi_gitea_full_backup_last_run_failed", 0, service="gitea"),
sample(
"awoooi_gitea_full_backup_restore_drill_performed",
0,
service="gitea",
),
sample(
"awoooi_gitea_full_backup_secret_value_collected",
0,
service="gitea",
),
sample(
"awoooi_gitea_full_backup_production_restore_performed",
0,
service="gitea",
),
]
for component in [
"database",
"app_settings",
"lfs_objects",
"package_registry",
"attachments",
"hooks_custom_assets",
]:
samples.append(
sample(
"awoooi_gitea_full_backup_component_receipt",
1,
service="gitea",
component=component,
receipt="gitea_dump_restic",
)
)
return samples
def test_gitea_full_server_backup_dr_receipts_separates_bundle_from_full_dr() -> None:
payload = _payload()
assert payload["schema_version"] == "gitea_full_server_backup_dr_receipts_v1"
assert payload["status"] == "gitea_full_server_backup_dr_receipts_gaps_present"
assert payload["ready"] is False
assert payload["summary"]["repo_bundle_ready"] is True
assert payload["summary"]["component_count"] == 10
assert payload["summary"]["verified_ready_component_count"] == 3
assert payload["summary"]["source_ready_component_count"] == 6
assert payload["summary"]["receipt_required_component_count"] == 0
assert payload["summary"]["approval_required_component_count"] == 1
assert payload["active_gap_ids"] == ["restore_drill_approval_gate"]
assert payload["operation_boundaries"]["backup_execution_allowed"] is False
assert payload["operation_boundaries"]["restore_execution_allowed"] is False
assert payload["operation_boundaries"]["offsite_sync_execution_allowed"] is False
assert payload["operation_boundaries"]["secret_value_read_allowed"] is False
by_id = {row["component_id"]: row for row in payload["component_rows"]}
assert by_id["repository_bundles"]["status"] == "verified_ready"
assert by_id["gitea_server_backup_target"]["status"] == "verified_ready"
assert by_id["google_drive_offsite_mirror"]["status"] == "verified_ready"
assert by_id["restore_drill_approval_gate"]["status"] == "approval_required"
assert by_id["gitea_database_receipt"]["status"] == "source_ready"
assert by_id["gitea_packages_receipt"]["status"] == "source_ready"
assert by_id["gitea_attachments_receipt"]["production_restore_performed"] is False
assert payload["source_contract"]["component_metric_writer_source_ready"] is True
assert payload["source_contract"]["secret_values_collected_by_source_inspection"] is False
def test_gitea_full_server_backup_dr_receipts_promotes_live_component_metrics() -> None:
repo_root = Path(__file__).resolve().parents[3]
payload = build_gitea_full_server_backup_dr_receipts(
repo_bundle_backup_readback=_repo_bundle_payload(),
backup_dr_readiness_matrix=load_latest_backup_dr_readiness_matrix(),
backup_dr_target_inventory=load_latest_backup_dr_target_inventory(),
source_contract=build_gitea_full_server_backup_source_contract(repo_root),
metric_samples=_full_backup_metric_samples(),
generated_at="2026-07-10T00:02:00+08:00",
)
assert payload["summary"]["verified_ready_component_count"] == 9
assert payload["summary"]["source_ready_component_count"] == 0
assert payload["summary"]["gitea_full_backup_component_receipt_count"] == 6
assert payload["receipt_metric_rollup"]["secret_value_collected"] is False
assert payload["receipt_metric_rollup"]["production_restore_performed"] is False
def test_gitea_full_server_backup_dr_receipts_endpoint_returns_payload(monkeypatch) -> None:
monkeypatch.setattr(
agents,
"load_latest_gitea_full_server_backup_dr_receipts",
_payload,
)
app = FastAPI()
app.include_router(router, prefix="/api/v1")
client = TestClient(app)
response = client.get("/api/v1/agents/gitea-full-server-backup-dr-receipts")
assert response.status_code == 200
data = response.json()
assert data["schema_version"] == "gitea_full_server_backup_dr_receipts_v1"
assert data["active_gap_count"] == len(data["active_gap_ids"])

View File

@@ -20,23 +20,12 @@ def test_gitea_workflow_runner_health_endpoint_returns_committed_snapshot():
assert data["program_status"]["current_task_id"] == "P1-002"
assert data["program_status"]["next_task_id"] == "P1-003"
assert data["program_status"]["read_only_mode"] is True
assert data["rollups"]["total_workflows"] == len(data["workflow_records"]) == 9
assert data["rollups"]["workflows_with_schedule"] == 2
assert data["rollups"]["workflows_with_workflow_dispatch"] == 7
assert data["rollups"]["workflow_ids_requiring_runner_attestation"] == [
"agent_market_watch",
"ansible_lint",
"cd_dev",
"code_review",
"deploy_alerts",
"e2e_health",
"run_migration",
"type_sync_check",
]
assert data["rollups"]["runner_contracts_requiring_action"] == [
"ubuntu_latest_gitea_runner_label"
]
assert data["rollups"]["notification_contracts_quiet_success_count"] == 2
assert data["rollups"]["total_workflows"] == len(data["workflow_records"]) == 12
assert data["rollups"]["workflows_with_schedule"] == 4
assert data["rollups"]["workflows_with_workflow_dispatch"] == 12
assert data["rollups"]["workflow_ids_requiring_runner_attestation"] == []
assert data["rollups"]["runner_contracts_requiring_action"] == []
assert data["rollups"]["notification_contracts_quiet_success_count"] == 3
assert data["operation_boundaries"]["read_only_api_allowed"] is True
assert data["operation_boundaries"]["workflow_modification_allowed"] is False
assert data["operation_boundaries"]["runner_restart_allowed"] is False
@@ -46,9 +35,18 @@ def test_gitea_workflow_runner_health_endpoint_returns_committed_snapshot():
assert data["approval_boundaries"]["runner_mutation_authorized"] is False
assert data["approval_boundaries"]["migration_trigger_authorized"] is False
cd_pipeline = next(row for row in data["workflow_records"] if row["workflow_id"] == "cd_pipeline")
assert cd_pipeline["runner_evidence_status"] == "host_runner_mapped"
assert cd_pipeline["job_count"] == 3
assert cd_pipeline["runner_evidence_status"] == "non110_host_runner_mapped"
assert cd_pipeline["runner_labels"] == ["awoooi-non110-host"]
assert cd_pipeline["job_count"] == 5
e2e_health = next(row for row in data["workflow_records"] if row["workflow_id"] == "e2e_health")
assert e2e_health["notification_policy"] == "failure_only"
ai_technology_watch = next(
row for row in data["workflow_records"] if row["workflow_id"] == "ai_technology_watch"
)
assert ai_technology_watch["runner_labels"] == ["awoooi-non110-ubuntu"]
harbor_repair = next(
row for row in data["workflow_records"] if row["workflow_id"] == "harbor_110_local_repair"
)
assert harbor_repair["runner_evidence_status"] == "non110_host_runner_mapped"
assert "workflow 修改批准" in data["operator_contract"]["must_not_interpret_as"]
assert "Secret 已讀取或可輸出" in data["operator_contract"]["must_not_interpret_as"]

View File

@@ -1,6 +1,6 @@
{
"schema_version": "gitea_workflow_runner_health_v1",
"generated_at": "2026-06-05T10:56:16+08:00",
"generated_at": "2026-07-09T23:20:00+08:00",
"program_status": {
"overall_completion_percent": 100,
"current_priority": "P1",
@@ -11,12 +11,15 @@
"source_refs": [
"docs/schemas/gitea_workflow_runner_health_v1.schema.json",
".gitea/workflows/agent-market-watch.yaml",
".gitea/workflows/ai-technology-watch.yaml",
".gitea/workflows/ansible-lint.yml",
".gitea/workflows/awoooi-onboarding-warning-step.yaml",
".gitea/workflows/cd-dev.yaml",
".gitea/workflows/cd.yaml",
".gitea/workflows/code-review.yaml",
".gitea/workflows/deploy-alerts.yaml",
".gitea/workflows/e2e-health.yaml",
".gitea/workflows/harbor-110-local-repair.yaml",
".gitea/workflows/run-migration.yml",
".gitea/workflows/type-sync-check.yaml",
"scripts/ci/check-gitea-step-env-secrets.js",
@@ -27,37 +30,26 @@
"scripts/ops/stop-stale-gitea-actions-jobs.sh"
],
"rollups": {
"total_workflows": 9,
"total_workflows": 12,
"by_workflow_status": {
"manifest_mapped": 9
"manifest_mapped": 12
},
"by_runner_evidence_status": {
"owner_attestation_required": 7,
"host_runner_mapped": 1,
"comment_ambiguous": 1
"non110_runner_mapped": 9,
"non110_host_runner_mapped": 3
},
"workflows_with_schedule": 2,
"workflows_with_workflow_dispatch": 7,
"workflows_with_schedule": 4,
"workflows_with_workflow_dispatch": 12,
"workflows_with_notify_bridge": 6,
"workflows_with_actionable_or_failure_quiet_policy": 2,
"workflow_ids_requiring_runner_attestation": [
"agent_market_watch",
"ansible_lint",
"cd_dev",
"code_review",
"deploy_alerts",
"e2e_health",
"run_migration",
"type_sync_check"
],
"total_runner_contracts": 4,
"runner_contracts_requiring_action": [
"ubuntu_latest_gitea_runner_label"
],
"notification_contracts_total": 6,
"notification_contracts_quiet_success_count": 2,
"workflows_with_actionable_or_failure_quiet_policy": 3,
"workflow_ids_requiring_runner_attestation": [],
"total_runner_contracts": 5,
"runner_contracts_requiring_action": [],
"notification_contracts_total": 7,
"notification_contracts_quiet_success_count": 3,
"notification_contracts_quiet_success_ids": [
"agent_market_watch_actionable_only",
"ai_technology_watch_actionable_only",
"e2e_health_failure_only"
]
},
@@ -75,9 +67,9 @@
],
"schedule_cadence": "每週一 09:00 Asia/Taipeicron=0 1 * * 1 UTC",
"runner_labels": [
"ubuntu-latest"
"awoooi-non110-ubuntu"
],
"runner_evidence_status": "owner_attestation_required",
"runner_evidence_status": "non110_runner_mapped",
"job_count": 1,
"notification_policy": "actionable_only_no_success_noise",
"notify_bridge_calls": 0,
@@ -86,23 +78,48 @@
".gitea/workflows/agent-market-watch.yaml",
"docs/evaluations/agent_market_watch_report_2026-06-04_watch_expanded.json"
],
"next_action": "保留 actionable-only下一步只補 runner label owner attestation,不啟用額外通知或 paid API。"
"next_action": "保留 actionable-only維持 non110 runner label不啟用額外通知或 paid API。"
},
{
"workflow_id": "ai_technology_watch",
"file_ref": ".gitea/workflows/ai-technology-watch.yaml",
"display_name": "AI Technology Watch",
"scope": "AI 技術雷達監控;只產生 watch/readback不切換 provider、不進 paid API 或 production route。",
"status": "manifest_mapped",
"risk_level": "medium",
"triggers": [
"workflow_dispatch",
"schedule"
],
"schedule_cadence": "定期技術雷達觀察;由 workflow source 定義 cron。",
"runner_labels": [
"awoooi-non110-ubuntu"
],
"runner_evidence_status": "non110_runner_mapped",
"job_count": 1,
"notification_policy": "actionable_only_no_success_noise",
"notify_bridge_calls": 0,
"secrets_policy_status": "不讀 Secret payload只保留 committed workflow metadata 與治理 flags。",
"evidence_refs": [
".gitea/workflows/ai-technology-watch.yaml"
],
"next_action": "保留 actionable-only後續只補技術雷達 readback 與報告 freshness。"
},
{
"workflow_id": "ansible_lint",
"file_ref": ".gitea/workflows/ansible-lint.yml",
"display_name": "Ansible Lint",
"scope": "Ansible 檔案 lint;無通知橋接。",
"display_name": "Ansible / Reboot Recovery Contract",
"scope": "Ansible / reboot recovery contract 驗證;無通知橋接。",
"status": "manifest_mapped",
"risk_level": "low",
"triggers": [
"push"
"workflow_dispatch"
],
"schedule_cadence": "無定期排程",
"runner_labels": [
"ubuntu-latest"
"awoooi-non110-ubuntu"
],
"runner_evidence_status": "owner_attestation_required",
"runner_evidence_status": "non110_runner_mapped",
"job_count": 1,
"notification_policy": "read_only_no_notify",
"notify_bridge_calls": 0,
@@ -110,7 +127,32 @@
"evidence_refs": [
".gitea/workflows/ansible-lint.yml"
],
"next_action": "補 ubuntu-latest 在 Gitea Actions 的 owner attestation不改 workflow label。"
"next_action": "維持 non110 runner label 與 no-notify 合約。"
},
{
"workflow_id": "awoooi_onboarding_warning_step",
"file_ref": ".gitea/workflows/awoooi-onboarding-warning-step.yaml",
"display_name": "AWOOOI Onboarding Warning Step",
"scope": "受控 onboarding warning-step只允許 workflow_dispatch不支援 push/pull_request 自動觸發。",
"status": "manifest_mapped",
"risk_level": "medium",
"triggers": [
"workflow_dispatch"
],
"schedule_cadence": "無定期排程",
"runner_labels": [
"awoooi-non110-host"
],
"runner_evidence_status": "non110_host_runner_mapped",
"job_count": 2,
"notification_policy": "manual_status_exception",
"notify_bridge_calls": 0,
"secrets_policy_status": "不讀 Secret payload此 workflow 用於 shape/guard validation。",
"evidence_refs": [
".gitea/workflows/awoooi-onboarding-warning-step.yaml",
"docs/operations/awoooi-gitea-onboarding-warning-step-template-copy-receipt.snapshot.json"
],
"next_action": "維持 controlled workflow_dispatch不得改成 push/pull_request 觸發。"
},
{
"workflow_id": "cd_dev",
@@ -120,14 +162,13 @@
"status": "manifest_mapped",
"risk_level": "high",
"triggers": [
"push:dev",
"workflow_dispatch"
],
"schedule_cadence": "無定期排程",
"runner_labels": [
"ubuntu-latest"
"awoooi-non110-ubuntu"
],
"runner_evidence_status": "owner_attestation_required",
"runner_evidence_status": "non110_runner_mapped",
"job_count": 1,
"notification_policy": "deployment_status_exception",
"notify_bridge_calls": 3,
@@ -136,13 +177,13 @@
".gitea/workflows/cd-dev.yaml",
"scripts/ci/notify-awoooi-cicd.sh"
],
"next_action": "保留部署狀態例外;補 runner owner attestation 與通知降噪邊界,不直接改 dev CD。"
"next_action": "保留部署狀態例外;後續補 dev deploy marker / smoke receipt 統一讀回。"
},
{
"workflow_id": "cd_pipeline",
"file_ref": ".gitea/workflows/cd.yaml",
"display_name": "CD Pipeline",
"scope": "main branch code-review 後的正式測試、build、deploy、post-deploy使用 awoooi-host runner。",
"scope": "main branch 正式測試、build、deploy、post-deploy使用 non110 host runner label並保留 stale-run guard。",
"status": "manifest_mapped",
"risk_level": "critical",
"triggers": [
@@ -151,10 +192,10 @@
],
"schedule_cadence": "無定期排程",
"runner_labels": [
"awoooi-host"
"awoooi-non110-host"
],
"runner_evidence_status": "host_runner_mapped",
"job_count": 3,
"runner_evidence_status": "non110_host_runner_mapped",
"job_count": 5,
"notification_policy": "deployment_status_exception",
"notify_bridge_calls": 9,
"secrets_policy_status": "正式 CD 使用 check-gitea-step-env-secrets guard不在本 snapshot 讀取任何 Secret payload。",
@@ -164,7 +205,7 @@
"scripts/ci/cleanup-host-runner-workspace.sh",
"scripts/ci/wait-host-web-build-pressure.sh"
],
"next_action": "保留 awoooi-host 合約與 post-deploy smoke任何 CD 修改仍需獨立 review / deploy gate。"
"next_action": "保留 non110 host 合約與 post-deploy smoke任何 CD 修改仍需獨立 review / deploy gate。"
},
{
"workflow_id": "code_review",
@@ -174,40 +215,38 @@
"status": "manifest_mapped",
"risk_level": "high",
"triggers": [
"push",
"workflow_dispatch"
],
"schedule_cadence": "無定期排程",
"runner_labels": [
"ubuntu-latest"
"awoooi-non110-ubuntu"
],
"runner_evidence_status": "owner_attestation_required",
"runner_evidence_status": "non110_runner_mapped",
"job_count": 1,
"notification_policy": "manual_status_exception",
"notify_bridge_calls": 2,
"secrets_policy_status": "不讀 Secret payload仍需 owner attestation 證明 runner 與通知 secret name parity。",
"secrets_policy_status": "不讀 Secret payload仍需維持通知 secret name parity 與 guard。",
"evidence_refs": [
".gitea/workflows/code-review.yaml",
"scripts/ci/notify-awoooi-cicd.sh"
],
"next_action": "保留 review 狀態通知;補 runner label 與 secret-name hygiene 證據。"
"next_action": "保留 review 狀態通知;後續與 PR required check policy 綁定。"
},
{
"workflow_id": "deploy_alerts",
"file_ref": ".gitea/workflows/deploy-alerts.yaml",
"display_name": "Deploy Alert Rules",
"scope": "告警規則部署流程;屬人工/部署狀態例外,不在 P1-002 變更 alert rule。",
"scope": "告警規則部署流程;屬人工/部署狀態例外,不在本 snapshot 變更 alert rule。",
"status": "manifest_mapped",
"risk_level": "high",
"triggers": [
"workflow_dispatch",
"push"
"workflow_dispatch"
],
"schedule_cadence": "無定期排程",
"runner_labels": [
"ubuntu-latest"
"awoooi-non110-ubuntu"
],
"runner_evidence_status": "owner_attestation_required",
"runner_evidence_status": "non110_runner_mapped",
"job_count": 1,
"notification_policy": "manual_status_exception",
"notify_bridge_calls": 1,
@@ -217,7 +256,7 @@
"scripts/ci/notify-awoooi-cicd.sh",
"docs/HARD_RULES.md"
],
"next_action": "P1-003 再盤點 Alertmanager / Prometheus 合約;P1-002 不改 alert rules。"
"next_action": "後續再盤點 Alertmanager / Prometheus 合約;本 snapshot 不改 alert rules。"
},
{
"workflow_id": "e2e_health",
@@ -232,9 +271,9 @@
],
"schedule_cadence": "每日 00:00 Asia/Taipeicron=0 16 * * * UTC",
"runner_labels": [
"ubuntu-latest"
"awoooi-non110-ubuntu"
],
"runner_evidence_status": "owner_attestation_required",
"runner_evidence_status": "non110_runner_mapped",
"job_count": 1,
"notification_policy": "failure_only",
"notify_bridge_calls": 1,
@@ -243,13 +282,38 @@
".gitea/workflows/e2e-health.yaml",
"scripts/ci/notify-awoooi-cicd.sh"
],
"next_action": "保留 failure-only補 runner owner attestation 與最近 run 證據。"
"next_action": "保留 failure-only最近 run 與 alert receipt stage 證據。"
},
{
"workflow_id": "harbor_110_local_repair",
"file_ref": ".gitea/workflows/harbor-110-local-repair.yaml",
"display_name": "AWOOOI Harbor 110 Local Repair",
"scope": "Harbor 110 local repair guarded workflow低頻 schedule + workflow_dispatch內部仍有 no-auto-apply guard。",
"status": "manifest_mapped",
"risk_level": "critical",
"triggers": [
"workflow_dispatch",
"schedule"
],
"schedule_cadence": "低頻 hourly guard由 workflow source 定義 cron。",
"runner_labels": [
"awoooi-non110-host"
],
"runner_evidence_status": "non110_host_runner_mapped",
"job_count": 2,
"notification_policy": "manual_status_exception",
"notify_bridge_calls": 0,
"secrets_policy_status": "不讀 Secret payloadrepair workflow 只保留 guarded execution source。",
"evidence_refs": [
".gitea/workflows/harbor-110-local-repair.yaml"
],
"next_action": "維持 no-auto-apply repair guard不得由 API 直接觸發 repair。"
},
{
"workflow_id": "run_migration",
"file_ref": ".gitea/workflows/run-migration.yml",
"display_name": "run-migration",
"scope": "手動 migration workflow不得由 P1-002 觸發。",
"scope": "手動 migration workflow不得由本 readback 觸發。",
"status": "manifest_mapped",
"risk_level": "critical",
"triggers": [
@@ -257,18 +321,18 @@
],
"schedule_cadence": "無定期排程",
"runner_labels": [
"ubuntu-latest # 或 self-hosted runner on 110"
"awoooi-non110-ubuntu"
],
"runner_evidence_status": "comment_ambiguous",
"runner_evidence_status": "non110_runner_mapped",
"job_count": 1,
"notification_policy": "manual_status_exception",
"notify_bridge_calls": 1,
"secrets_policy_status": "Migration 相關 secret / DB 權限不得由 P1-002 讀取或擴權。",
"secrets_policy_status": "Migration 相關 secret / DB 權限不得由 readback 讀取或擴權。",
"evidence_refs": [
".gitea/workflows/run-migration.yml",
"scripts/ci/notify-awoooi-cicd.sh"
],
"next_action": "只讀補 runner label owner attestation 與 migration approval boundary不觸發 workflow。"
"next_action": "只讀維持 migration approval boundary不觸發 workflow。"
},
{
"workflow_id": "type_sync_check",
@@ -278,56 +342,60 @@
"status": "manifest_mapped",
"risk_level": "low",
"triggers": [
"push"
"workflow_dispatch"
],
"schedule_cadence": "無定期排程",
"runner_labels": [
"ubuntu-latest"
"awoooi-non110-ubuntu"
],
"runner_evidence_status": "owner_attestation_required",
"runner_evidence_status": "non110_runner_mapped",
"job_count": 1,
"notification_policy": "read_only_no_notify",
"notify_bridge_calls": 0,
"secrets_policy_status": "無通知橋接;仍需 runner label attestation。",
"secrets_policy_status": "無通知橋接;維持 non110 runner label。",
"evidence_refs": [
".gitea/workflows/type-sync-check.yaml"
],
"next_action": "補 runner label owner attestation不改 workflow。"
"next_action": "維持 no-notify後續只補 artifact retention。"
}
],
"runner_contracts": [
{
"contract_id": "awoooi_host_runner",
"display_name": "awoooi-host 正式 CD runner",
"contract_id": "awoooi_non110_host_runner",
"display_name": "awoooi-non110-host controlled runner label",
"status": "manifest_mapped",
"risk_level": "critical",
"runner_labels": [
"awoooi-host"
"awoooi-non110-host"
],
"used_by_workflows": [
"cd_pipeline"
"awoooi_onboarding_warning_step",
"cd_pipeline",
"harbor_110_local_repair"
],
"health_contract": "正式 CD tests/build/post-deploy 均使用 awoooi-hostcleanup 與 build-pressure guard 只做等候 / 清理,不代表可任意重啟 runner",
"health_contract": "host-level controlled workflows use the explicit awoooi-non110-host label; this snapshot does not restart or mutate the runner.",
"guardrail_refs": [
"scripts/ci/cleanup-host-runner-workspace.sh",
"scripts/ci/wait-host-web-build-pressure.sh"
],
"evidence_refs": [
".gitea/workflows/cd.yaml",
"docs/LOGBOOK.md"
".gitea/workflows/harbor-110-local-repair.yaml",
".gitea/workflows/awoooi-onboarding-warning-step.yaml"
],
"next_action": "維持正式 CD runner 合約;任何 systemd / runner 變更另走人工批准。"
"next_action": "維持 explicit non110 host label任何 runner/service 變更另走維護窗口。"
},
{
"contract_id": "ubuntu_latest_gitea_runner_label",
"display_name": "ubuntu-latest Gitea runner label 對應",
"status": "action_required",
"contract_id": "awoooi_non110_ubuntu_runner",
"display_name": "awoooi-non110-ubuntu controlled runner label",
"status": "manifest_mapped",
"risk_level": "high",
"runner_labels": [
"ubuntu-latest"
"awoooi-non110-ubuntu"
],
"used_by_workflows": [
"agent_market_watch",
"ai_technology_watch",
"ansible_lint",
"cd_dev",
"code_review",
@@ -336,17 +404,18 @@
"run_migration",
"type_sync_check"
],
"health_contract": "多數 workflow 仍標示 ubuntu-latest需要 Gitea runner owner 以脫敏 metadata 證明實際 runner 對應、容量與維護責任。",
"health_contract": "general CI/CD and governance workflows use explicit awoooi-non110-ubuntu label instead of a generic runner label.",
"guardrail_refs": [
"docs/HARD_RULES.md",
"docs/security/S4-9-CANONICAL-OWNER-RESPONSE-ENVELOPE.md"
],
"evidence_refs": [
".gitea/workflows/agent-market-watch.yaml",
".gitea/workflows/cd-dev.yaml",
".gitea/workflows/e2e-health.yaml",
".gitea/workflows/run-migration.yml"
],
"next_action": "建立 owner attestation request不得直接把 label 改成 self-hosted 或啟用新 runner。"
"next_action": "維持 explicit non110 ubuntu label後續補容量/freshness 指標,不直接註冊或重啟 runner。"
},
{
"contract_id": "runner_watchdog_systemd",
@@ -357,10 +426,9 @@
"actions.runner.owenhytsai-awoooi.awoooi-110.service"
],
"used_by_workflows": [
"cd_pipeline",
"Gitea Actions host runner service"
],
"health_contract": "setup script 只描述 WatchdogSec=300、Restart=always、StartLimitBurst=5P1-002 不套用、不 restart、不 systemctl daemon-reload。",
"health_contract": "setup script 只描述 WatchdogSec=300、Restart=always、StartLimitBurst=5本 snapshot 不套用、不 restart、不 systemctl daemon-reload。",
"guardrail_refs": [
"scripts/setup-runner-watchdog.sh"
],
@@ -389,6 +457,30 @@
"scripts/ops/stop-stale-gitea-actions-jobs.sh"
],
"next_action": "維持 dry-run-only不得由治理頁或 API 直接停止 container。"
},
{
"contract_id": "secret_env_hygiene_guard",
"display_name": "Gitea step env secret hygiene guard",
"status": "manifest_mapped",
"risk_level": "high",
"runner_labels": [
"all_gitea_actions"
],
"used_by_workflows": [
"cd_pipeline",
"code_review",
"cd_dev"
],
"health_contract": "check-gitea-step-env-secrets.js is the committed source guard for step env / action input hygiene; this snapshot reads no secret values.",
"guardrail_refs": [
"scripts/ci/check-gitea-step-env-secrets.js"
],
"evidence_refs": [
"scripts/ci/check-gitea-step-env-secrets.js",
".gitea/workflows/cd.yaml",
".gitea/workflows/code-review.yaml"
],
"next_action": "保留 secret-name hygiene guard後續只做 redacted name inventory不讀 secret value。"
}
],
"notification_contracts": [
@@ -405,7 +497,22 @@
"evidence_refs": [
".gitea/workflows/agent-market-watch.yaml"
],
"next_action": "保留每週觀察 cadenceP1-002 不增加外部 API 或通知頻率。"
"next_action": "保留每週觀察 cadence不增加外部 API 或通知頻率。"
},
{
"contract_id": "ai_technology_watch_actionable_only",
"display_name": "AI Technology Watch actionable-only",
"status": "preserved",
"policy_kind": "actionable_only",
"success_noise_policy": "無 actionable 技術採用變更時保持安靜,不發成功洗版訊息。",
"failure_policy": "watch 失敗或需 review 時才產生 action-required evidence。",
"workflow_refs": [
"ai_technology_watch"
],
"evidence_refs": [
".gitea/workflows/ai-technology-watch.yaml"
],
"next_action": "保留技術雷達 cadence後續補報告 freshness readback。"
},
{
"contract_id": "e2e_health_failure_only",
@@ -421,7 +528,7 @@
".gitea/workflows/e2e-health.yaml",
"scripts/ci/notify-awoooi-cicd.sh"
],
"next_action": "保留 failure-only後續補最近 run readback。"
"next_action": "保留 failure-only後續補最近 run readback。"
},
{
"contract_id": "cd_pipeline_status_exception",
@@ -456,23 +563,27 @@
"next_action": "保留現況;後續評估 dev 通知是否需要降噪。"
},
{
"contract_id": "review_and_manual_workflow_status_exception",
"display_name": "Review / manual workflow 狀態例外",
"contract_id": "review_repair_and_manual_workflow_status_exception",
"display_name": "Review / repair / manual workflow 狀態例外",
"status": "exception_documented",
"policy_kind": "manual_status_exception",
"success_noise_policy": "code-review、alert deploy、migration 的狀態訊號不自動擴張到成功洗版;手動 workflow 成功仍應看情境與 release evidence。",
"failure_policy": "review、alert deploymigration failure 必須留可追蹤證據。",
"success_noise_policy": "code-review、alert deploy、migration、repair 的狀態訊號不自動擴張到成功洗版;手動 workflow 成功仍應看情境與 release evidence。",
"failure_policy": "review、alert deploymigration 或 repair failure 必須留可追蹤證據。",
"workflow_refs": [
"awoooi_onboarding_warning_step",
"code_review",
"deploy_alerts",
"harbor_110_local_repair",
"run_migration"
],
"evidence_refs": [
".gitea/workflows/awoooi-onboarding-warning-step.yaml",
".gitea/workflows/code-review.yaml",
".gitea/workflows/deploy-alerts.yaml",
".gitea/workflows/harbor-110-local-repair.yaml",
".gitea/workflows/run-migration.yml"
],
"next_action": "P1-003 盤點告警流程時再處理 alert deployP1-002 不發通知、不觸發 migration。"
"next_action": "後續補 alert/repair receipt本 snapshot 不發通知、不觸發 migration 或 repair。"
},
{
"contract_id": "lint_and_typecheck_no_notify",
@@ -489,7 +600,7 @@
".gitea/workflows/ansible-lint.yml",
".gitea/workflows/type-sync-check.yaml"
],
"next_action": "維持 no-notifyrunner attestation。"
"next_action": "維持 no-notifyartifact retention。"
}
],
"latest_observations": [
@@ -514,7 +625,7 @@
{
"observation_id": "workflow_secret_guard_present",
"status": "verified",
"summary": "正式 CD 已具備 check-gitea-step-env-secrets guard可阻擋 secrets 掛在 step env / action inputP1-002 不讀任何 Secret payload。",
"summary": "正式 CD / code-review / dev CD 已具備 check-gitea-step-env-secrets guard可阻擋 secrets 掛在 step env / action input本 snapshot 不讀任何 Secret payload。",
"evidence_refs": [
"scripts/ci/check-gitea-step-env-secrets.js",
".gitea/workflows/cd.yaml"
@@ -533,7 +644,7 @@
"deploy / migration workflow 觸發批准"
],
"secret_display_policy": "只允許顯示 workflow / secret-name hygiene 與 redacted metadata不得讀 token、runner token、webhook secret、authorization header 或任何 Secret payload。",
"runner_mutation_policy": "本 snapshot 只描述 runner label、watchdog 草案與 dry-run guard不得 systemctl restart、docker stop、修改 label、註冊 runner 或套用 watchdog。",
"runner_mutation_policy": "本 snapshot 只描述 explicit non110 runner label、watchdog 草案與 dry-run guard不得 systemctl restart、docker stop、修改 label、註冊 runner 或套用 watchdog。",
"notification_policy": "成功不洗版是預設治理方向failure-only / actionable-only 合約需保留CD/review/manual workflow 的狀態通知例外需另外標示。"
},
"operation_boundaries": {

View File

@@ -14,6 +14,73 @@ SERVICE="gitea"
GITEA_CONTAINER="gitea"
LOCAL_REPO="${BACKUP_BASE}/gitea"
DUMP_DIR="/tmp/gitea-backup-$$"
TEXTFILE_DIR="${NODE_EXPORTER_TEXTFILE_DIR:-/home/wooo/node_exporter_textfiles}"
TEXTFILE_PATH="${GITEA_FULL_BACKUP_RECEIPT_TEXTFILE:-${TEXTFILE_DIR}/gitea_full_backup_receipt.prom}"
HOST_LABEL="${AIOPS_HOST_LABEL:-110}"
GITEA_FULL_BACKUP_COMPONENTS="database repositories app_settings lfs_objects package_registry attachments hooks_custom_assets"
label_escape() {
printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'
}
write_gitea_full_backup_receipt() {
local status="$1"
local timestamp="$2"
local duration="$3"
local snapshot_id="${4:-unknown}"
local tmp
local host
local service_label
local status_label
local component
local ok=0
local failed=1
if [ "${status}" = "success" ]; then
ok=1
failed=0
fi
host="$(label_escape "${HOST_LABEL}")"
service_label="$(label_escape "${SERVICE}")"
status_label="$(label_escape "${status}")"
install -d -m 755 "${TEXTFILE_DIR}" || return 0
tmp="$(mktemp "${TEXTFILE_PATH}.tmp.XXXXXX")" || return 0
{
echo "# HELP awoooi_gitea_full_backup_last_success_timestamp Unix timestamp of the latest successful Gitea full-server backup receipt."
echo "# TYPE awoooi_gitea_full_backup_last_success_timestamp gauge"
echo "# HELP awoooi_gitea_full_backup_last_run_failed Whether the latest Gitea full-server backup run failed."
echo "# TYPE awoooi_gitea_full_backup_last_run_failed gauge"
echo "# HELP awoooi_gitea_full_backup_duration_seconds Duration of the latest Gitea full-server backup run."
echo "# TYPE awoooi_gitea_full_backup_duration_seconds gauge"
echo "# HELP awoooi_gitea_full_backup_dump_file_present Whether the Gitea dump file was produced before restic backup."
echo "# TYPE awoooi_gitea_full_backup_dump_file_present gauge"
echo "# HELP awoooi_gitea_full_backup_restic_snapshot_present Whether a restic snapshot id was recorded for the Gitea dump."
echo "# TYPE awoooi_gitea_full_backup_restic_snapshot_present gauge"
echo "# HELP awoooi_gitea_full_backup_component_receipt Whether the Gitea full-server dump includes a redacted component receipt."
echo "# TYPE awoooi_gitea_full_backup_component_receipt gauge"
echo "# HELP awoooi_gitea_full_backup_restore_drill_performed Whether this receipt performed a production restore drill."
echo "# TYPE awoooi_gitea_full_backup_restore_drill_performed gauge"
echo "# HELP awoooi_gitea_full_backup_secret_value_collected Whether this receipt collected secret values."
echo "# TYPE awoooi_gitea_full_backup_secret_value_collected gauge"
echo "# HELP awoooi_gitea_full_backup_production_restore_performed Whether this receipt restored anything into production."
echo "# TYPE awoooi_gitea_full_backup_production_restore_performed gauge"
echo "awoooi_gitea_full_backup_last_success_timestamp{host=\"${host}\",service=\"${service_label}\"} $([ "${ok}" = "1" ] && echo "${timestamp}" || echo 0)"
echo "awoooi_gitea_full_backup_last_run_failed{host=\"${host}\",service=\"${service_label}\",status=\"${status_label}\"} ${failed}"
echo "awoooi_gitea_full_backup_duration_seconds{host=\"${host}\",service=\"${service_label}\"} ${duration}"
echo "awoooi_gitea_full_backup_dump_file_present{host=\"${host}\",service=\"${service_label}\"} ${ok}"
echo "awoooi_gitea_full_backup_restic_snapshot_present{host=\"${host}\",service=\"${service_label}\",snapshot_id=\"$(label_escape "${snapshot_id}")\"} ${ok}"
for component in ${GITEA_FULL_BACKUP_COMPONENTS}; do
echo "awoooi_gitea_full_backup_component_receipt{host=\"${host}\",service=\"${service_label}\",component=\"$(label_escape "${component}")\",receipt=\"gitea_dump_restic\"} ${ok}"
done
echo "awoooi_gitea_full_backup_restore_drill_performed{host=\"${host}\",service=\"${service_label}\"} 0"
echo "awoooi_gitea_full_backup_secret_value_collected{host=\"${host}\",service=\"${service_label}\"} 0"
echo "awoooi_gitea_full_backup_production_restore_performed{host=\"${host}\",service=\"${service_label}\"} 0"
} >"${tmp}"
mv "${tmp}" "${TEXTFILE_PATH}" || return 0
chmod 0644 "${TEXTFILE_PATH}" || true
}
cleanup() {
rm -rf "${DUMP_DIR}"
@@ -37,6 +104,8 @@ main() {
docker exec -u git "${GITEA_CONTAINER}" rm -f /tmp/gitea-dump.zip
log_success "Gitea dump 完成"
else
duration=$(($(date +%s) - start_time))
write_gitea_full_backup_receipt "dump_failed" "0" "${duration}" "none" || true
log_error "Gitea dump 失敗"
notify_clawbot "failed" "${SERVICE}" "Gitea dump 失敗"
exit 1
@@ -44,13 +113,25 @@ main() {
if [ ! -d "${LOCAL_REPO}/data" ]; then
log_info "初始化本地 Restic 倉庫..."
restic -r "${LOCAL_REPO}" init --password-file "${RESTIC_PASSWORD_FILE}"
if ! restic -r "${LOCAL_REPO}" init --password-file "${RESTIC_PASSWORD_FILE}"; then
duration=$(($(date +%s) - start_time))
write_gitea_full_backup_receipt "restic_init_failed" "0" "${duration}" "none" || true
log_error "Restic 初始化失敗"
notify_clawbot "failed" "${SERVICE}" "Gitea Restic 初始化失敗"
exit 1
fi
fi
tags=$(build_tags "${SERVICE}")
restic -r "${LOCAL_REPO}" backup "${DUMP_DIR}" \
if ! restic -r "${LOCAL_REPO}" backup "${DUMP_DIR}" \
--password-file "${RESTIC_PASSWORD_FILE}" \
${tags}
${tags}; then
duration=$(($(date +%s) - start_time))
write_gitea_full_backup_receipt "restic_backup_failed" "0" "${duration}" "none" || true
log_error "Restic 備份失敗"
notify_clawbot "failed" "${SERVICE}" "Gitea Restic 備份失敗"
exit 1
fi
snapshot_id=$(restic -r "${LOCAL_REPO}" snapshots --latest 1 --json --password-file "${RESTIC_PASSWORD_FILE}" 2>/dev/null | grep -oP '"short_id":"\K[^"]+' | head -1 || true)
log_success "Restic 備份完成: ${snapshot_id:-unknown}"
@@ -61,6 +142,7 @@ main() {
log_info "Offsite copy is handled by sync-offsite-backups.sh; no direct rclone sync here."
duration=$(($(date +%s) - start_time))
write_gitea_full_backup_receipt "success" "$(date +%s)" "${duration}" "${snapshot_id:-unknown}" || true
log_success "========== Gitea 備份完成 (${duration}s) =========="
notify_clawbot "success" "${SERVICE}" "Gitea 備份完成" "${duration}"
}

View File

@@ -0,0 +1,44 @@
from __future__ import annotations
from pathlib import Path
ROOT = Path(__file__).resolve().parents[3]
BACKUP_GITEA = ROOT / "scripts" / "backup" / "backup-gitea.sh"
def test_backup_gitea_writes_full_server_component_receipts() -> None:
text = BACKUP_GITEA.read_text(encoding="utf-8")
assert "write_gitea_full_backup_receipt" in text
assert "awoooi_gitea_full_backup_component_receipt" in text
assert "awoooi_gitea_full_backup_secret_value_collected" in text
assert "awoooi_gitea_full_backup_production_restore_performed" in text
assert "gitea dump -c /data/gitea/conf/app.ini" in text
assert "GITEA_FULL_BACKUP_COMPONENTS" in text
for component in [
"database",
"repositories",
"app_settings",
"lfs_objects",
"package_registry",
"attachments",
"hooks_custom_assets",
]:
assert component in text
def test_backup_gitea_receipt_contract_stays_no_secret_no_restore() -> None:
text = BACKUP_GITEA.read_text(encoding="utf-8")
assert "awoooi_gitea_full_backup_secret_value_collected" in text
assert "awoooi_gitea_full_backup_production_restore_performed" in text
assert (
'secret_value_collected{host=\\"${host}\\",service=\\"${service_label}\\"} 0'
in text
)
assert (
'production_restore_performed{host=\\"${host}\\",service=\\"${service_label}\\"} 0'
in text
)