Skip to content

Commit 34baea4

Browse files
Pigbibicodex
andcommitted
ci: add manual verified M0 publisher
Co-Authored-By: Codex <noreply@openai.com>
1 parent 17e7f2a commit 34baea4

3 files changed

Lines changed: 322 additions & 0 deletions

File tree

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
name: Publish verified M0 research ledger
2+
3+
# This deliberately has no push, schedule, or repository-dispatch trigger.
4+
# An operator must select one already-successful QAR weekly run by its immutable
5+
# run ID. This workflow only transfers a research-only ledger to the console;
6+
# it has no strategy, platform, runtime, or order access.
7+
on:
8+
workflow_dispatch:
9+
inputs:
10+
qar_run_id:
11+
description: "Required successful QAR Weekly Intelligent Advisory Review run ID; this workflow never selects the latest run."
12+
required: true
13+
type: string
14+
15+
permissions:
16+
contents: read
17+
18+
concurrency:
19+
group: publish-m0-research-ledger-${{ inputs.qar_run_id }}
20+
cancel-in-progress: false
21+
22+
jobs:
23+
publish-verified-research-ledger:
24+
runs-on: ubuntu-latest
25+
timeout-minutes: 10
26+
env:
27+
QAR_REPOSITORY: QuantStrategyLab/QuantAdvisorResearch
28+
QAR_WEEKLY_WORKFLOW_ID: "285971223"
29+
QAR_WEEKLY_ARTIFACT_NAME: weekly-model-recommendations
30+
QAR_RUN_ID: ${{ inputs.qar_run_id }}
31+
steps:
32+
- name: Checkout publisher revision
33+
uses: actions/checkout@v6
34+
35+
- name: Set up Python
36+
uses: actions/setup-python@v6
37+
with:
38+
python-version: "3.12"
39+
40+
- name: Verify immutable successful QAR weekly run
41+
env:
42+
QAR_ARTIFACT_READ_TOKEN: ${{ secrets.QAR_ARTIFACT_READ_TOKEN }}
43+
run: |
44+
set -euo pipefail
45+
if [ -z "${QAR_ARTIFACT_READ_TOKEN:-}" ]; then
46+
echo "QAR_ARTIFACT_READ_TOKEN is required to read the fixed QAR artifact." >&2
47+
exit 2
48+
fi
49+
run_metadata="${RUNNER_TEMP}/qar-weekly-run.json"
50+
GH_TOKEN="${QAR_ARTIFACT_READ_TOKEN}" gh api \
51+
"repos/${QAR_REPOSITORY}/actions/runs/${QAR_RUN_ID}" > "${run_metadata}"
52+
53+
python3 - "${run_metadata}" <<'PY'
54+
import json
55+
import os
56+
import re
57+
import sys
58+
from pathlib import Path
59+
60+
metadata = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
61+
run_id = os.environ["QAR_RUN_ID"]
62+
expected_repository = os.environ["QAR_REPOSITORY"]
63+
expected_workflow_id = int(os.environ["QAR_WEEKLY_WORKFLOW_ID"])
64+
if not re.fullmatch(r"[1-9][0-9]{0,19}", run_id):
65+
raise SystemExit("qar_run_id must be one explicit positive GitHub Actions run ID")
66+
if metadata.get("id") != int(run_id):
67+
raise SystemExit("QAR run metadata ID mismatch")
68+
if metadata.get("status") != "completed" or metadata.get("conclusion") != "success":
69+
raise SystemExit("QAR run must already be completed successfully")
70+
if metadata.get("workflow_id") != expected_workflow_id:
71+
raise SystemExit("QAR run is not the fixed Weekly Intelligent Advisory Review workflow")
72+
repository = metadata.get("repository")
73+
if not isinstance(repository, dict) or repository.get("full_name") != expected_repository:
74+
raise SystemExit("QAR run repository mismatch")
75+
revision = metadata.get("head_sha")
76+
if not isinstance(revision, str) or not re.fullmatch(r"[0-9a-f]{40}", revision):
77+
raise SystemExit("QAR run head revision is invalid")
78+
with Path(os.environ["GITHUB_ENV"]).open("a", encoding="utf-8") as env_file:
79+
env_file.write(f"QAR_SOURCE_REVISION={revision}\n")
80+
PY
81+
82+
- name: Resolve exactly one fixed QAR artifact
83+
env:
84+
QAR_ARTIFACT_READ_TOKEN: ${{ secrets.QAR_ARTIFACT_READ_TOKEN }}
85+
run: |
86+
set -euo pipefail
87+
artifacts_metadata="${RUNNER_TEMP}/qar-weekly-artifacts.json"
88+
GH_TOKEN="${QAR_ARTIFACT_READ_TOKEN}" gh api \
89+
"repos/${QAR_REPOSITORY}/actions/runs/${QAR_RUN_ID}/artifacts?per_page=100" > "${artifacts_metadata}"
90+
91+
python3 - "${artifacts_metadata}" <<'PY'
92+
import json
93+
import os
94+
from pathlib import Path
95+
96+
payload = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
97+
artifacts = payload.get("artifacts")
98+
if not isinstance(artifacts, list):
99+
raise SystemExit("QAR artifact listing is invalid")
100+
expected_name = os.environ["QAR_WEEKLY_ARTIFACT_NAME"]
101+
candidates = [
102+
artifact for artifact in artifacts
103+
if isinstance(artifact, dict)
104+
and artifact.get("name") == expected_name
105+
and artifact.get("expired") is False
106+
]
107+
if len(candidates) != 1:
108+
raise SystemExit("QAR run must expose exactly one unexpired fixed weekly artifact")
109+
artifact = candidates[0]
110+
artifact_id = artifact.get("id")
111+
if not isinstance(artifact_id, int) or artifact_id <= 0:
112+
raise SystemExit("QAR artifact ID is invalid")
113+
workflow_run = artifact.get("workflow_run")
114+
if not isinstance(workflow_run, dict) or workflow_run.get("id") != int(os.environ["QAR_RUN_ID"]):
115+
raise SystemExit("QAR artifact workflow-run binding mismatch")
116+
with Path(os.environ["GITHUB_ENV"]).open("a", encoding="utf-8") as env_file:
117+
env_file.write(f"QAR_ARTIFACT_ID={artifact_id}\n")
118+
PY
119+
120+
- name: Download and verify one M0 source snapshot
121+
env:
122+
QAR_ARTIFACT_READ_TOKEN: ${{ secrets.QAR_ARTIFACT_READ_TOKEN }}
123+
run: |
124+
set -euo pipefail
125+
artifact_zip="${RUNNER_TEMP}/qar-weekly-artifact.zip"
126+
source_snapshot="${RUNNER_TEMP}/m0-research-source-snapshot.json"
127+
GH_TOKEN="${QAR_ARTIFACT_READ_TOKEN}" gh api --method GET \
128+
"repos/${QAR_REPOSITORY}/actions/artifacts/${QAR_ARTIFACT_ID}/zip" \
129+
> "${artifact_zip}"
130+
131+
python3 - "${artifact_zip}" "${source_snapshot}" <<'PY'
132+
import hashlib
133+
import json
134+
import os
135+
import re
136+
import sys
137+
import zipfile
138+
from pathlib import Path
139+
140+
archive_path = Path(sys.argv[1])
141+
output_path = Path(sys.argv[2])
142+
snapshot_pattern = re.compile(
143+
r"(?:[^/]+/)*m0_research_source_snapshot_[0-9]{4}-[0-9]{2}-[0-9]{2}\.json"
144+
)
145+
try:
146+
with zipfile.ZipFile(archive_path) as archive:
147+
matches = []
148+
for info in archive.infolist():
149+
name = info.filename
150+
if name.startswith(("/", "\\")) or "\\" in name or ".." in Path(name).parts:
151+
raise SystemExit("QAR artifact contains an unsafe archive member path")
152+
if not info.is_dir() and snapshot_pattern.fullmatch(name):
153+
matches.append(info)
154+
if len(matches) != 1:
155+
raise SystemExit("QAR artifact must contain exactly one dated M0 source snapshot")
156+
source_info = matches[0]
157+
if source_info.file_size <= 0 or source_info.file_size > 2 * 1024 * 1024:
158+
raise SystemExit("QAR M0 source snapshot size is invalid")
159+
raw = archive.read(source_info)
160+
except (OSError, zipfile.BadZipFile) as exc:
161+
raise SystemExit("QAR artifact ZIP is invalid") from exc
162+
163+
try:
164+
snapshot = json.loads(raw.decode("utf-8"))
165+
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
166+
raise SystemExit("QAR M0 source snapshot is not JSON") from exc
167+
if not isinstance(snapshot, dict):
168+
raise SystemExit("QAR M0 source snapshot must be a JSON object")
169+
if snapshot.get("schema_version") != "qsl_m0_research_source_snapshot.v1":
170+
raise SystemExit("QAR M0 source snapshot schema mismatch")
171+
if snapshot.get("source_id") != "quant-advisor-research":
172+
raise SystemExit("QAR M0 source snapshot source ID mismatch")
173+
digest = snapshot.get("source_report_digest")
174+
if not isinstance(digest, str) or not re.fullmatch(r"[0-9a-f]{64}", digest):
175+
raise SystemExit("QAR M0 source report digest is invalid")
176+
output_path.write_bytes(raw)
177+
sha256 = hashlib.sha256(raw).hexdigest()
178+
with Path(os.environ["GITHUB_ENV"]).open("a", encoding="utf-8") as env_file:
179+
env_file.write(f"M0_SOURCE_SNAPSHOT_PATH={output_path}\n")
180+
env_file.write(f"M0_SOURCE_SNAPSHOT_SHA256={sha256}\n")
181+
PY
182+
183+
- name: Build and publish verified no-order M0 ledger
184+
env:
185+
QSL_M0_RESEARCH_LEDGER_PUBLISH_URL: ${{ vars.M0_RESEARCH_SYNC_URL }}
186+
QSL_M0_RESEARCH_LEDGER_PUBLISH_TOKEN: ${{ secrets.M0_RESEARCH_SYNC_TOKEN }}
187+
run: |
188+
set -euo pipefail
189+
if [ -z "${QSL_M0_RESEARCH_LEDGER_PUBLISH_URL:-}" ] || [ -z "${QSL_M0_RESEARCH_LEDGER_PUBLISH_TOKEN:-}" ]; then
190+
echo "The dedicated M0 publication URL variable and token secret are required." >&2
191+
exit 2
192+
fi
193+
python3 python/scripts/build_m0_research_publisher_envelope.py \
194+
--source-snapshot "${M0_SOURCE_SNAPSHOT_PATH}" \
195+
--output "${RUNNER_TEMP}/m0-research-publisher-envelope.json" \
196+
--source-artifact-repository "${QAR_REPOSITORY}" \
197+
--source-artifact-revision "${QAR_SOURCE_REVISION}" \
198+
--source-artifact-run-id "${QAR_RUN_ID}" \
199+
--source-artifact-id "${QAR_ARTIFACT_ID}" \
200+
--source-artifact-sha256 "${M0_SOURCE_SNAPSHOT_SHA256}" \
201+
--producer-repository "${GITHUB_REPOSITORY}" \
202+
--producer-revision "${GITHUB_SHA}" \
203+
--publish

