Skip to content

Commit cafbe61

Browse files
Pigbibicodex
andcommitted
ci: use scoped GitHub App for QAR artifacts
Co-Authored-By: Codex <noreply@openai.com>
1 parent e1a46c9 commit cafbe61

3 files changed

Lines changed: 63 additions & 23 deletions

File tree

.github/workflows/publish-m0-research-ledger.yml

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,26 @@ jobs:
4141
with:
4242
python-version: "3.12"
4343

44+
# The App must be installed only on QuantAdvisorResearch with Actions:read.
45+
# The action limits its short-lived token to that one repository and revokes
46+
# it after this job. It is never available to the publishing step below.
47+
- name: Create scoped QAR artifact-reader token
48+
id: qar-artifact-reader
49+
uses: actions/create-github-app-token@v3
50+
with:
51+
app-id: ${{ vars.QAR_ARTIFACT_READER_APP_ID }}
52+
private-key: ${{ secrets.QAR_ARTIFACT_READER_APP_PRIVATE_KEY }}
53+
owner: QuantStrategyLab
54+
repositories: QuantAdvisorResearch
55+
permission-actions: read
56+
4457
- name: Verify immutable successful QAR weekly run
4558
env:
46-
QAR_ARTIFACT_READ_TOKEN: ${{ secrets.QAR_ARTIFACT_READ_TOKEN }}
59+
GH_TOKEN: ${{ steps.qar-artifact-reader.outputs.token }}
4760
run: |
4861
set -euo pipefail
49-
if [ -z "${QAR_ARTIFACT_READ_TOKEN:-}" ]; then
50-
echo "QAR_ARTIFACT_READ_TOKEN is required to read the fixed QAR artifact." >&2
51-
exit 2
52-
fi
5362
run_metadata="${RUNNER_TEMP}/qar-weekly-run.json"
54-
GH_TOKEN="${QAR_ARTIFACT_READ_TOKEN}" gh api \
63+
gh api \
5564
"repos/${QAR_REPOSITORY}/actions/runs/${QAR_RUN_ID}" > "${run_metadata}"
5665
5766
python3 - "${run_metadata}" <<'PY'
@@ -92,11 +101,11 @@ jobs:
92101
93102
- name: Resolve exactly one fixed QAR artifact
94103
env:
95-
QAR_ARTIFACT_READ_TOKEN: ${{ secrets.QAR_ARTIFACT_READ_TOKEN }}
104+
GH_TOKEN: ${{ steps.qar-artifact-reader.outputs.token }}
96105
run: |
97106
set -euo pipefail
98107
artifacts_metadata="${RUNNER_TEMP}/qar-weekly-artifacts.json"
99-
GH_TOKEN="${QAR_ARTIFACT_READ_TOKEN}" gh api \
108+
gh api \
100109
"repos/${QAR_REPOSITORY}/actions/runs/${QAR_RUN_ID}/artifacts?per_page=100" > "${artifacts_metadata}"
101110
102111
python3 - "${artifacts_metadata}" <<'PY'
@@ -130,12 +139,12 @@ jobs:
130139
131140
- name: Download and verify one M0 source snapshot
132141
env:
133-
QAR_ARTIFACT_READ_TOKEN: ${{ secrets.QAR_ARTIFACT_READ_TOKEN }}
142+
GH_TOKEN: ${{ steps.qar-artifact-reader.outputs.token }}
134143
run: |
135144
set -euo pipefail
136145
artifact_zip="${RUNNER_TEMP}/qar-weekly-artifact.zip"
137146
source_snapshot="${RUNNER_TEMP}/m0-research-source-snapshot.json"
138-
GH_TOKEN="${QAR_ARTIFACT_READ_TOKEN}" gh api --method GET \
147+
gh api --method GET \
139148
"repos/${QAR_REPOSITORY}/actions/artifacts/${QAR_ARTIFACT_ID}/zip" \
140149
> "${artifact_zip}"
141150

docs/m0_research_publisher_envelope_contract.md

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,8 @@ deployment branch 也必须只允许 `main`;从其他 ref 手动 dispatch 时
105105
- artifact:`weekly-model-recommendations`
106106
- artifact 内唯一命名为 `m0_research_source_snapshot_YYYY-MM-DD.json` 的文件。
107107

