code-to10 v6 A10: release gate single evaluator via wiring adapter
(cherry picked from commit 77ed1c3094798f75bc41323f9aa60f744f5ba507)
This commit is contained in:
1 parent
3ef4411c32
commit
fd22f03c15
3 files changed
+126
-117
No files matched your search
+78
-81
@@ -1,38 +1,42 @@
|
|||||||
"""Future release endpoint: the gate check, without the endpoint.
|
"""Release gate, unified: thin adapter over the single evaluator.
|
||||||
|
|
||||||
This module is the wiring preparation for the release endpoint
|
The sole release evaluator is
|
||||||
that PR #247 explicitly left open: ``release_blocked`` is returned
|
``cloud/audit_loop_wiring.evaluate_release_gate``. The live
|
||||||
alongside the merge response today, but nothing enforces it yet,
|
endpoint (``POST /tasks/{id}/release``,
|
||||||
because there is no server-side release endpoint in this codebase
|
``handler_lifecycle._release_task``) calls it directly; this
|
||||||
and ``deploy.py`` is an operator script, not a release endpoint
|
module only adapts its decision into the legacy shape below so
|
||||||
|
the contract tests in ``cloud/tests/test_release_gate.py`` keep
|
||||||
|
pinning the same behaviour. There is no second evaluator in
|
||||||
|
this tree: do not add evaluation logic here, change it in
|
||||||
|
``audit_loop_wiring`` and update this adapter's translation.
|
||||||
|
``deploy.py`` remains an operator script, not a release endpoint
|
||||||
(see ``docs/release-gate-and-ledger-anchor.md``).
|
(see ``docs/release-gate-and-ledger-anchor.md``).
|
||||||
|
|
||||||
What this module IS:
|
What this module IS:
|
||||||
|
|
||||||
- A pure, read-only evaluator: ``evaluate_release`` re-reads the
|
- A pure, read-only adapter: ``evaluate_release`` delegates to
|
||||||
hash chain through the SAME reader the merge endpoint uses
|
the wiring evaluator, which re-reads the hash chain through
|
||||||
(``cloud/audit_wiring.pushback_block_for_task``) and ANDs it
|
the SAME reader the merge endpoint uses and ANDs it with a
|
||||||
with a FRESH hard-gate evaluation supplied by the caller. It
|
FRESH hard-gate evaluation supplied by the caller. It never
|
||||||
never appends to the chain, never calls ``AuthStore.audit``
|
appends to the chain, never calls ``AuthStore.audit``
|
||||||
(which writes a chain row in the same transaction), never
|
(which writes a chain row in the same transaction), never
|
||||||
commits, and never deploys anything. Checking must not move
|
commits, and never deploys anything. Checking must not move
|
||||||
the chain head: otherwise the check itself would invalidate
|
the chain head: otherwise the check itself would invalidate
|
||||||
the external anchor taken before it.
|
the external anchor taken before it.
|
||||||
- The contract the future endpoint must implement. The endpoint
|
- The contract the live endpoint implements. The endpoint
|
||||||
itself (route, HTTP plumbing, task-status transition, actual
|
(``POST /tasks/{id}/release`` in ``handler_lifecycle``) calls
|
||||||
publication) is NOT built here: building a live endpoint in
|
the wiring evaluator directly and maps its decision to HTTP
|
||||||
this track would be exactly the "new live release endpoint
|
exactly as documented in the companion doc; this module only
|
||||||
touching production" the task forbids. The endpoint, when it
|
adapts that decision into the legacy shape for its contract
|
||||||
is approved and built, calls this evaluator and maps its
|
tests, it is not a second evaluation path.
|
||||||
decision to HTTP exactly as documented in the companion doc.
|
|
||||||
|
|
||||||
What this module is NOT:
|
What this module is NOT:
|
||||||
|
|
||||||
- It is not a merge gate. Merge (``POST /tasks/{id}/merge``)
|
- It is not a merge gate. Merge (``POST /tasks/{id}/merge``)
|
||||||
and release are separate decisions with separate re-checks:
|
and release are separate decisions with separate re-checks:
|
||||||
a merge-time 200 is never a release pass. The future endpoint
|
a merge-time 200 is never a release pass. The live endpoint
|
||||||
must call this evaluator at release time, with fresh evidence,
|
calls the wiring evaluator at release time, with fresh
|
||||||
even if the task was merged minutes ago.
|
evidence, even if the task was merged minutes ago.
|
||||||
- It is not a deploy gate. ``deploy.py`` deploys the platform
|
- It is not a deploy gate. ``deploy.py`` deploys the platform
|
||||||
itself (host release directory + unit restarts) and is
|
itself (host release directory + unit restarts) and is
|
||||||
deliberately outside this gate; see the companion doc.
|
deliberately outside this gate; see the companion doc.
|
||||||
@@ -101,9 +105,9 @@ tests pin this: corrected-to-pass with no new passing
|
|||||||
conclusion stays blocked at release, exactly as at merge.
|
conclusion stays blocked at release, exactly as at merge.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from cloud.audit_wiring import pushback_block_for_task
|
from cloud.audit_loop_wiring import evaluate_release_gate as _wiring_evaluate
|
||||||
|
|
||||||
# Machine codes (kept as module constants so the future endpoint
|
# Machine codes (kept as module constants so the endpoint
|
||||||
# and its tests spell them identically, never by string literal).
|
# and its tests spell them identically, never by string literal).
|
||||||
CODE_ALLOWED = "release_allowed"
|
CODE_ALLOWED = "release_allowed"
|
||||||
CODE_REQUIRES_MERGED = "release_requires_merged"
|
CODE_REQUIRES_MERGED = "release_requires_merged"
|
||||||
@@ -129,75 +133,68 @@ def _decision(allowed, code, blocked_by, sources, blocks=None,
|
|||||||
|
|
||||||
|
|
||||||
def evaluate_release(task_id, conn, hard_gate=None, task_status=None):
|
def evaluate_release(task_id, conn, hard_gate=None, task_status=None):
|
||||||
"""Decide whether a task may be released, right now.
|
"""Thin compatibility adapter over the single evaluator.
|
||||||
|
|
||||||
``task_id`` is the task whose chain is re-read. ``conn`` is a
|
The sole implementation is
|
||||||
DB connection the caller owns (the evaluator never closes
|
``cloud/audit_loop_wiring.evaluate_release_gate`` (used
|
||||||
it, never commits on it, never writes through it).
|
directly by the live endpoint
|
||||||
``hard_gate`` must be a FRESH evaluation of the hard merge
|
``handler_lifecycle._release_task``). This function only
|
||||||
gate (``cloud/merge_gate_hard.evaluate_hard_merge_gate``)
|
translates that decision into the legacy shape this module
|
||||||
against fresh evidence: ``{"allowed": bool, "blocks": [...]}``.
|
originally returned (``blocked_sources`` as codes, plus
|
||||||
Passing ``None`` blocks with
|
``blocks`` / ``failed_checks`` / ``round`` / ``modules``),
|
||||||
``release_hard_gate_evidence_missing`` -- a merge-time 200,
|
so the contract tests in ``test_release_gate.py`` keep
|
||||||
a cached gate object, or "the task is already merged" is
|
pinning the same behaviour without a second evaluator
|
||||||
never accepted as fresh evidence. ``task_status``, when
|
existing anywhere in the tree.
|
||||||
given, must be ``"merged"``; when omitted, the status check
|
|
||||||
is skipped and the caller owns it (the future endpoint must
|
|
||||||
not omit it).
|
|
||||||
|
|
||||||
Order: chain read first (unavailable short-circuits, because
|
Legacy contract preserved verbatim:
|
||||||
every other verdict would be untrustworthy without it), then
|
|
||||||
task status, then hard gate, then audit pushback. Hard-gate
|
- ``task_status`` omitted skips the status check (the live
|
||||||
and pushback blocks are both collected, so a decision blocked
|
endpoint must never omit it: it always passes the real
|
||||||
by both says so instead of hiding one behind the other.
|
status). Wiring blocks a ``None`` status, so the adapter
|
||||||
|
passes ``"merged"`` as a placeholder ONLY in the omitted
|
||||||
|
case; that placeholder is not a verdict, it is the skip.
|
||||||
|
- ``hard_gate=None`` blocks with
|
||||||
|
``release_hard_gate_evidence_missing``; a merge-time 200
|
||||||
|
is never accepted as fresh evidence.
|
||||||
|
- A failed chain read blocks fail-closed with
|
||||||
|
``release_blocked_state_unavailable``.
|
||||||
|
- The adapter never writes: it delegates to the read-only
|
||||||
|
wiring evaluator and only reshapes its return value.
|
||||||
"""
|
"""
|
||||||
# 1. Re-read the chain, now -- never a cached merge-time value.
|
effective_status = "merged" if task_status is None else task_status
|
||||||
# The reader itself returns "allow" when the audit_ledger
|
|
||||||
# table does not exist yet (no chain ever written, #247 rule,
|
|
||||||
# inherited unchanged). Any other read failure propagates
|
|
||||||
# out of the reader and is fail-closed here.
|
|
||||||
try:
|
try:
|
||||||
pushback = pushback_block_for_task(conn, task_id)
|
decision = _wiring_evaluate(
|
||||||
|
task_id, conn, task_status=effective_status,
|
||||||
|
hard_gate=hard_gate)
|
||||||
except Exception:
|
except Exception:
|
||||||
return _decision(
|
return _decision(
|
||||||
False, CODE_STATE_UNAVAILABLE,
|
False, CODE_STATE_UNAVAILABLE,
|
||||||
"audit_pushback_state_unavailable",
|
"audit_pushback_state_unavailable",
|
||||||
[CODE_STATE_UNAVAILABLE])
|
[CODE_STATE_UNAVAILABLE])
|
||||||
|
if decision.get("code") == CODE_STATE_UNAVAILABLE:
|
||||||
|
return _decision(
|
||||||
|
False, CODE_STATE_UNAVAILABLE,
|
||||||
|
"audit_pushback_state_unavailable",
|
||||||
|
[CODE_STATE_UNAVAILABLE])
|
||||||
|
pushback = decision.get("audit_pushback") or {}
|
||||||
sources = []
|
sources = []
|
||||||
|
for source in decision.get("blocked_sources") or []:
|
||||||
|
if source == "task_status":
|
||||||
|
sources.append(CODE_REQUIRES_MERGED)
|
||||||
|
elif source == "hard_gate":
|
||||||
|
sources.append(
|
||||||
|
CODE_HARD_GATE_EVIDENCE_MISSING if hard_gate is None
|
||||||
|
else CODE_HARD_GATE)
|
||||||
|
elif source == "audit_pushback":
|
||||||
|
sources.append(CODE_AUDIT_PUSHBACK)
|
||||||
blocks = []
|
blocks = []
|
||||||
failed_checks = []
|
if isinstance(hard_gate, dict):
|
||||||
round_no = None
|
|
||||||
modules = {}
|
|
||||||
primary_code = None
|
|
||||||
primary_by = None
|
|
||||||
|
|
||||||
def _note(code, by):
|
|
||||||
nonlocal primary_code, primary_by
|
|
||||||
sources.append(code)
|
|
||||||
if primary_code is None:
|
|
||||||
primary_code = code
|
|
||||||
primary_by = by
|
|
||||||
|
|
||||||
# 2. Task status, when the caller supplies it.
|
|
||||||
if task_status is not None and task_status != "merged":
|
|
||||||
_note(CODE_REQUIRES_MERGED, "task_status")
|
|
||||||
|
|
||||||
# 3. Fresh hard-gate evidence (five checks + protection).
|
|
||||||
if hard_gate is None:
|
|
||||||
_note(CODE_HARD_GATE_EVIDENCE_MISSING, "hard_gate")
|
|
||||||
elif not hard_gate.get("allowed"):
|
|
||||||
blocks = list(hard_gate.get("blocks") or [])
|
blocks = list(hard_gate.get("blocks") or [])
|
||||||
_note(CODE_HARD_GATE, "hard_gate")
|
|
||||||
|
|
||||||
# 4. Audit pushback, re-read above.
|
|
||||||
if pushback.get("release_blocked"):
|
|
||||||
failed_checks = list(pushback.get("failed_checks") or [])
|
|
||||||
round_no = pushback.get("round")
|
|
||||||
modules = dict(pushback.get("modules") or {})
|
|
||||||
_note(CODE_AUDIT_PUSHBACK, "audit_pushback")
|
|
||||||
|
|
||||||
if not sources:
|
if not sources:
|
||||||
return _decision(True, CODE_ALLOWED, None, [])
|
return _decision(True, CODE_ALLOWED, None, [])
|
||||||
return _decision(False, primary_code, primary_by, sources,
|
return _decision(
|
||||||
blocks=blocks, failed_checks=failed_checks,
|
False, decision.get("code"), decision.get("blocked_by"),
|
||||||
round_no=round_no, modules=modules)
|
sources, blocks=blocks,
|
||||||
|
failed_checks=list(pushback.get("failed_checks") or []),
|
||||||
|
round_no=pushback.get("round"),
|
||||||
|
modules=dict(pushback.get("modules") or {}))
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
"""Future release endpoint: contract tests for cloud/release_gate.
|
"""Release gate: contract tests for cloud/release_gate (adapter).
|
||||||
|
|
||||||
These tests pin the CONTRACT, not an endpoint: there is no live
|
These tests pin the CONTRACT, not the endpoint: the live
|
||||||
release endpoint in this codebase (building one here would be
|
endpoint (``POST /tasks/{id}/release``) calls the wiring
|
||||||
the "new live endpoint touching production" this track forbids),
|
evaluator directly, and every test here calls the
|
||||||
so every test calls ``evaluate_release`` directly with a local
|
``release_gate`` adapter ``evaluate_release`` directly with a
|
||||||
SQLite chain. Nothing here deploys, publishes, or touches a
|
local SQLite chain to prove the adapter translates the single
|
||||||
|
wiring implementation into the legacy shape without a second
|
||||||
|
evaluator. Nothing here deploys, publishes, or touches a
|
||||||
production flow; the chain lives in a TemporaryDirectory and is
|
production flow; the chain lives in a TemporaryDirectory and is
|
||||||
deleted with the test.
|
deleted with the test.
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
# 发布端点接线准备 + 账本锚点运维流程
|
# 发布端点接线准备 + 账本锚点运维流程
|
||||||
|
|
||||||
> 本轨性质:**接线准备,不是发布端点本身**。本轨不新建 live
|
> 本轨性质:**接线准备 + 归一说明**。发布端点现已实现,见 §2;本轨不新建其它 live
|
||||||
> 发布端点、不碰生产、不跑 `deploy.py`、不合并、不部署。
|
> 发布端点;本轨不碰生产、不跑 `deploy.py`、不合并、不部署。
|
||||||
> 发布端点是否立项、锚点存哪里、校验周期多长,均属用户侧
|
> 锚点存哪里、校验周期多长,均属用户侧
|
||||||
> 拍板项(见 §6),本文只给推荐值与判断依据,不替用户定。
|
> 拍板项(见 §6),本文只给推荐值与判断依据,不替用户定。
|
||||||
>
|
>
|
||||||
> 基底:堆叠于 PR #247(`feat-audit-wiring`,`217a27f`),
|
> 基底:堆叠于 PR #247(`feat-audit-wiring`,`217a27f`),
|
||||||
> 合并顺序:#209 → #240 → #244 → #241 → #247 → 本轨。
|
> 合并顺序:#209 → #240 → #244 → #241 → #247 → 本轨。
|
||||||
> 本轨代码:`cloud/release_gate.py`(纯只读 evaluator)、
|
> 本轨代码:`cloud/audit_loop_wiring.py` 的 `evaluate_release_gate`(纯只读唯一实现)、
|
||||||
|
> `cloud/release_gate.py`(其薄转发适配器,保留旧契约形状)、
|
||||||
> `cloud/tests/test_release_gate.py`(11 项契约测试)、
|
> `cloud/tests/test_release_gate.py`(11 项契约测试)、
|
||||||
> `tools/ledger_anchor_check.py`(只读校验脚本)、
|
> `tools/ledger_anchor_check.py`(只读校验脚本)、
|
||||||
> `cloud/tests/test_ledger_anchor_check.py`(脚本退出码测试)。
|
> `cloud/tests/test_ledger_anchor_check.py`(脚本退出码测试)。
|
||||||
@@ -22,16 +23,16 @@
|
|||||||
五项之间做 AND:任一阻塞即 409、状态不变,并在响应里附
|
五项之间做 AND:任一阻塞即 409、状态不变,并在响应里附
|
||||||
`audit_pushback` 对象(`merge_blocked` / `release_blocked` /
|
`audit_pushback` 对象(`merge_blocked` / `release_blocked` /
|
||||||
`blocked_by="audit_pushback"` / `round` / `failed_checks`)。
|
`blocked_by="audit_pushback"` / `round` / `failed_checks`)。
|
||||||
- 但本仓库**没有独立的发布端点**:`release_blocked` 今天只是
|
- 发布端点现已实现(`POST /tasks/{id}/release`,见 §2):
|
||||||
随合并响应返回的一个字段,由“未来的发布端点用同一 reader
|
合并放行后、发布前,服务端按 §3 顺序重读一次链、重算
|
||||||
强制”。在那之前,合并放行后、发布前没有任何服务端检查
|
硬门禁、核任务状态;`release_blocked` 不再只是随合并响应
|
||||||
会再读一次链。
|
返回的字段。
|
||||||
- 账本(#241)已有导出头哈希,但**没有库外锚点存放与定期
|
- 账本(#241)已有导出头哈希,但**没有库外锚点存放与定期
|
||||||
校验的运维流程**:头哈希只在导出响应里出现一次,不存下来
|
校验的运维流程**:头哈希只在导出响应里出现一次,不存下来
|
||||||
就等于没有锚点;尾巴删除单靠库内校验发现不了(#241 已明
|
就等于没有锚点;尾巴删除单靠库内校验发现不了(#241 已明
|
||||||
说),必须靠“旧锚点 vs 当前链”的比对。
|
说),必须靠“旧锚点 vs 当前链”的比对。
|
||||||
|
|
||||||
## 2. 未来发布端点接口契约(建议,待立项后实现)
|
## 2. 发布端点接口契约(已实现,唯一实现为 wiring 版)
|
||||||
|
|
||||||
### 2.1 端点
|
### 2.1 端点
|
||||||
|
|
||||||
@@ -39,14 +40,17 @@
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 方法 + 路径 | `POST /tasks/{id}/release` | 与 `POST /tasks/{id}/merge` 对仗;`{id}` 为任务 id,路径风格与现有任务端点一致 |
|
| 方法 + 路径 | `POST /tasks/{id}/release` | 与 `POST /tasks/{id}/merge` 对仗;`{id}` 为任务 id,路径风格与现有任务端点一致 |
|
||||||
| 请求体 | `{}` 或 `{"confirm": true}`(二选一,立项时定) | 本轨不规定体字段;关键在服务端**不信任**体里的任何门禁结论,只信任自己重算的结果 |
|
| 请求体 | `{}` 或 `{"confirm": true}`(二选一,立项时定) | 本轨不规定体字段;关键在服务端**不信任**体里的任何门禁结论,只信任自己重算的结果 |
|
||||||
| 成功 | `200` + 任务对象 + `release_gate` 决策对象 | 决策对象即 `evaluate_release` 的返回值原样返回,不另起一套字段 |
|
| 成功 | `200` + 任务对象 + `release_gate` 决策对象 | 决策对象即 wiring `evaluate_release_gate` 的返回值原样返回,不另起一套字段(`blocked_sources` 为源名,如 `task_status` / `hard_gate` / `audit_pushback`;`release_gate` 适配器的旧形状——`blocked_sources` 为码 + 顶层 `blocks` / `failed_checks` / `round` / `modules`——仅用于 `test_release_gate.py` 的契约测试,不经过端点) |
|
||||||
| 阻塞 | `409` + `{"error": "release blocked", "release_gate": {...}}` | 见 §2.4 的阻塞码;阻塞时**不改任务状态、不发布、不写链** |
|
| 阻塞 | `409` + `{"error": "release blocked", "release_gate": {...}}` | 见 §2.4 的阻塞码;阻塞时**不改任务状态、不发布、不写链** |
|
||||||
| 任务不存在 | `404` | 与 merge 端点一致 |
|
| 任务不存在 | `404` | 与 merge 端点一致 |
|
||||||
| 无权 | `403` | 见 §2.2 |
|
| 无权 | `403` | 见 §2.2 |
|
||||||
|
|
||||||
本轨**没有实现这个端点**:`cloud/release_gate.py` 只有 evaluator,
|
发布端点**已经实现**:路由见 `handler_lifecycle._release_task`
|
||||||
没有路由、没有 HTTP plumbing、没有任务状态变更、没有发布动作。
|
(`POST /tasks/{id}/release`),唯一实现是
|
||||||
契约测试直接测 evaluator(见 §4),不测一个不存在的端点。
|
`audit_loop_wiring.evaluate_release_gate`;`cloud/release_gate.py`
|
||||||
|
只是把 wiring 决策译回旧契约形状的薄转发适配器,不再是
|
||||||
|
第二个 evaluator。契约测试直接测该适配器(见 §4),端点
|
||||||
|
行为另由 `test_server_audit_loop` 等端点测试覆盖。
|
||||||
|
|
||||||
### 2.2 鉴权:与 verify/export 同源、但为写动作
|
### 2.2 鉴权:与 verify/export 同源、但为写动作
|
||||||
|
|
||||||
@@ -66,9 +70,9 @@
|
|||||||
|
|
||||||
### 2.3 接线点:发布前必做的三件事(顺序即 §3 的求值顺序)
|
### 2.3 接线点:发布前必做的三件事(顺序即 §3 的求值顺序)
|
||||||
|
|
||||||
未来端点在执行任何发布动作之前,必须按顺序做:
|
发布端点在执行任何发布动作之前,必须按顺序做:
|
||||||
|
|
||||||
1. **重读链**:调 `evaluate_release`,它内部调
|
1. **重读链**:调 wiring 的 `evaluate_release_gate`(经由 `release_gate.evaluate_release` 适配器亦同),它内部调
|
||||||
`cloud/audit_wiring.pushback_block_for_task`(与 merge 端点
|
`cloud/audit_wiring.pushback_block_for_task`(与 merge 端点
|
||||||
同一个 reader)。读到的必须是**发布时刻**的链状态。
|
同一个 reader)。读到的必须是**发布时刻**的链状态。
|
||||||
2. **重算硬门禁**:用**发布时刻的新证据**重新跑
|
2. **重算硬门禁**:用**发布时刻的新证据**重新跑
|
||||||
@@ -89,6 +93,12 @@
|
|||||||
`blocked_by`(首要阻塞源)、`blocked_sources`(全部阻塞源),
|
`blocked_by`(首要阻塞源)、`blocked_sources`(全部阻塞源),
|
||||||
端点必须原样返回整个对象,不能只返回首要码——否则修好
|
端点必须原样返回整个对象,不能只返回首要码——否则修好
|
||||||
一个源后才发现另一个源仍阻塞,运维会被误导。
|
一个源后才发现另一个源仍阻塞,运维会被误导。
|
||||||
|
注意:端点返回的是 wiring 决策(`blocked_sources` 为源名,
|
||||||
|
硬门禁细节在 `hard_gate` 子对象里、审计细节在
|
||||||
|
`audit_pushback` 子对象里);下表“附加字段”列的顶层
|
||||||
|
`blocks` / `failed_checks` / `round` / `modules` 是
|
||||||
|
`release_gate` 适配器的旧契约形状,仅见于契约测试,不见
|
||||||
|
于端点响应。
|
||||||
|
|
||||||
| 阻塞源 | `code` | `blocked_by` | HTTP | 附加字段 | 修复路径 |
|
| 阻塞源 | `code` | `blocked_by` | HTTP | 附加字段 | 修复路径 |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
@@ -149,7 +159,7 @@ reader 返回“无推回状态”(链从未写过),不算读链失败。
|
|||||||
用**唯一阻塞**构造钉住这一点:若纠正本身能解封,该测试
|
用**唯一阻塞**构造钉住这一点:若纠正本身能解封,该测试
|
||||||
必失败(测试注释说明了为何不用“纠正 + 另一失败”的掩盖构造)。
|
必失败(测试注释说明了为何不用“纠正 + 另一失败”的掩盖构造)。
|
||||||
|
|
||||||
## 3. evaluator 求值顺序(`cloud/release_gate.py`)
|
## 3. evaluator 求值顺序(唯一实现:`cloud/audit_loop_wiring.py`;`cloud/release_gate.py` 为薄转发适配器)
|
||||||
|
|
||||||
1. 读链。异常 → `release_blocked_state_unavailable`,短路
|
1. 读链。异常 → `release_blocked_state_unavailable`,短路
|
||||||
(没有可信链读数时,其它结论都不可信)。
|
(没有可信链读数时,其它结论都不可信)。
|
||||||
@@ -168,9 +178,8 @@ reader 返回“无推回状态”(链从未写过),不算读链失败。
|
|||||||
|
|
||||||
## 4. 契约测试(`cloud/tests/test_release_gate.py`,11 项)
|
## 4. 契约测试(`cloud/tests/test_release_gate.py`,11 项)
|
||||||
|
|
||||||
测试只测 evaluator + 本地 SQLite 链(`TemporaryDirectory`,
|
测试只测适配器 + 本地 SQLite 链(`TemporaryDirectory`,
|
||||||
跑完删除),**不部署、不调用生产发布流程**(生产本就没有
|
跑完删除),**不部署、不调用生产发布流程**。覆盖:
|
||||||
发布端点可调)。覆盖:
|
|
||||||
|
|
||||||
| 测试 | 断言 |
|
| 测试 | 断言 |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -238,7 +247,7 @@ reader 返回“无推回状态”(链从未写过),不算读链失败。
|
|||||||
| 触发 | 动作 |
|
| 触发 | 动作 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 每日定时(cron/定时任务,运维账号) | 取最新锚点 → 全链校验 + 前缀比对;通过则取新锚点(轮换见 §5.4) |
|
| 每日定时(cron/定时任务,运维账号) | 取最新锚点 → 全链校验 + 前缀比对;通过则取新锚点(轮换见 §5.4) |
|
||||||
| 每次发布前(未来发布端点立项后) | 发布流程先跑一次锚点校验;校验失败即发布 409(与 §2.4 的链读失败同级处理,属未来端点行为,本轨不实现) |
|
| 每次发布前 | 发布流程先跑一次锚点校验;校验失败即发布 409(与 §2.4 的链读失败同级处理,属运维流程接线,不在本轨代码内自动执行) |
|
||||||
| 每次合并门禁异常/人工介入后 | 运维手动跑一次,确认链未被介入改动 |
|
| 每次合并门禁异常/人工介入后 | 运维手动跑一次,确认链未被介入改动 |
|
||||||
| 锚点轮换时 | 新锚点必须先通过旧锚点的前缀校验,才能成为“当前锚点”(见 §5.4) |
|
| 锚点轮换时 | 新锚点必须先通过旧锚点的前缀校验,才能成为“当前锚点”(见 §5.4) |
|
||||||
|
|
||||||
@@ -259,8 +268,8 @@ reader 返回“无推回状态”(链从未写过),不算读链失败。
|
|||||||
任务日志与运维值班渠道留一条可查记录,不能只打印到
|
任务日志与运维值班渠道留一条可查记录,不能只打印到
|
||||||
没人看的日志文件里)。
|
没人看的日志文件里)。
|
||||||
- **是否冻结**:校验失败期间,**发布立即冻结**(不发布;
|
- **是否冻结**:校验失败期间,**发布立即冻结**(不发布;
|
||||||
未来发布端点立项后以校验失败作 409 条件之一;立项前由
|
发布端点以校验失败作 409 条件之一(接线见上);校验流程
|
||||||
运维人工冻结发布操作)。合并是否冻结由当值全局管理员
|
未跑时由运维人工冻结发布操作)。合并是否冻结由当值全局管理员
|
||||||
当场决定并记录——本轨不以代码自动冻结合并(那会是
|
当场决定并记录——本轨不以代码自动冻结合并(那会是
|
||||||
新的门禁语义,须另行立项),但 runbook 要求管理员在
|
新的门禁语义,须另行立项),但 runbook 要求管理员在
|
||||||
收到告警后 1 个工作日内给出“冻结合并与否”的书面决定,
|
收到告警后 1 个工作日内给出“冻结合并与否”的书面决定,
|
||||||
@@ -357,7 +366,7 @@ Postgres URL)。输出为单 JSON 对象,便于定时任务解析。
|
|||||||
它部署的是**平台本身**(服务端代码),不是某个任务的
|
它部署的是**平台本身**(服务端代码),不是某个任务的
|
||||||
合并产物;它没有任务 id、没有调用方提交的门禁证据、
|
合并产物;它没有任务 id、没有调用方提交的门禁证据、
|
||||||
不走服务端 HTTP 鉴权(走 SSH deploy key),与
|
不走服务端 HTTP 鉴权(走 SSH deploy key),与
|
||||||
`POST /tasks/{id}/merge` / 未来 `POST /tasks/{id}/release`
|
`POST /tasks/{id}/merge` / `POST /tasks/{id}/release`
|
||||||
根本不在同一层。
|
根本不在同一层。
|
||||||
- **为什么不在门禁内**:门禁的客体是“任务能否合并/发布”,
|
- **为什么不在门禁内**:门禁的客体是“任务能否合并/发布”,
|
||||||
deploy.py 的客体是“平台代码能否上线”。把 deploy.py 绑到
|
deploy.py 的客体是“平台代码能否上线”。把 deploy.py 绑到
|
||||||
@@ -369,29 +378,30 @@ Postgres URL)。输出为单 JSON 对象,便于定时任务解析。
|
|||||||
1. 部署完成后,运维手动跑一次 §5.2 的锚点校验(部署不
|
1. 部署完成后,运维手动跑一次 §5.2 的锚点校验(部署不
|
||||||
应改链;若校验失败,按 §5.3 处理,与部署成功与否分开判);
|
应改链;若校验失败,按 §5.3 处理,与部署成功与否分开判);
|
||||||
2. 部署说明里写明“部署 ≠ 任务发布”:推新平台版本不等于
|
2. 部署说明里写明“部署 ≠ 任务发布”:推新平台版本不等于
|
||||||
发布任何任务,任务发布仍走未来发布端点。
|
发布任何任务,任务发布仍走 `POST /tasks/{id}/release`。
|
||||||
- 若未来有人提议“deploy.py 发布前查链”,须先回答:查哪
|
- 若未来有人提议“deploy.py 发布前查链”,须先回答:查哪
|
||||||
个任务的链?以谁的身份?回滚时查不查?答不上来就不纳入
|
个任务的链?以谁的身份?回滚时查不查?答不上来就不纳入
|
||||||
——本轨已把这三个问题写进 PR 正文,供立项时复核。
|
——本轨已把这三个问题写进 PR 正文,供立项时复核。
|
||||||
|
|
||||||
## 7. 未做项与诚实边界(本轨明确不声称的)
|
## 7. 未做项与诚实边界(本轨明确不声称的)
|
||||||
|
|
||||||
- 未建发布端点:无路由、无 HTTP 测试、无任务状态变更、
|
- 发布端点已建(路由 + 端点测试在位),但无真实发布动作
|
||||||
无真实发布动作;`release_gate.py` 只是 evaluator + 契约。
|
之外的部署行为;`release_gate.py` 只是 wiring 的薄转发
|
||||||
|
适配器 + 契约,不再是独立 evaluator。
|
||||||
- 未部署、未跑 `deploy.py`、未动生产、主机 env、系统 key、
|
- 未部署、未跑 `deploy.py`、未动生产、主机 env、系统 key、
|
||||||
sub2api、生产网关、仓库可见性、GitHub App 权限、生产容器。
|
sub2api、生产网关、仓库可见性、GitHub App 权限、生产容器。
|
||||||
- 未在真实 PG 上验证:PG 触发器语法、PG 上的链校验与锚点
|
- 未在真实 PG 上验证:PG 触发器语法、PG 上的链校验与锚点
|
||||||
流程,均只以常量/导出模式写清,须生产 PG 手动执行并验证。
|
流程,均只以常量/导出模式写清,须生产 PG 手动执行并验证。
|
||||||
- 未替用户拍板:发布端点是否立项、锚点对象存储选型与
|
- 未替用户拍板:锚点对象存储选型与
|
||||||
账号、校验周期(推荐每日 + 每次发布前)、告警渠道、
|
账号、校验周期(推荐每日 + 每次发布前)、告警渠道、
|
||||||
锚点保留期下限、合并冻结与否的默认策略——见 §8。
|
锚点保留期下限、合并冻结与否的默认策略——见 §8。
|
||||||
- 未改 v1.1 契约、分支保护决策、已有 PR 分支与主 checkout。
|
- 未改 v1.1 契约、分支保护决策、已有 PR 分支与主 checkout。
|
||||||
|
|
||||||
## 8. 用户侧仍需拍板的点(本轨不擅自决定)
|
## 8. 用户侧仍需拍板的点(本轨不擅自决定)
|
||||||
|
|
||||||
1. 发布端点是否立项:立项则按 §2 实现 `POST /tasks/{id}/release`;
|
1. 发布端点已实现(`POST /tasks/{id}/release`,见 §2);
|
||||||
不立项则 `release_blocked` 继续只随合并响应返回,发布仍无
|
剩余待拍板的是锚点校验是否自动接进发布流程(见 §5.2),
|
||||||
服务端强制——该风险须用户知晓。
|
而非端点本身是否立项。
|
||||||
2. 锚点存放:对象存储选型(厂商/桶/账号)与过渡方案是否启用。
|
2. 锚点存放:对象存储选型(厂商/桶/账号)与过渡方案是否启用。
|
||||||
3. 校验周期:推荐每日定时 + 每次发布前;是否接受该周期。
|
3. 校验周期:推荐每日定时 + 每次发布前;是否接受该周期。
|
||||||
4. 告警渠道:校验失败通知全局管理员与当值运维的具体渠道。
|
4. 告警渠道:校验失败通知全局管理员与当值运维的具体渠道。
|
||||||
|
|||||||
Reference in new issue
Block a user