feat(iwooos): 強化 public gateway 緊急變更回補
All checks were successful
Code Review / ai-code-review (push) Successful in 24s
CD Pipeline / tests (push) Successful in 1m46s
CD Pipeline / build-and-deploy (push) Successful in 6m27s
CD Pipeline / post-deploy-checks (push) Successful in 2m59s

This commit is contained in:
Your Name
2026-06-15 14:03:41 +08:00
parent ed8c19059d
commit 9b8ca2c509
13 changed files with 311 additions and 61 deletions

View File

@@ -24,8 +24,8 @@ TAIPEI = timezone(timedelta(hours=8))
CONTROL_STATUS_BY_CATEGORY = {
"nginx_public_gateway": {
"coverage_status": "rendered_diff_acceptance_ledger_ready_needs_owner_live_diff",
"coverage_percent": 88,
"coverage_status": "emergency_change_backfill_ready_needs_owner_live_diff",
"coverage_percent": 90,
"evidence_refs": [
"docs/security/NGINX-CONFIG-DRIFT-DETECTOR.md",
"docs/security/nginx-config-drift-repo.snapshot.json",
@@ -38,8 +38,8 @@ CONTROL_STATUS_BY_CATEGORY = {
"docs/security/public-gateway-rendered-diff-acceptance.snapshot.json",
"docs/schemas/public_gateway_preflight_inventory_v1.schema.json",
],
"current_gap": "已固定 owner response acceptance 與 rendered diff evidence acceptance 只讀帳本owner response、live conf、rendered diff evidence、nginx -t evidence、route smoke evidence、maintenance window 與 rollback owner 仍全部為 0。",
"next_owner_action": "補 public gateway owner 回覆、owner-provided live conf、source-to-live rendered diff ref、nginx -t evidence ref、route smoke evidence ref、maintenance window 與 rollback owner。",
"current_gap": "已固定 owner response acceptance、手動 / 緊急 gateway 變更回補欄位與 rendered diff evidence acceptance 只讀帳本owner response、live conf、rendered diff evidence、nginx -t evidence、route smoke evidence、maintenance window 與 rollback owner 仍全部為 0。",
"next_owner_action": "補 public gateway owner 回覆、owner-provided live conf、source-to-live rendered diff ref、nginx -t evidence ref、route smoke evidence ref、change intent / break-glass、route health impact、rollback validation、maintenance window 與 rollback owner。",
},
"dns_tls_certbot": {
"coverage_status": "owner_response_acceptance_ledger_ready_needs_certificate_owner_evidence",
@@ -370,6 +370,7 @@ def build_report(root: Path, generated_at: str | None) -> dict[str, Any]:
"repo_only_preflight_contract_ready_needs_owner_live_diff",
"owner_response_acceptance_ledger_ready_needs_owner_live_diff",
"rendered_diff_acceptance_ledger_ready_needs_owner_live_diff",
"emergency_change_backfill_ready_needs_owner_live_diff",
"owner_response_acceptance_ledger_ready_needs_runtime_evidence",
"change_evidence_acceptance_ready_needs_gitops_owner_evidence",
"secret_injection_change_evidence_acceptance_ready_needs_owner_evidence",

View File

@@ -17,7 +17,7 @@ from typing import Any
EXPECTED_CATEGORIES = {
"nginx_public_gateway": 88,
"nginx_public_gateway": 90,
"dns_tls_certbot": 78,
"k8s_production_gitops": 64,
"secret_metadata": 68,
@@ -112,10 +112,10 @@ ARTIFACT_SPECS = [
"status": "owner_response_acceptance_ledger_ready_no_runtime_action",
"list_counts": {
"acceptance_candidates": 3,
"blocked_actions": 22,
"reviewer_checks": 16,
"outcome_lanes": 7,
"required_owner_response_fields": 16,
"blocked_actions": 28,
"reviewer_checks": 22,
"outcome_lanes": 8,
"required_owner_response_fields": 22,
},
"summary_counts": {
"acceptance_candidate_count": 3,

View File

@@ -46,6 +46,12 @@ ACCEPTANCE_FIELDS = [
"change_time_window",
"cross_project_impact_ref",
"communication_sync_ref",
"change_intent_or_ticket_ref",
"pre_change_approval_ref",
"break_glass_reason_ref",
"route_health_impact_ref",
"rollback_validation_ref",
"post_change_monitoring_window",
"reviewer_outcome",
"followup_owner",
"not_approval",
@@ -67,6 +73,12 @@ REQUIRED_OWNER_RESPONSE_FIELDS = [
"change_time_window",
"cross_project_impact_ref",
"communication_sync_ref",
"change_intent_or_ticket_ref",
"pre_change_approval_ref",
"break_glass_reason_ref",
"route_health_impact_ref",
"rollback_validation_ref",
"post_change_monitoring_window",
"followup_owner",
]
@@ -131,6 +143,30 @@ REVIEWER_CHECKS = [
"check_id": "communication_sync_ref_present",
"instruction": "必須提供通知或協調 ref證明相關專案 owner 已收到影響與回復狀態。",
},
{
"check_id": "change_intent_or_ticket_ref_present",
"instruction": "每次 Public Gateway / Nginx 變更都必須有 change intent、ticket、incident 或 maintenance ref不接受口頭或聊天截圖當唯一依據。",
},
{
"check_id": "pre_change_approval_or_break_glass_present",
"instruction": "正常變更需有 pre-change approval ref緊急變更需有 break-glass reason ref且不得把 break-glass 當成事前批准。",
},
{
"check_id": "route_health_impact_present",
"instruction": "必須提供 route health / service health impact ref確認受影響 domain、upstream、WebSocket、ACME 與 API 是否恢復。",
},
{
"check_id": "rollback_validation_present",
"instruction": "必須提供 rollback validation ref證明回滾路徑、回滾 owner 與回滾後驗證方式可追溯。",
},
{
"check_id": "post_change_monitoring_window_present",
"instruction": "必須提供 post-change monitoring window讓告警、route smoke 與跨專案回復狀態能對齊。",
},
{
"check_id": "manual_change_not_silent",
"instruction": "任何手動或緊急 gateway 變更不得靜默收件;若缺 actor、意圖、影響、通知或回滾驗證必須走補件或拒收。",
},
{
"check_id": "counts_transition_safe",
"instruction": "只有 reviewer record 可更新 received / accepted / rejected不得同時開 runtime gate。",
@@ -162,6 +198,10 @@ OUTCOME_LANES = [
"lane_id": "owner_review_only_update",
"meaning": "只允許更新只讀 owner review ledger不得修改 Nginx、DNS、TLS 或 workflow。",
},
{
"lane_id": "emergency_change_backfill_required",
"meaning": "手動或緊急 gateway 變更只能進事後補件,不得因 break-glass 而自動接受或開 runtime gate。",
},
{
"lane_id": "waiting_runtime_gate",
"meaning": "即使 owner response acceptedruntime gate 仍等待獨立人工批准。",
@@ -189,6 +229,12 @@ BLOCKED_ACTIONS = [
"accept_missing_change_time_window",
"skip_cross_project_impact_review",
"skip_incident_communication_sync",
"accept_silent_manual_nginx_change",
"treat_break_glass_as_approval",
"mark_gateway_change_resolved_without_route_health",
"skip_rollback_validation",
"skip_post_change_monitoring",
"hide_cross_project_route_impact",
"open_runtime_gate",
"add_action_button",
]
@@ -240,6 +286,12 @@ def acceptance_candidate(diff_gate: dict[str, Any]) -> dict[str, Any]:
"change_time_window": "pending_owner_response",
"cross_project_impact_ref": None,
"communication_sync_ref": None,
"change_intent_or_ticket_ref": None,
"pre_change_approval_ref": None,
"break_glass_reason_ref": None,
"route_health_impact_ref": None,
"rollback_validation_ref": None,
"post_change_monitoring_window": "pending_owner_response",
"reviewer_outcome": "waiting_owner_response",
"followup_owner": "pending_owner_response",
"acceptance_fields": ACCEPTANCE_FIELDS,
@@ -336,6 +388,12 @@ def build_report(
"change_time_window_accepted_count": 0,
"cross_project_impact_accepted_count": 0,
"communication_sync_accepted_count": 0,
"change_intent_accepted_count": 0,
"pre_change_approval_accepted_count": 0,
"break_glass_reason_accepted_count": 0,
"route_health_impact_accepted_count": 0,
"rollback_validation_accepted_count": 0,
"post_change_monitoring_window_accepted_count": 0,
"runtime_gate_count": 0,
"action_button_count": 0,
},
@@ -369,7 +427,7 @@ def build_report(
"等待 owner response未收到前不得更新 accepted count。",
"收到回覆後先走 raw payload / secret / scope / evidence ref 檢查,不合格即隔離、拒收或補件。",
"metadata 合格也只能進 rendered diff reviewer review`nginx -t`、reload、route smoke 與 production write 仍需獨立人工批准。",
"若涉及手動或緊急 gateway 變更,必須先補 change actor/source、time window、cross-project impactcommunication sync refs。",
"若涉及手動或緊急 gateway 變更,必須先補 change actor/source、time window、cross-project impactcommunication sync、change intent、break-glass reason、route health impact、rollback validation 與 post-change monitoring refs。",
],
}

View File

@@ -2988,12 +2988,12 @@ def validate(root: Path) -> None:
assert_equal(
"high_value_config_coverage.coverage_categories.nginx.coverage_percent",
nginx_gateway_category["coverage_percent"],
88,
90,
)
assert_equal(
"high_value_config_coverage.coverage_categories.nginx.coverage_status",
nginx_gateway_category["coverage_status"],
"rendered_diff_acceptance_ledger_ready_needs_owner_live_diff",
"emergency_change_backfill_ready_needs_owner_live_diff",
)
for evidence_ref in [
"docs/security/PUBLIC-GATEWAY-PREFLIGHT-INVENTORY.md",
@@ -5862,11 +5862,11 @@ def validate(root: Path) -> None:
"acceptance_candidate_count": 3,
"c0_acceptance_candidate_count": 2,
"c1_acceptance_candidate_count": 1,
"acceptance_field_count": 27,
"required_owner_response_field_count": 16,
"reviewer_check_count": 16,
"outcome_lane_count": 7,
"blocked_action_count": 22,
"acceptance_field_count": 33,
"required_owner_response_field_count": 22,
"reviewer_check_count": 22,
"outcome_lane_count": 8,
"blocked_action_count": 28,
"request_sent_count": 0,
"recipient_confirmed_count": 0,
"owner_response_received_count": 0,
@@ -5892,6 +5892,12 @@ def validate(root: Path) -> None:
"change_time_window_accepted_count": 0,
"cross_project_impact_accepted_count": 0,
"communication_sync_accepted_count": 0,
"change_intent_accepted_count": 0,
"pre_change_approval_accepted_count": 0,
"break_glass_reason_accepted_count": 0,
"route_health_impact_accepted_count": 0,
"rollback_validation_accepted_count": 0,
"post_change_monitoring_window_accepted_count": 0,
"runtime_gate_count": 0,
"action_button_count": 0,
}
@@ -5935,6 +5941,12 @@ def validate(root: Path) -> None:
"change_time_window_present",
"cross_project_impact_review_present",
"communication_sync_ref_present",
"change_intent_or_ticket_ref_present",
"pre_change_approval_or_break_glass_present",
"route_health_impact_present",
"rollback_validation_present",
"post_change_monitoring_window_present",
"manual_change_not_silent",
"counts_transition_safe",
]
assert_equal(
@@ -5949,6 +5961,7 @@ def validate(root: Path) -> None:
"request_supplement",
"ready_for_rendered_diff_review",
"owner_review_only_update",
"emergency_change_backfill_required",
"waiting_runtime_gate",
]
assert_equal(
@@ -5977,6 +5990,12 @@ def validate(root: Path) -> None:
"accept_missing_change_time_window",
"skip_cross_project_impact_review",
"skip_incident_communication_sync",
"accept_silent_manual_nginx_change",
"treat_break_glass_as_approval",
"mark_gateway_change_resolved_without_route_health",
"skip_rollback_validation",
"skip_post_change_monitoring",
"hide_cross_project_route_impact",
"open_runtime_gate",
"add_action_button",
]
@@ -5989,27 +6008,27 @@ def validate(root: Path) -> None:
assert_equal(
f"public_gateway_owner_response_acceptance.{item['acceptance_candidate_id']}.field_count",
len(item["acceptance_fields"]),
27,
33,
)
assert_equal(
f"public_gateway_owner_response_acceptance.{item['acceptance_candidate_id']}.required_owner_response_fields",
len(item["required_owner_response_fields"]),
16,
22,
)
assert_equal(
f"public_gateway_owner_response_acceptance.{item['acceptance_candidate_id']}.reviewer_checks",
len(item["reviewer_checks"]),
16,
22,
)
assert_equal(
f"public_gateway_owner_response_acceptance.{item['acceptance_candidate_id']}.outcome_lanes",
len(item["outcome_lanes"]),
7,
8,
)
assert_equal(
f"public_gateway_owner_response_acceptance.{item['acceptance_candidate_id']}.blocked_actions",
len(item["blocked_actions"]),
22,
28,
)
assert_true(
f"public_gateway_owner_response_acceptance.{item['acceptance_candidate_id']}.not_approval",
@@ -16222,14 +16241,18 @@ def validate(root: Path) -> None:
"high_value_config_control_coverage_category_count=14",
"high_value_config_control_coverage_c0_category_count=8",
"high_value_config_control_coverage_c1_category_count=4",
"high_value_config_control_coverage_average_percent=68",
"high_value_config_control_coverage_average_percent=69",
"high_value_config_control_coverage_needs_live_evidence_count=9",
"high_value_config_control_coverage_owner_response_required_count=14",
"high_value_config_control_coverage_owner_response_received_count=0",
"high_value_config_control_coverage_owner_response_accepted_count=0",
"high_value_config_control_coverage_runtime_gate_count=0",
"high_value_config_control_coverage_action_button_count=0",
"nginx_public_gateway_coverage_percent=88",
"nginx_public_gateway_coverage_percent=90",
"public_gateway_owner_response_acceptance_required_owner_response_field_count=22",
"public_gateway_owner_response_acceptance_reviewer_check_count=22",
"public_gateway_owner_response_acceptance_outcome_lane_count=8",
"public_gateway_owner_response_acceptance_blocked_action_count=28",
"k8s_production_gitops_coverage_percent=64",
"secret_metadata_coverage_percent=68",
"gitea_workflow_runner_source_control_coverage_percent=72",
@@ -16461,7 +16484,7 @@ def validate(root: Path) -> None:
assert_text_contains(
"iwooos_page.critical_config_priority_nginx_percent",
iwooos_projection_page,
"{ key: 'nginxGateway', rank: 'P0-1', state: '88% / live 0'",
"{ key: 'nginxGateway', rank: 'P0-1', state: '90% / live 0'",
)
assert_text_contains(
"iwooos_page.critical_config_priority_dns_percent",