108-
在下载前,workflow 用专用的 `QAR_ARTIFACT_READ_TOKEN` 验证 run ID、成功状态、
109-
workflow 身份、来源仓库和 `head_repository``head_branch=main`、可信 event(仅
108+
在下载前,workflow 用 GitHub App 临时安装令牌验证 run ID、成功状态、workflow 身份
109+
来源仓库和 `head_repository``head_branch=main`、可信 event(仅
110110
`schedule``workflow_dispatch`)、immutable `head_sha`,以及 artifact 与该 run 的绑定。下载后,
111111
它拒绝不安全 ZIP 路径、多个或缺失 snapshot、超过 2 MiB 的 snapshot、错误 schema/source
112112
ID 或无效 report digest,并计算**原始 snapshot 字节**的 SHA-256。该 SHA、QAR revision、
@@ -120,10 +120,32 @@ run ID 和 artifact ID 都作为 `source_artifact` metadata 显式传给构建
120120
| variable `M0_RESEARCH_SYNC_URL` | `QSL_M0_RESEARCH_LEDGER_PUBLISH_URL` | HTTPS 研究台账接收地址 |
121121
| secret `M0_RESEARCH_SYNC_TOKEN` | `QSL_M0_RESEARCH_LEDGER_PUBLISH_TOKEN` | 接收端专用 Bearer token |
122122

123-
`QAR_ARTIFACT_READ_TOKEN``M0_RESEARCH_SYNC_TOKEN``M0_RESEARCH_SYNC_URL` 都必须配置
124-
`m0-research-publisher` Environment 中,而不是 repository-level 默认作用域。两个 token
125-
必须是不同的值和不同的最小权限用途:前者只能读取固定 QAR repository 的 Actions run/artifact,
126-
后者只能向 M0 接收端发布封套;不得复用、互相授予或写入运行时/平台配置。
123+
`m0-research-publisher` Environment 还必须保存 GitHub App 配置,而不是任何长期 QAR PAT:
124+
125+
| GitHub 配置 | 用途 |
126+
| --- | --- |
127+
| variable `QAR_ARTIFACT_READER_APP_ID` | 只安装于 QAR 的 GitHub App ID;不是 secret |
128+
| secret `QAR_ARTIFACT_READER_APP_PRIVATE_KEY` | 该 App 的 PEM private key |
129+
130+
App 只能安装到 `QuantStrategyLab/QuantAdvisorResearch`,repository permission 只能是
131+
`Actions: Read-only`。工作流明确把生成的安装令牌进一步缩小到该 owner/repository 和
132+
`actions:read`,并只注入三个 QAR API 读取步骤;`actions/create-github-app-token` 会在 job
133+
结束时撤销令牌。它从不进入构建/发布步骤、封套、输出或日志。
134+
135+
`M0_RESEARCH_SYNC_TOKEN``M0_RESEARCH_SYNC_URL`、上述 App ID 和 private key 都必须配置
136+
`m0-research-publisher` Environment 中,而不是 repository-level 默认作用域。App 的读取权限
137+
与 M0 发布 token 完全分离;后者只能向 M0 接收端发布封套,二者不得复用、互相授予或写入运行时、
138+
平台配置。
139+
140+
### 一次性创建 GitHub App
141+
142+
组织管理员可在 [QuantStrategyLab GitHub Apps](https://github.com/organizations/QuantStrategyLab/settings/apps/new)
143+
创建私有 App:名称可用 `QSL M0 Research Artifact Reader`,Homepage URL 用
144+
`https://github.com/QuantStrategyLab/QuantRuntimeSettings`,关闭 webhook,唯一 repository permission
145+
选择 `Actions: Read-only`。创建后生成一把 private key,并把**完整 PEM 文件内容**保存为
146+
`QAR_ARTIFACT_READER_APP_PRIVATE_KEY`,把 App 的 App ID 保存为
147+
`QAR_ARTIFACT_READER_APP_ID`。最后通过 App 的 Install 页面选择组织 `QuantStrategyLab`,并且
148+
只选择 `QuantAdvisorResearch`。不要把 PEM 或任何 access token 粘贴到 issue、PR、日志或聊天中。
127149

128150
同一个 `M0_RESEARCH_SYNC_TOKEN` 还必须以**同名、同值的独立 secret**配置到已有的
129151
`runtime-strategy-switch` Environment。它只会在控制台 Worker 的部署 workflow 中被写入

python/tests/test_manual_m0_research_publisher_workflow.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,21 @@ def test_workflow_is_manual_and_binds_one_explicit_successful_qar_artifact(self)
4848
self.assertIn("--source-artifact-id \"${QAR_ARTIFACT_ID}\"", workflow)
4949
self.assertIn("--source-artifact-sha256 \"${M0_SOURCE_SNAPSHOT_SHA256}\"", workflow)
5050

51-
def test_workflow_uses_only_dedicated_read_and_publish_credentials(self):
51+
def test_workflow_uses_only_scoped_app_reader_and_publish_credentials(self):
5252
workflow = WORKFLOW.read_text(encoding="utf-8")
5353

54-
self.assertIn("QAR_ARTIFACT_READ_TOKEN: ${{ secrets.QAR_ARTIFACT_READ_TOKEN }}", workflow)
55-
self.assertIn("GH_TOKEN=\"${QAR_ARTIFACT_READ_TOKEN}\" gh api", workflow)
54+
self.assertIn("uses: actions/create-github-app-token@v3", workflow)
55+
self.assertIn("id: qar-artifact-reader", workflow)
56+
self.assertIn("app-id: ${{ vars.QAR_ARTIFACT_READER_APP_ID }}", workflow)
57+
self.assertIn("private-key: ${{ secrets.QAR_ARTIFACT_READER_APP_PRIVATE_KEY }}", workflow)
58+
self.assertIn("owner: QuantStrategyLab", workflow)
59+
self.assertIn("repositories: QuantAdvisorResearch", workflow)
60+
self.assertIn("permission-actions: read", workflow)
61+
self.assertNotIn("QAR_ARTIFACT_READ_TOKEN", workflow)
62+
self.assertEqual(
63+
workflow.count("GH_TOKEN: ${{ steps.qar-artifact-reader.outputs.token }}"),
64+
3,
65+
)
5666
self.assertIn(
5767
"QSL_M0_RESEARCH_LEDGER_PUBLISH_URL: ${{ vars.M0_RESEARCH_SYNC_URL }}",
5868
workflow,
@@ -72,20 +82,19 @@ def test_workflow_uses_only_dedicated_read_and_publish_credentials(self):
7282
self.assertNotIn("${QSL_M0_RESEARCH_LEDGER_PUBLISH_URL}", workflow)
7383
self.assertNotIn("${QSL_M0_RESEARCH_LEDGER_PUBLISH_TOKEN}", workflow)
7484

75-
read_token_steps = workflow.count("QAR_ARTIFACT_READ_TOKEN: ${{ secrets.QAR_ARTIFACT_READ_TOKEN }}")
76-
self.assertEqual(read_token_steps, 3)
7785
self.assertEqual(workflow.count("QSL_M0_RESEARCH_LEDGER_PUBLISH_URL: ${{ vars.M0_RESEARCH_SYNC_URL }}"), 1)
7886
self.assertEqual(workflow.count("QSL_M0_RESEARCH_LEDGER_PUBLISH_TOKEN: ${{ secrets.M0_RESEARCH_SYNC_TOKEN }}"), 1)
7987
build_step = workflow.split("- name: Build and publish verified no-order M0 ledger", maxsplit=1)[1]
80-
self.assertNotIn("QAR_ARTIFACT_READ_TOKEN", build_step)
88+
self.assertNotIn("qar-artifact-reader.outputs.token", build_step)
89+
self.assertNotIn("QAR_ARTIFACT_READER_APP_PRIVATE_KEY", build_step)
8190

8291
def test_sensitive_values_are_not_emitted_by_workflow_commands(self):
8392
workflow = WORKFLOW.read_text(encoding="utf-8")
8493

8594
for line in workflow.splitlines():
8695
self.assertFalse(
8796
re.search(
88-
r"\b(?:echo|printf)\b.*\$\{?(?:QAR_ARTIFACT_READ_TOKEN|QSL_M0_RESEARCH_LEDGER_PUBLISH_(?:URL|TOKEN))",
97+
r"\b(?:echo|printf)\b.*\$\{?(?:QAR_ARTIFACT_READER_APP_PRIVATE_KEY|QSL_M0_RESEARCH_LEDGER_PUBLISH_(?:URL|TOKEN))",
8998
line,
9099
),
91100
line,

0 commit comments

Comments
 (0)