docs/m0_research_publisher_envelope_contract.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,35 @@ header;它从不写进封套、标准输出、错误信息或日志。该工
8585
发布 endpoint 只是研究资料接收端:接收者仍必须重验 schema、artifact metadata、
8686
`ledger_sha256``ledger.policy``research_only/no_order` 固定值。接收、展示或
8787
排队研究任务都不能构成 P4/P5/P6、Shadow、Paper 或 live 授权。
88+
89+
## 手动发布已验证的 QAR 周报
90+
91+
`.github/workflows/publish-m0-research-ledger.yml` 是唯一的跨仓 M0 发布入口。
92+
它只有 `workflow_dispatch`,不按 push、定时任务或其他 workflow 事件自动运行。操作员
93+
必须输入一个**已经成功完成**`QuantStrategyLab/QuantAdvisorResearch`「Weekly
94+
Intelligent Advisory Review」run ID;它不会检索、猜测或自动采用最新 run。
95+
96+
该入口固定只读以下来源:
97+
98+
- repository:`QuantStrategyLab/QuantAdvisorResearch`
99+
- workflow:`Weekly Intelligent Advisory Review`(GitHub workflow ID `285971223`);
100+
- artifact:`weekly-model-recommendations`
101+
- artifact 内唯一命名为 `m0_research_source_snapshot_YYYY-MM-DD.json` 的文件。
102+
103+
在下载前,workflow 用专用的 `QAR_ARTIFACT_READ_TOKEN` 验证 run ID、成功状态、
104+
workflow 身份、来源仓库、immutable `head_sha`,以及 artifact 与该 run 的绑定。下载后,
105+
它拒绝不安全 ZIP 路径、多个或缺失 snapshot、超过 2 MiB 的 snapshot、错误 schema/source
106+
ID 或无效 report digest,并计算**原始 snapshot 字节**的 SHA-256。该 SHA、QAR revision、
107+
run ID 和 artifact ID 都作为 `source_artifact` metadata 显式传给构建器,构建器会再次验证
108+
字节 SHA 后才生成封套。
109+
110+
工作流只使用两个专用发布值,并映射到构建器固定读取的环境变量:
111+
112+
| GitHub 配置 | 构建器环境变量 | 用途 |
113+
| --- | --- | --- |
114+
| variable `M0_RESEARCH_SYNC_URL` | `QSL_M0_RESEARCH_LEDGER_PUBLISH_URL` | HTTPS 研究台账接收地址 |
115+
| secret `M0_RESEARCH_SYNC_TOKEN` | `QSL_M0_RESEARCH_LEDGER_PUBLISH_TOKEN` | 接收端专用 Bearer token |
116+
117+
URL、发布 token 和 QAR 读取 token 不会写进封套、`GITHUB_STEP_SUMMARY` 或 workflow 输出。该
118+
workflow 不读取运行时、平台、selector、策略或券商配置;其唯一网络写入是构建器在
119+
`--publish` 明确指定时,对上述研究接收地址发送经过校验的 no-order 封套。
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
from __future__ import annotations
2+
3+
import re
4+
import unittest
5+
from pathlib import Path
6+
7+
8+
ROOT = Path(__file__).resolve().parents[2]
9+
WORKFLOW = ROOT / ".github" / "workflows" / "publish-m0-research-ledger.yml"
10+
11+
12+
class ManualM0ResearchPublisherWorkflowTest(unittest.TestCase):
13+
def test_workflow_is_manual_and_binds_one_explicit_successful_qar_artifact(self):
14+
workflow = WORKFLOW.read_text(encoding="utf-8")
15+
16+
self.assertIn("workflow_dispatch:", workflow)
17+
self.assertNotRegex(workflow, r"(?m)^ (?:push|pull_request|schedule|repository_dispatch):")
18+
self.assertRegex(
19+
workflow,
20+
r"(?s)qar_run_id:\n.*?required: true\n.*?type: string",
21+
)
22+
self.assertIn("never selects the latest run", workflow)
23+
self.assertNotIn("gh run list", workflow)
24+
self.assertNotIn("actions/runs?", workflow)
25+
self.assertIn("QAR_REPOSITORY: QuantStrategyLab/QuantAdvisorResearch", workflow)
26+
self.assertIn('QAR_WEEKLY_WORKFLOW_ID: "285971223"', workflow)
27+
self.assertIn("QAR_WEEKLY_ARTIFACT_NAME: weekly-model-recommendations", workflow)
28+
self.assertIn('"repos/${QAR_REPOSITORY}/actions/runs/${QAR_RUN_ID}"', workflow)
29+
self.assertIn("QAR run must already be completed successfully", workflow)
30+
self.assertIn("QAR run is not the fixed Weekly Intelligent Advisory Review workflow", workflow)
31+
self.assertIn("QAR artifact workflow-run binding mismatch", workflow)
32+
self.assertIn("QAR artifact must contain exactly one dated M0 source snapshot", workflow)
33+
self.assertIn("m0_research_source_snapshot_[0-9]{4}-[0-9]{2}-[0-9]{2}", workflow)
34+
self.assertIn("M0_SOURCE_SNAPSHOT_SHA256", workflow)
35+
self.assertIn('> "${artifact_zip}"', workflow)
36+
self.assertNotIn("--output \"${artifact_zip}\"", workflow)
37+
self.assertIn("--source-artifact-revision \"${QAR_SOURCE_REVISION}\"", workflow)
38+
self.assertIn("--source-artifact-run-id \"${QAR_RUN_ID}\"", workflow)
39+
self.assertIn("--source-artifact-id \"${QAR_ARTIFACT_ID}\"", workflow)
40+
self.assertIn("--source-artifact-sha256 \"${M0_SOURCE_SNAPSHOT_SHA256}\"", workflow)
41+
42+
def test_workflow_uses_only_dedicated_read_and_publish_credentials(self):
43+
workflow = WORKFLOW.read_text(encoding="utf-8")
44+
45+
self.assertIn("QAR_ARTIFACT_READ_TOKEN: ${{ secrets.QAR_ARTIFACT_READ_TOKEN }}", workflow)
46+
self.assertIn("GH_TOKEN=\"${QAR_ARTIFACT_READ_TOKEN}\" gh api", workflow)
47+
self.assertIn(
48+
"QSL_M0_RESEARCH_LEDGER_PUBLISH_URL: ${{ vars.M0_RESEARCH_SYNC_URL }}",
49+
workflow,
50+
)
51+
self.assertIn(
52+
"QSL_M0_RESEARCH_LEDGER_PUBLISH_TOKEN: ${{ secrets.M0_RESEARCH_SYNC_TOKEN }}",
53+
workflow,
54+
)
55+
self.assertIn("build_m0_research_publisher_envelope.py", workflow)
56+
self.assertIn("--publish", workflow)
57+
self.assertNotIn("gh workflow run", workflow)
58+
self.assertNotIn("runtime_settings.py", workflow)
59+
self.assertNotIn("platform-config", workflow)
60+
self.assertNotIn("manual-strategy-switch", workflow)
61+
self.assertNotIn("broker", workflow.lower())
62+
self.assertNotIn("selector", workflow.lower())
63+
self.assertNotIn("${QSL_M0_RESEARCH_LEDGER_PUBLISH_URL}", workflow)
64+
self.assertNotIn("${QSL_M0_RESEARCH_LEDGER_PUBLISH_TOKEN}", workflow)
65+
66+
read_token_steps = workflow.count("QAR_ARTIFACT_READ_TOKEN: ${{ secrets.QAR_ARTIFACT_READ_TOKEN }}")
67+
self.assertEqual(read_token_steps, 3)
68+
self.assertEqual(workflow.count("QSL_M0_RESEARCH_LEDGER_PUBLISH_URL: ${{ vars.M0_RESEARCH_SYNC_URL }}"), 1)
69+
self.assertEqual(workflow.count("QSL_M0_RESEARCH_LEDGER_PUBLISH_TOKEN: ${{ secrets.M0_RESEARCH_SYNC_TOKEN }}"), 1)
70+
build_step = workflow.split("- name: Build and publish verified no-order M0 ledger", maxsplit=1)[1]
71+
self.assertNotIn("QAR_ARTIFACT_READ_TOKEN", build_step)
72+
73+
def test_sensitive_values_are_not_emitted_by_workflow_commands(self):
74+
workflow = WORKFLOW.read_text(encoding="utf-8")
75+
76+
for line in workflow.splitlines():
77+
self.assertFalse(
78+
re.search(
79+
r"\b(?:echo|printf)\b.*\$\{?(?:QAR_ARTIFACT_READ_TOKEN|QSL_M0_RESEARCH_LEDGER_PUBLISH_(?:URL|TOKEN))",
80+
line,
81+
),
82+
line,
83+
)
84+
85+
86+
if __name__ == "__main__":
87+
unittest.main()

0 commit comments

Comments
 (0)