Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/notify-route-identity.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ stdout 和 GitHub summary 使用 `qsl.notify_route_identity.v1`:固定 status/

`status=ok` / exit 0 仅表示诊断形成结果,**不表示 route 已就绪**。看逐服务 `bot_status=match|mismatch|unknown`、`same_bot_as_contract=true|false|null` 与固定 reason。scope 始终 `project_contract_only`,`canonical_route_status=unknown`。

附加 `traffic_issue` / `traffic_detail` 只给出固定结构原因;依据 [Cloud Run REST v1 TrafficTarget](https://cloud.google.com/run/docs/reference/rest/v1/TrafficTarget) 的“defaults to zero if unspecified”,仅字典行完全缺少 percent 键时沿用 zero skip 并记 `percent_default_zero`(显式 null/bool/string/float 或越界仍拒绝),缺省零本身不证明 positive traffic、批准身份或就绪,也不作宽泛 parser 迁移。

## 尚未验收的入口

`same_chat_as_canonical` 和 `same_topic_as_canonical` 固定 null、对应 status unknown:当前没有批准的中央 private canonical reference,本批不创造新 secret、全局路由规则或 topic 值,也不额外读取 chat secret。既有 runtime chat 为 QSL_GLOBAL 优先 GLOBAL;heartbeat/guard 只遍历 GLOBAL 全列表;中央日报优先 QSL_GLOBAL/GLOBAL/plugin 且只取首 ID。它们需要按各自原 selector 在受保护比较边界取得三态证据,不能重新统一解析来使其匹配。
Expand Down
42 changes: 40 additions & 2 deletions scripts/notify_route_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ def _traffic(service: dict) -> tuple[list[str], str, str]:
serving = []
total = 0
for row in rows:
percent = row.get("percent") if isinstance(row, dict) else None
percent = row.get("percent", 0) if isinstance(row, dict) else None
if type(percent) is not int or not 0 <= percent <= 100:
return [], "unknown", "traffic_invalid"
if percent == 0:
Expand All @@ -180,6 +180,42 @@ def _traffic(service: dict) -> tuple[list[str], str, str]:
return serving, "single" if len(serving) == 1 else "split", "none"


def _traffic_shape(service: dict, reason: str) -> tuple[str, str]:
"""Project fixed structural reasons without changing the parser's decision."""
status = service.get("status")
if not isinstance(status, dict) or "traffic" not in status:
return "traffic_missing", "none"
rows = status["traffic"]
if not isinstance(rows, list):
return "not_list", "none"
detail = "none"
seen = set()
for row in rows:
if not isinstance(row, dict):
return "row_invalid", detail
if "percent" not in row:
# REST v1 TrafficTarget: only an unspecified percent defaults to zero.
detail = "percent_default_zero"
continue
percent = row["percent"]
if type(percent) is not int:
return "percent_type_invalid", detail
if not 0 <= percent <= 100:
return "percent_out_of_range", detail
if percent == 0:
continue
if "revisionName" not in row:
return "revision_missing", detail
name = row["revisionName"]
if not isinstance(name, str) or not re.fullmatch(_SERVICE, name):
return "revision_invalid", detail
if name in seen:
return "duplicate", detail
seen.add(name)
issue = {"traffic_incomplete": "incomplete", "no_positive_traffic": "no_positive", "none": "none"}
return issue.get(reason, "unavailable"), detail


def _unknown_comparison() -> dict:
return {
"bot_status": "unknown", "same_bot_as_contract": None,
Expand All @@ -190,6 +226,7 @@ def _unknown_comparison() -> dict:

def _observe_service(service: str, ordinal: int, project: str, region: str, contract_id: int | None) -> dict:
out = {"service_label": f"service#{ordinal}", "traffic_status": "unknown",
"traffic_issue": "unavailable", "traffic_detail": "unavailable",
"reason_code": "service_describe_failed", **_unknown_comparison(), "revisions": []}
try:
spec = json.loads(_gcloud("run", "services", "describe", service, f"--project={project}",
Expand All @@ -200,7 +237,8 @@ def _observe_service(service: str, ordinal: int, project: str, region: str, cont
out["reason_code"] = "service_metadata_invalid"
return out
serving, traffic_status, reason = _traffic(spec)
out.update(traffic_status=traffic_status, reason_code=reason)
issue, detail = _traffic_shape(spec, reason)
out.update(traffic_status=traffic_status, reason_code=reason, traffic_issue=issue, traffic_detail=detail)
if not serving:
return out
for index, name in enumerate(serving, start=1):
Expand Down
76 changes: 76 additions & 0 deletions tests/test_notify_route_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,3 +391,79 @@ def test_workflow_protects_existing_inputs_before_headers_and_keeps_manual_main_
assert "SOURCE_SHA: ${{ github.sha }}" in protected
assert "::add-mask::" not in protected
assert "run: python3 scripts/notify_route_check.py" in protected


@pytest.mark.parametrize("use_default", [False, True])
def test_documented_zero_target_does_not_hide_positive_serving_revision(tmp_path, use_default):
cloud = FakeCloud()
zero = {"tag": "PRIVATE_ZERO_TAG", "url": "https://PRIVATE_ZERO_URL"}
if not use_default:
zero["percent"] = 0
cloud.service["status"]["traffic"].append(zero)
report, _, _ = observe(tmp_path, cloud)
row = report["services"][0]
assert row["traffic_status"] == "single"
assert row["same_bot_as_contract"] is False
assert row["traffic_issue"] == "none"
assert row["traffic_detail"] == ("percent_default_zero" if use_default else "none")
assert len(row["revisions"]) == 1
assert row["revisions"][0]["same_bot_as_contract"] is False
public = json.dumps(report) + (tmp_path / "summary").read_text()
assert "PRIVATE_ZERO_TAG" not in public and "PRIVATE_ZERO_URL" not in public


@pytest.mark.parametrize("status,issue,detail", [
(None, "traffic_missing", "none"),
({}, "traffic_missing", "none"),
({"traffic": None}, "not_list", "none"),
({"traffic": {}}, "not_list", "none"),
({"traffic": "PRIVATE_BAD_TRAFFIC"}, "not_list", "none"),
({"traffic": [None]}, "row_invalid", "none"),
({"traffic": ["PRIVATE_BAD_ROW"]}, "row_invalid", "none"),
({"traffic": [{"tag": "PRIVATE_ZERO_TAG"}]}, "no_positive", "percent_default_zero"),
({"traffic": [{"revisionName": REVISION}]}, "no_positive", "percent_default_zero"),
({"traffic": [{"percent": None}]}, "percent_type_invalid", "none"),
({"traffic": [{"percent": True}]}, "percent_type_invalid", "none"),
({"traffic": [{"percent": "100"}]}, "percent_type_invalid", "none"),
({"traffic": [{"percent": 100.0}]}, "percent_type_invalid", "none"),
({"traffic": [{"percent": -1}]}, "percent_out_of_range", "none"),
({"traffic": [{"percent": 101}]}, "percent_out_of_range", "none"),
({"traffic": [{"percent": 100}]}, "revision_missing", "none"),
({"traffic": [{"percent": 100, "revisionName": None}]}, "revision_invalid", "none"),
({"traffic": [{"percent": 100, "revisionName": ""}]}, "revision_invalid", "none"),
({"traffic": [{"percent": 100, "revisionName": "PRIVATE_BAD_REVISION/path"}]}, "revision_invalid", "none"),
({"traffic": [{"percent": 50, "revisionName": REVISION},
{"percent": 50, "revisionName": REVISION}]}, "duplicate", "none"),
({"traffic": [{"percent": 50, "revisionName": REVISION}]}, "incomplete", "none"),
({"traffic": []}, "no_positive", "none"),
({"traffic": [{"percent": 0}]}, "no_positive", "none"),
({"traffic": [{"tag": "PRIVATE_ZERO_TAG"}, {"percent": False}]},
"percent_type_invalid", "percent_default_zero"),
({"traffic": [{"percent": False}, {"tag": "PRIVATE_ZERO_TAG"}]}, "percent_type_invalid", "none"),
])
def test_traffic_shape_reason_is_fixed_and_never_turns_bad_metadata_into_match(tmp_path, status, issue, detail):
cloud = FakeCloud()
cloud.service["status"] = status
report, _, _ = observe(tmp_path, cloud)
row = report["services"][0]
assert row["traffic_issue"] == issue
assert row["traffic_detail"] == detail
assert row["same_bot_as_contract"] is None
assert row["bot_status"] == "unknown"
assert row["revisions"] == []
assert not any(args[:3] == ("run", "revisions", "describe") for args in cloud.calls)
summary = (tmp_path / "summary").read_text()
assert '"traffic_issue": "' + issue + '"' in summary
assert '"traffic_detail": "' + detail + '"' in summary
for private in ["PRIVATE_ZERO_TAG", "PRIVATE_ZERO_URL", "PRIVATE_BAD_TRAFFIC", "PRIVATE_BAD_ROW",
"PRIVATE_BAD_REVISION", "PRIVATE_BAD_STATUS"]:
assert private not in json.dumps(report) + summary


def test_unread_service_has_unavailable_shape_instead_of_inferred_default(tmp_path):
cloud = FakeCloud()
cloud.fail.add(("service", SERVICE))
report, _, _ = observe(tmp_path, cloud)
row = report["services"][0]
assert row["traffic_issue"] == row["traffic_detail"] == "unavailable"
assert row["same_bot_as_contract"] is None
Loading