diff --git a/.gitea/workflows/cd-dev.yaml b/.gitea/workflows/cd-dev.yaml index f81309bac..5de370a6c 100644 --- a/.gitea/workflows/cd-dev.yaml +++ b/.gitea/workflows/cd-dev.yaml @@ -8,8 +8,8 @@ name: CD Pipeline (Dev) on: - push: - branches: [dev] + # 2026-06-28 Codex: 110 runner 壓力事故期間,dev deploy 不再由 push 自動觸發。 + # 恢復自動 deploy 前必須完成 runner 搬遷或非 110 硬限流。 workflow_dispatch: concurrency: diff --git a/.gitea/workflows/deploy-alerts.yaml b/.gitea/workflows/deploy-alerts.yaml index d9d518b91..46ef82ee5 100644 --- a/.gitea/workflows/deploy-alerts.yaml +++ b/.gitea/workflows/deploy-alerts.yaml @@ -8,12 +8,8 @@ name: Deploy Alert Rules on: - push: - branches: [main] - paths: - - 'ops/monitoring/alerts-unified.yml' - - 'ops/monitoring/slo-rules.yml' - - 'scripts/ops/deploy-alerts.sh' + # 2026-06-28 Codex: 110 runner 壓力事故期間,告警規則部署改為受控手動觸發。 + # push path filtering 不得在 runner 搬遷 / 硬限流前重開。 workflow_dispatch: env: diff --git a/.gitea/workflows/run-migration.yml b/.gitea/workflows/run-migration.yml index d5eeba9bc..21ac946dc 100644 --- a/.gitea/workflows/run-migration.yml +++ b/.gitea/workflows/run-migration.yml @@ -8,15 +8,13 @@ # 3. 跑後寫 asset_discovery_run + automation_operation_log 記錄 # 4. 失敗自動 rollback (single transaction + ON_ERROR_STOP) # -# 觸發: push to main,且 apps/api/migrations/ 有變更 +# 觸發: 受控手動觸發;apps/api/migrations/ 有變更時先做 source diff / dry-run / rollback verifier name: run-migration on: - push: - branches: [main] - paths: - - 'apps/api/migrations/*.sql' + # 2026-06-28 Codex: migration 屬高價值 runtime change,且 110 runner 正在壓力事故保護期。 + # 不得由 push 自動觸發;恢復前必須完成 runner 搬遷 / 硬限流與 migration dry-run gate。 workflow_dispatch: env: diff --git a/.gitea/workflows/type-sync-check.yaml b/.gitea/workflows/type-sync-check.yaml index 1d71b505d..efb229c7e 100644 --- a/.gitea/workflows/type-sync-check.yaml +++ b/.gitea/workflows/type-sync-check.yaml @@ -3,7 +3,7 @@ # ============================================================================= # 驗證 Pydantic 模型與 TypeScript 型別是否同步 # -# 觸發條件: apps/api/src/models/** 變更 +# 觸發條件: 受控手動觸發;apps/api/src/models/** 變更時由本地 guard / reviewer 指派執行 # 失敗時: 開發者需執行 `pnpm --filter @awoooi/shared-types generate` # # 2026-03-31 Claude Code (Phase 14.3) @@ -11,17 +11,8 @@ name: Type Sync Check on: - push: - branches: [main] - paths: - - 'apps/api/src/models/**' - - 'packages/shared-types/**' - - 'scripts/generate-schemas.py' - pull_request: - paths: - - 'apps/api/src/models/**' - - 'packages/shared-types/**' - - 'scripts/generate-schemas.py' + # 2026-06-28 Codex: 110 runner 壓力事故期間,禁止 push / PR 自動啟動 type sync。 + workflow_dispatch: jobs: check-type-sync: diff --git a/AGENTS.md b/AGENTS.md index 73f9c95cb..683d1cdd1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,7 +46,7 @@ 正確動作是 AI 自動補齊 target selector、source-of-truth diff、check-mode / dry-run、rollback、post-apply verifier、KM / PlayBook trust writeback,然後推進可驗證、可回滾、低爆炸半徑的實作。 -**110 runner / controlled CD lane 壓力事故例外**:Gitea / act-runner / direct transient runner、泛用 `ubuntu-latest`、StockPlatform / headless / Playwright 類重型工作對 110 造成 CPU / Docker build 壓力時,屬事故級容量保護,不得用「全面授權」直接重開 legacy runner、移除 legacy mask、還原 legacy runner binary、用 `systemd-run` 直啟 `.real` binary,或把 host pressure gate 改成 warn-only。未完成 runner 搬遷或非 110 硬限流前,`awoooi-cd-lane.service`、`awoooi-cd-lane-drain.service`、direct runner 與 Gitea runner 必須由 `awoooi-runner-failclosed-enforcer.timer`、`awoooi-runner-failclosed-authority.timer` 與 `/etc/cron.d/awoooi-runner-failclosed-authority` 維持 masked / inactive / no process / no job container / root restore-source left `0`;cron / systemd authority 必須執行 `/usr/local/lib/awoooi/enforce-110-runner-failclosed.authority.sh`,讓外部 opener 覆寫 canonical `/usr/local/lib/awoooi/enforce-110-runner-failclosed.sh` 時仍能自動修復。舊 `/tmp/enforce-110-runner-failclosed.sh`、`/tmp/awoooi-enforce-runner-failclosed-110.sh*` enforcer source、startup open drop-in、`awoooi-runner-failclosed-opened-*`、`awoooi-runner-failclosed-*-opened-*`、`awoooi-runner-failclosed-quarantine-*`、`failclosed-final-mask-*` disabler artifact 與 restore-source 也必須封存或改成 fail-closed stub。Gitea `cd.yaml` / `code-review.yaml` push workflow 維持 manual-only。 +**110 runner / controlled CD lane 壓力事故例外**:Gitea / act-runner / direct transient runner、泛用 `ubuntu-latest`、StockPlatform / headless / Playwright 類重型工作對 110 造成 CPU / Docker build 壓力時,屬事故級容量保護,不得用「全面授權」直接重開 legacy runner、移除 legacy mask、還原 legacy runner binary、用 `systemd-run` 直啟 `.real` binary,或把 host pressure gate 改成 warn-only。未完成 runner 搬遷或非 110 硬限流前,`awoooi-cd-lane.service`、`awoooi-cd-lane-drain.service`、direct runner 與 Gitea runner 必須由 `awoooi-runner-failclosed-enforcer.timer`、`awoooi-runner-failclosed-authority.timer` 與 `/etc/cron.d/awoooi-runner-failclosed-authority` 維持 masked / inactive / no process / no job container / root restore-source left `0`;cron / systemd authority 必須執行 `/usr/local/lib/awoooi/enforce-110-runner-failclosed.authority.sh`,讓外部 opener 覆寫 canonical `/usr/local/lib/awoooi/enforce-110-runner-failclosed.sh` 時仍能自動修復。舊 `/tmp/enforce-110-runner-failclosed.sh`、`/tmp/awoooi-enforce-runner-failclosed-110.sh*` enforcer source、startup open drop-in、`awoooi-runner-failclosed-opened-*`、`awoooi-runner-failclosed-*-opened-*`、`awoooi-runner-failclosed-quarantine-*`、`failclosed-final-mask-*` disabler artifact 與 restore-source 也必須封存或改成 fail-closed stub。Gitea `.gitea/workflows` 不得把 `push` / `pull_request` 自動事件接到 `awoooi-ubuntu` / `awoooi-host`,也不得恢復 `ubuntu-latest` / `self-hosted` 泛用 label;變更必須通過 `ops/runner/guard-gitea-runner-pressure.py`。 --- diff --git a/docs/HARD_RULES.md b/docs/HARD_RULES.md index e668f17fe..45280f0c1 100644 --- a/docs/HARD_RULES.md +++ b/docs/HARD_RULES.md @@ -8,7 +8,7 @@ | 欄位 | 值 | |------|-----| -| **版本** | v2.6 | +| **版本** | v2.7 | | **建立日期** | 2026-03-20 (台北) | | **建立者** | Claude Code | | **最後修改** | 2026-06-28 (台北) | @@ -35,6 +35,7 @@ | v2.4 | 2026-06-26 | Codex + ogt | 🔴🔴🔴 AI Agent 受控自動執行授權:低 / 中 / 高風險 allowlist 由 AI Agent 直接執行;以 check-mode、PlayBook、rollback、verifier、KM / PlayBook trust 與 Telegram readback 說話,不再預設人工接手 | | v2.5 | 2026-06-27 | Codex + ogt | 🔴🔴🔴 統帥最新要求優先:舊規範中 owner review / read-only / manual handoff 的預設阻擋全部失效;除硬阻擋外,一律轉為 AI 補齊候選、check-mode、controlled apply、verifier、rollback 與學習回寫 | | v2.6 | 2026-06-28 | Codex + ogt | 🔴🔴🔴 統帥全面授權:所有非事故級 hard gate / guard 預設打開為 AI controlled apply;`0 / false` 只能作 evidence,不得阻擋實作;真正硬阻擋只剩 secret、不可逆資料破壞、reboot / node drain / firewall cutover、攻擊型掃描、成本 / provider、force push / repo refs 破壞、raw secret volume 與未經 replay / shadow / canary 的核心替換 | +| v2.7 | 2026-06-28 | Codex + ogt | 🔴🔴🔴 110 runner 壓力防再發:`.gitea/workflows` 不得把 push / pull_request 自動事件接到 `awoooi-ubuntu` / `awoooi-host` 或泛用 label;新增 source guard 驗證 | --- @@ -293,6 +294,8 @@ force push / 刪 repo / 刪 refs / 改 repo visibility / raw runtime secret volu 允許的 controlled apply 是降壓與防再發:停止 / disable / mask legacy runner、mask direct transient unit、quarantine legacy runner binary、收斂 labels、補 source fail-closed guard、限制 concurrency、把 smoke 改成排程 / 非 110 runner,以及執行只讀 pressure / cold-start verifier。未完成 runner 搬遷或非 110 硬限流前,`awoooi-cd-lane.service`、`awoooi-cd-lane-drain.service`、direct runner 與 Gitea runner 必須由 `awoooi-runner-failclosed-enforcer.timer`、`awoooi-runner-failclosed-authority.timer` 與 `/etc/cron.d/awoooi-runner-failclosed-authority` 維持 masked / inactive / no process / no job container / root restore-source left `0`;cron / systemd authority 必須執行 `/usr/local/lib/awoooi/enforce-110-runner-failclosed.authority.sh`,並用該 authority copy 修復 canonical `/usr/local/lib/awoooi/enforce-110-runner-failclosed.sh`。若外部 opener 暫時恢復 unit 或覆寫 canonical,只能恢復成帶 `ConditionPathExists=/run/awoooi-runner-migrated-or-hard-limited` 的 fail-closed stub,下一輪 cron authority / authority / enforcer 必須再收斂回 masked / inactive。verifier 不得再接受單一 `controlled_open` lane。 +同一事故期內,`.gitea/workflows` 不得再讓 `push`、`pull_request` 或 `pull_request_target` 自動事件命中 `awoooi-ubuntu` / `awoooi-host`,也不得恢復 `ubuntu-latest`、`ubuntu-*` 或 `self-hosted` 泛用 label。所有 workflow 變更必須通過 `ops/runner/guard-gitea-runner-pressure.py`;若要恢復自動事件,必須先提交 runner 搬遷 / 非 110 硬限流 source-of-truth diff、rollback 與 post-apply verifier。 + 恢復 runner 必須同時具備: 1. target selector:明確列出 service、runner dir、label 與承接 repo。 diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index acce1723b..36e371964 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,19 @@ +## 2026-06-28 — 19:10 110 runner push-trigger pressure source guard + +**完成內容**: +- `.gitea/workflows/cd-dev.yaml`、`deploy-alerts.yaml`、`type-sync-check.yaml`、`run-migration.yml` 移除 push / PR 自動觸發,改為 `workflow_dispatch` controlled path。 +- 新增 `ops/runner/guard-gitea-runner-pressure.py`,source guard 禁止 `.gitea/workflows` 把 `push` / `pull_request` / `pull_request_target` 自動事件接到 `awoooi-ubuntu` / `awoooi-host`,也禁止恢復 `ubuntu-latest` / `ubuntu-*` / `self-hosted` 泛用 label。 +- `scripts/ops/ansible-validate.sh` 接入 runner pressure guard;`AGENTS.md`、`docs/HARD_RULES.md`、MASTER 與 `ops/runner/README.md` 同步規範:全面授權不等於重開 110 legacy runner,也不等於恢復自動 push runner 壓力。 + +**驗證結果**: +- `python3 ops/runner/guard-gitea-runner-pressure.py --root .`:`GITEA_RUNNER_PRESSURE_GUARD_OK workflow_files=10 scheduled_workflows=3 auto_branch_events_on_110=0 generic_runner_labels=0`。 +- `python3 -m py_compile ops/runner/guard-gitea-runner-pressure.py`:通過。 +- `ruby -e 'require "yaml"; Dir[".gitea/workflows/*.{yml,yaml}"].sort.each { |p| YAML.load_file(p); puts "YAML_OK #{p}" }'`:10 個 Gitea workflow 皆通過。 +- `git diff --check`:通過。 +- 本機 `python3` 缺 PyYAML,未用 `scripts/ops/ansible-validate.sh` 做完整 Ansible validation;本輪已用 Ruby YAML parse + 新 guard + Python compile 覆蓋本次 workflow / guard 改動。 + +**邊界**:未讀 secret / runner token / raw session / SQLite / auth / `.env`;未重啟 Docker / Nginx / firewall / K3s / DB;未打開 legacy runner 或 controlled drain lane;未使用 GitHub API / gh / GitHub Actions。 + ## 2026-06-28 — 18:58 110 fail-closed authority cron immutable 修復與 P3 讀回 **背景**:`1fdbc96a9` 曾把 fail-closed authority source / timer source 刪除,live 110 也被改成 disabled stub:authority / canonical SHA `03bbf87d...`、`/etc/cron.d/awoooi-runner-failclosed-authority` 內容為 `No cron jobs are defined`、`awoooi-cd-lane-drain.service active/enabled`。後續 `f4d1b99da` 與 `6840c3578` 已 revert 該方向;`5c540460c` / `3b3fbef63` 補上 cron 檔 immutable 覆寫修復。 diff --git a/docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md b/docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md index 11a667478..2947189c6 100644 --- a/docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md +++ b/docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md @@ -153,7 +153,7 @@ AWOOOI / AwoooP / IwoooS 不是單純監控頁、告警轉發器或資安清冊 3. 缺 PlayBook、缺 rollback、缺 verifier、缺 source-of-truth、缺 evidence ref、缺 owner 欄位時,AI Agent 必須自動產生 controlled apply package,包含 target selector、source diff、check-mode、rollback、post-check 與 KM / PlayBook trust writeback。 4. Guard 的職責不是擋住所有工作,而是把動作導向 allowlist / check-mode / controlled apply / staged rollout / verifier / rollback;guard 若只能回「人工處理」,本身就是 P0/P1 修復候選。 5. 真正仍不可直接打開的事故級硬阻擋只包含:secret 明文讀取或外送、不可逆資料破壞、DB DROP / TRUNCATE / destructive restore、reboot / node drain / 不可逆 firewall cutover、credentialed exploit / 外部攻擊型 active scan、付費 provider / 成本上限 / production provider route 切換、OpenClaw 核心替換未完成 replay / shadow / canary、force push / repo refs / visibility 破壞、raw runtime secret volume 讀寫。 -6. 110 runner 容量事故屬硬保護例外:不得重開 legacy runner、解除 legacy fail-closed、恢復泛用 label 或把 host pressure gate warn-only;未完成 runner 搬遷或非 110 硬限流前,AWOOOI controlled CD lane / drain lane 也必須由 `awoooi-runner-failclosed-enforcer.timer`、`awoooi-runner-failclosed-authority.timer` 與 `/etc/cron.d/awoooi-runner-failclosed-authority` 維持 masked / inactive / no process / no job container / root restore-source left `0`,cron / systemd authority 必須執行 `/usr/local/lib/awoooi/enforce-110-runner-failclosed.authority.sh` 並修復 canonical `/usr/local/lib/awoooi/enforce-110-runner-failclosed.sh`,舊 `/tmp/enforce-110-runner-failclosed.sh`、`/tmp/awoooi-enforce-runner-failclosed-110.sh*` opener source、`awoooi-runner-failclosed-opened-*`、`awoooi-runner-failclosed-*-opened-*`、`awoooi-runner-failclosed-quarantine-*` 與 `failclosed-final-mask-*` disabler artifact 必須封成 fail-closed stub,workflow push trigger 維持 manual-only。 +6. 110 runner 容量事故屬硬保護例外:不得重開 legacy runner、解除 legacy fail-closed、恢復泛用 label 或把 host pressure gate warn-only;未完成 runner 搬遷或非 110 硬限流前,AWOOOI controlled CD lane / drain lane 也必須由 `awoooi-runner-failclosed-enforcer.timer`、`awoooi-runner-failclosed-authority.timer` 與 `/etc/cron.d/awoooi-runner-failclosed-authority` 維持 masked / inactive / no process / no job container / root restore-source left `0`,cron / systemd authority 必須執行 `/usr/local/lib/awoooi/enforce-110-runner-failclosed.authority.sh` 並修復 canonical `/usr/local/lib/awoooi/enforce-110-runner-failclosed.sh`,舊 `/tmp/enforce-110-runner-failclosed.sh`、`/tmp/awoooi-enforce-runner-failclosed-110.sh*` opener source、`awoooi-runner-failclosed-opened-*`、`awoooi-runner-failclosed-*-opened-*`、`awoooi-runner-failclosed-quarantine-*` 與 `failclosed-final-mask-*` disabler artifact 必須封成 fail-closed stub;`.gitea/workflows` 不得把 `push`、`pull_request` 或 `pull_request_target` 自動事件接到 `awoooi-ubuntu` / `awoooi-host`,也不得恢復 `ubuntu-latest` / `ubuntu-*` / `self-hosted` 泛用 label,workflow 變更必須通過 `ops/runner/guard-gitea-runner-pressure.py`。 7. 資料 freshness gate 必須 source-aware:若 Drive / provider source preflight 證明沒有比最後乾淨 import 更新的來源,且 DB sync / import job 乾淨,stale business data 是 source freshness warning;auth/source/failed-folder/DB sync 有異常才是 hard blocker。 8. Provider proxy gate 必須避免成本 / route 誤開:未 provisioned 且 repo 已標 optional retired 的 LiteLLM 等 proxy,只能列 warning;不得為了過 health gate 自動啟動或切 production provider route。 diff --git a/ops/runner/README.md b/ops/runner/README.md index ef86c4b30..fbfb65213 100644 --- a/ops/runner/README.md +++ b/ops/runner/README.md @@ -438,8 +438,26 @@ root live artifact 與 lane registration 檔名都屬 restore source, 復活。 未完成 runner 搬遷、硬限流、smoke 排程前,不得解除 mask、恢復泛用 runner label、 -恢復 cd-lane / drain ELF,或把 host pressure gate 預設改成 warn-only;`cd.yaml` / -`code-review.yaml` push trigger 維持 manual-only。 +恢復 cd-lane / drain ELF,或把 host pressure gate 預設改成 warn-only;所有會命中 +`awoooi-ubuntu` / `awoooi-host` 的 `.gitea/workflows` 都不得保留 `push`、 +`pull_request` 或 `pull_request_target` 自動事件。 + +### 第九層修復: workflow pressure source guard + +2026-06-28 補上 source guard: + +```bash +python3 ops/runner/guard-gitea-runner-pressure.py --root . +``` + +此 guard 只讀 repo 內 `.gitea/workflows/*.yml` / `.yaml`,禁止兩類回歸: + +1. `push` / `pull_request` / `pull_request_target` 自動事件命中 `awoooi-ubuntu` 或 + `awoooi-host`。 +2. Gitea workflow 恢復 `ubuntu-latest`、`ubuntu-*` 或 `self-hosted` 泛用 label。 + +`scripts/ops/ansible-validate.sh` 會執行同一 guard。若要恢復自動事件,必須先有 +runner 搬遷或非 110 硬限流的 source-of-truth diff、rollback 與 post-apply verifier。 --- 版本: v2.0 | 更新: 2026-03-29 | 作者: Claude Code diff --git a/ops/runner/guard-gitea-runner-pressure.py b/ops/runner/guard-gitea-runner-pressure.py new file mode 100644 index 000000000..c27b57269 --- /dev/null +++ b/ops/runner/guard-gitea-runner-pressure.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +"""Guard Gitea workflows from reintroducing 110 runner pressure triggers. + +This is a source guard only. It reads committed `.gitea/workflows` files and +fails when automatic branch events target 110 incident runner labels, or when a +generic runner label is reintroduced. +""" + +from __future__ import annotations + +import argparse +import re +import sys +from dataclasses import dataclass +from pathlib import Path + + +AUTO_BRANCH_EVENTS = {"push", "pull_request", "pull_request_target"} +INCIDENT_RUNNER_LABELS = {"awoooi-ubuntu", "awoooi-host"} +GENERIC_LABELS = {"ubuntu-latest", "self-hosted"} + +RUNS_ON_RE = re.compile(r"^\s*runs-on:\s*(?P.+?)\s*$") +ON_RE = re.compile(r"^on:\s*(?P.*)$") +ON_CHILD_EVENT_RE = re.compile(r"^(?P\s+)(?P[A-Za-z_][A-Za-z0-9_-]*)\s*:") + + +@dataclass(frozen=True) +class WorkflowInfo: + path: Path + events: set[str] + labels: list[tuple[int, str]] + + +@dataclass(frozen=True) +class Violation: + path: Path + line: int + label: str + reason: str + events: set[str] + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", type=Path, default=Path.cwd(), help="Repository root") + parser.add_argument( + "--workflow-dir", + default=".gitea/workflows", + help="Workflow directory relative to --root", + ) + return parser.parse_args() + + +def remove_inline_comment(value: str) -> str: + return value.split("#", 1)[0].rstrip() + + +def strip_inline_comment(value: str) -> str: + return remove_inline_comment(value).strip() + + +def parse_inline_events(value: str) -> set[str]: + clean = strip_inline_comment(value).strip() + if not clean: + return set() + if clean.startswith("[") and clean.endswith("]"): + return {part.strip().strip("'\"") for part in clean[1:-1].split(",") if part.strip()} + if clean.startswith("{") and clean.endswith("}"): + return { + part.split(":", 1)[0].strip().strip("'\"") + for part in clean[1:-1].split(",") + if part.strip() + } + return {clean.strip().strip("'\"")} + + +def parse_events(lines: list[str]) -> set[str]: + events: set[str] = set() + for index, line in enumerate(lines): + match = ON_RE.match(remove_inline_comment(line)) + if not match: + continue + inline_events = parse_inline_events(match.group("value")) + if inline_events: + events.update(inline_events) + continue + + for child in lines[index + 1 :]: + if not child.strip() or child.lstrip().startswith("#"): + continue + if child[0] not in {" ", "\t"}: + break + child_match = ON_CHILD_EVENT_RE.match(remove_inline_comment(child)) + if child_match: + events.add(child_match.group("event")) + return events + + +def normalize_runs_on(value: str) -> list[str]: + clean = strip_inline_comment(value).strip() + if clean.startswith("[") and clean.endswith("]"): + return [part.strip().strip("'\"") for part in clean[1:-1].split(",") if part.strip()] + return [clean.strip().strip("'\"")] + + +def parse_workflow(path: Path) -> WorkflowInfo: + lines = path.read_text(encoding="utf-8").splitlines() + labels: list[tuple[int, str]] = [] + for line_number, line in enumerate(lines, start=1): + match = RUNS_ON_RE.match(line) + if not match: + continue + for label in normalize_runs_on(match.group("value")): + labels.append((line_number, label)) + return WorkflowInfo(path=path, events=parse_events(lines), labels=labels) + + +def label_is_generic(label: str) -> bool: + if label in GENERIC_LABELS: + return True + return label.startswith("ubuntu-") + + +def inspect(workflow_dir: Path) -> tuple[list[WorkflowInfo], list[Violation]]: + workflows = [parse_workflow(path) for path in sorted(workflow_dir.glob("*.y*ml"))] + violations: list[Violation] = [] + + for workflow in workflows: + auto_events = workflow.events & AUTO_BRANCH_EVENTS + for line, label in workflow.labels: + if label_is_generic(label): + violations.append( + Violation( + path=workflow.path, + line=line, + label=label, + reason="generic_runner_label_reintroduced", + events=workflow.events, + ) + ) + if auto_events and label in INCIDENT_RUNNER_LABELS: + violations.append( + Violation( + path=workflow.path, + line=line, + label=label, + reason="auto_branch_event_targets_110_incident_runner", + events=workflow.events, + ) + ) + return workflows, violations + + +def main() -> int: + args = parse_args() + root = args.root.expanduser().resolve() + workflow_dir = root / args.workflow_dir + if not workflow_dir.exists(): + print(f"GITEA_RUNNER_PRESSURE_GUARD_BLOCKED workflow_dir_missing={workflow_dir}") + return 2 + + workflows, violations = inspect(workflow_dir) + if violations: + print("GITEA_RUNNER_PRESSURE_GUARD_BLOCKED") + for item in violations: + rel_path = item.path.relative_to(root) + events = ",".join(sorted(item.events)) or "none" + print( + f"{rel_path}:{item.line} label={item.label} " + f"reason={item.reason} events={events}" + ) + return 1 + + scheduled = sum(1 for workflow in workflows if "schedule" in workflow.events) + print( + "GITEA_RUNNER_PRESSURE_GUARD_OK " + f"workflow_files={len(workflows)} " + f"scheduled_workflows={scheduled} " + "auto_branch_events_on_110=0 " + "generic_runner_labels=0" + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/ops/ansible-validate.sh b/scripts/ops/ansible-validate.sh index 12c736b27..5427b5c13 100755 --- a/scripts/ops/ansible-validate.sh +++ b/scripts/ops/ansible-validate.sh @@ -25,6 +25,9 @@ for path in paths: print(f"YAML_OK {path}") PY +echo "== Gitea runner 壓力防再發 guard ==" +python3 ops/runner/guard-gitea-runner-pressure.py --root "$ROOT_DIR" + echo "== Shell 語法 ==" bash -n \ scripts/reboot-recovery/full-stack-cold-start-check.sh \ @@ -69,6 +72,7 @@ echo "Shell 語法 OK" echo "== Python 語法 ==" python3 -m py_compile \ + ops/runner/guard-gitea-runner-pressure.py \ scripts/ops/docker-stats-textfile-exporter.py \ scripts/ops/systemd-units-textfile-exporter.py \ scripts/ops/storage-health-textfile-exporter.py \