From fc423f0825fb34d42a979fe9f34b49595f9e2702 Mon Sep 17 00:00:00 2001 From: xile611 Date: Thu, 17 Sep 2026 11:35:27 +0800 Subject: [PATCH 1/8] docs: plan manual Bug Server dispatch --- .../plans/2026-09-17-bug-server-dispatch.md | 43 +++++++++++++++++++ .../2026-09-17-bug-server-dispatch-design.md | 24 +++++++++++ 2 files changed, 67 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-17-bug-server-dispatch.md create mode 100644 docs/superpowers/specs/2026-09-17-bug-server-dispatch-design.md diff --git a/docs/superpowers/plans/2026-09-17-bug-server-dispatch.md b/docs/superpowers/plans/2026-09-17-bug-server-dispatch.md new file mode 100644 index 000000000..d8852e5d7 --- /dev/null +++ b/docs/superpowers/plans/2026-09-17-bug-server-dispatch.md @@ -0,0 +1,43 @@ +# Bug Server Manual Dispatch Implementation Plan + +> Execute inline in this task; the workflow design was approved in the conversation. The referenced superpowers execution skills are not installed, so implementation uses the available repository tools. + +**Goal:** Allow maintainers to test an external PR at a reviewed SHA without creating a temporary PR. + +**Architecture:** Add manual-only validation, build, and submission jobs to the existing workflow. Keep the credential-bearing runner separate from PR code and use the workflow commit for trusted scripts. + +**Tech Stack:** GitHub Actions, actions/github-script, Node.js 24, existing TypeScript Bug Server client. + +## Global Constraints + +- Base branch: `develop`; implementation branch: `codex/bugserver-workflow-dispatch`. +- Inputs: `pr_number` and full `head_sha`. +- PR artifacts are data only in the submission job. +- Existing automatic workflows and Bug Server API protocol retain their behavior. + +## Task 1: Validate and resolve the manual target + +Files: `.github/scripts/bug-server-dispatch.cjs`, `.github/scripts/bug-server-dispatch.test.cjs`. + +- [ ] Write Node tests for valid fork PRs, invalid PR numbers, malformed SHAs, stale SHAs, wrong base repository and non-default workflow branches. +- [ ] Run `node --test .github/scripts/bug-server-dispatch.test.cjs` and confirm the missing module fails. +- [ ] Implement `resolveBugServerTarget({ github, context, prNumber, headSha })`, returning `{ prNumber, sha, headRef, prUrl }`. Validate locally before calling `github.rest.pulls.get`; compare the returned PR's repository and current head with the requested target. +- [ ] Rerun the tests. + +## Task 2: Isolate build and submission + +Files: `.github/workflows/bug-server.yml`. + +- [ ] Add the two string inputs, retain existing automatic build behind a non-dispatch condition, and add manual validation/build/submission jobs with read-only repository permissions. +- [ ] Checkout PR code by the validated SHA, verify `git rev-parse HEAD`, build with the existing Rush commands, and upload only the generated bundle. +- [ ] Checkout the trusted client by `github.workflow_sha` in the submission job. Install `node-fetch@2.6.6`, `form-data@4.0.6`, `ts-node@10.9.0`, and `typescript@4.9.5` outside the repository with lifecycle scripts disabled. +- [ ] Run the client through the isolated ts-node executable with explicit CommonJS/esModuleInterop compiler options and reviewed PR metadata; expose the secret only for this command. +- [ ] Run actionlint and simulate the trusted client against mocked API responses, verifying that the bundle is uploaded without execution. + +## Task 3: Document and verify + +Files: `tools/bugserver-trigger/README.md`. + +- [ ] Document UI and CLI invocation, default-branch availability, tested head versus merge semantics, metadata, and result logs. +- [ ] Review the final diff for secret exposure, event regressions and shell interpolation; run `git diff --check`. +- [ ] Record completed checks and deliver the local branch. Do not claim a live Bug Server run before the default-branch workflow exists. diff --git a/docs/superpowers/specs/2026-09-17-bug-server-dispatch-design.md b/docs/superpowers/specs/2026-09-17-bug-server-dispatch-design.md new file mode 100644 index 000000000..2c766b8d8 --- /dev/null +++ b/docs/superpowers/specs/2026-09-17-bug-server-dispatch-design.md @@ -0,0 +1,24 @@ +# Bug Server 手动触发设计 + +## 目标与已确认方案 + +维护者无需创建临时 PR,即可通过 `Bug Server CI` 的 `workflow_dispatch` 验证外部 PR。用户已确认输入 PR 编号与完整 head SHA,并要求构建和持有 token 的上传过程分离。本次从 `develop` 创建 `codex/bugserver-workflow-dispatch` 实现。 + +## 取舍 + +- 采用独立的手动验证、构建、上传 jobs,保留已有 push / pull_request 自动流程。 +- 不改用 `pull_request_target` 执行外部 PR 代码。 +- 不在单个 job 中先构建再注入 token:构建期间启动的进程可能继续存在。 + +## 数据流与边界 + +1. 只允许从默认分支运行手动入口。验证 job 从 workflow 的固定 SHA 检出可信校验脚本。 +2. 校验 PR 编号、40 位十六进制 SHA,并通过 GitHub API 确认 SHA 等于该 PR 当前 head。错误在构建前终止。 +3. 构建 job 在独立 runner 中检出已验证的固定 SHA,不持有 Bug Server secret、不保留 checkout 凭据、不使用共享缓存。只上传 `tools/bugserver-trigger/dist/index.js`。 +4. 上传 job 在新的 runner 中检出 workflow SHA 对应的可信触发脚本。独立安装该脚本所需的固定版本依赖,不执行 PR 的安装脚本或产物。产物仅作为文件上传。 +5. 仅调用触发脚本的 step 注入 `BUG_SERVER_TOKEN`。传给现有脚本的提交、PR ref、源分支信息来自验证 job,避免记录成 develop 的提交。 +6. Actions summary 记录 PR 与测试 SHA;现有脚本继续输出 `scmVersion`、`bundleId` 和用例结果。 + +## 验证 + +使用 Node 内置测试覆盖输入校验、默认分支限制、SHA 不匹配以及 fork PR 成功解析;actionlint 校验 workflow。以本地 mock HTTP 模拟上传、SCM 构建和图片测试,验证可信脚本的独立运行及元数据传递。线上触发需要入口合入默认分支后执行。 From 95514c67ed0c2bee3b1a45641596c168b48a05b2 Mon Sep 17 00:00:00 2001 From: xile611 Date: Thu, 17 Sep 2026 11:39:21 +0800 Subject: [PATCH 2/8] feat: add manual Bug Server validation for fork PRs --- .github/scripts/bug-server-dispatch.cjs | 36 +++++ .github/scripts/bug-server-dispatch.test.cjs | 84 +++++++++++ .github/workflows/bug-server.yml | 139 +++++++++++++++++- .../plans/2026-09-17-bug-server-dispatch.md | 31 ++-- tools/bugserver-trigger/README.md | 43 ++++++ 5 files changed, 320 insertions(+), 13 deletions(-) create mode 100644 .github/scripts/bug-server-dispatch.cjs create mode 100644 .github/scripts/bug-server-dispatch.test.cjs create mode 100644 tools/bugserver-trigger/README.md diff --git a/.github/scripts/bug-server-dispatch.cjs b/.github/scripts/bug-server-dispatch.cjs new file mode 100644 index 000000000..401828e2b --- /dev/null +++ b/.github/scripts/bug-server-dispatch.cjs @@ -0,0 +1,36 @@ +async function resolveBugServerTarget({ github, context, prNumber, headSha }) { + const defaultBranch = context.payload.repository.default_branch; + if (context.ref !== `refs/heads/${defaultBranch}`) { + throw new Error(`Run this workflow from the default branch (${defaultBranch}).`); + } + if (!/^[1-9][0-9]*$/.test(prNumber) || !Number.isSafeInteger(Number(prNumber))) { + throw new Error('PR number must be a positive integer.'); + } + if (headSha.length !== 40 || !/^[0-9a-f]+$/i.test(headSha)) { + throw new Error('Head SHA must be a full 40-character hexadecimal commit SHA.'); + } + + const { data: pull } = await github.rest.pulls.get({ + ...context.repo, + pull_number: Number(prNumber) + }); + const repository = `${context.repo.owner}/${context.repo.repo}`; + if (pull.base.repo.full_name !== repository) { + throw new Error(`PR base repository must be ${repository}.`); + } + const sha = headSha.toLowerCase(); + if (pull.head.sha !== sha) { + throw new Error( + `PR #${prNumber} head changed: expected ${sha}, current ${pull.head.sha}. Review the current head before retrying.` + ); + } + + return { + prNumber: Number(prNumber), + sha, + headRef: pull.head.ref, + prUrl: pull.html_url + }; +} + +module.exports = { resolveBugServerTarget }; diff --git a/.github/scripts/bug-server-dispatch.test.cjs b/.github/scripts/bug-server-dispatch.test.cjs new file mode 100644 index 000000000..fd016e811 --- /dev/null +++ b/.github/scripts/bug-server-dispatch.test.cjs @@ -0,0 +1,84 @@ +const assert = require('node:assert/strict'); +const { test } = require('node:test'); +const { resolveBugServerTarget } = require('./bug-server-dispatch.cjs'); + +const sha = '40be3619d1608aa1d5827f0a465704eeb036a7d3'; + +function fixture(overrides = {}) { + const calls = []; + const context = { + repo: { owner: 'VisActor', repo: 'VRender' }, + ref: 'refs/heads/develop', + payload: { repository: { default_branch: 'develop' } } + }; + const pull = { + base: { repo: { full_name: 'VisActor/VRender' } }, + head: { sha, ref: 'feat/line-render-contribution', repo: { full_name: 'g1f9/VRender' } }, + html_url: 'https://github.com/VisActor/VRender/pull/2128' + }; + const github = { + rest: { + pulls: { + get: async params => { + calls.push(params); + return { data: pull }; + } + } + } + }; + return { args: { github, context, prNumber: '2128', headSha: sha, ...overrides }, calls, pull }; +} + +test('resolves the reviewed fork head, including source metadata', async () => { + const { args, calls } = fixture({ headSha: sha.toUpperCase() }); + assert.deepEqual(await resolveBugServerTarget(args), { + prNumber: 2128, + sha, + headRef: 'feat/line-render-contribution', + prUrl: 'https://github.com/VisActor/VRender/pull/2128' + }); + assert.deepEqual(calls, [{ owner: 'VisActor', repo: 'VRender', pull_number: 2128 }]); +}); + +for (const prNumber of ['', '0', '-1', '1.5', '2128;echo injected', '9007199254740992']) { + test(`rejects invalid PR number ${JSON.stringify(prNumber)} before API access`, async () => { + const { args, calls } = fixture({ prNumber }); + await assert.rejects(resolveBugServerTarget(args), /PR number/); + assert.equal(calls.length, 0); + }); +} + +for (const headSha of ['', '40be3619', 'g'.repeat(40), `${sha}\n`]) { + test(`rejects invalid SHA ${JSON.stringify(headSha)} before API access`, async () => { + const { args, calls } = fixture({ headSha }); + await assert.rejects(resolveBugServerTarget(args), /40-character/); + assert.equal(calls.length, 0); + }); +} + +test('rejects stale approval when the PR has a different head', async () => { + const { args, pull } = fixture(); + pull.head.sha = 'a'.repeat(40); + await assert.rejects(resolveBugServerTarget(args), /head changed/); +}); + +test('rejects a workflow launched from a non-default branch', async () => { + const { args, calls } = fixture(); + args.context.ref = 'refs/heads/feature'; + await assert.rejects(resolveBugServerTarget(args), /default branch/); + assert.equal(calls.length, 0); +}); + +test('rejects a PR belonging to another base repository', async () => { + const { args, pull } = fixture(); + pull.base.repo.full_name = 'someone/VRender'; + await assert.rejects(resolveBugServerTarget(args), /base repository/); +}); + +test('propagates API lookup failures without producing a build target', async () => { + const { args } = fixture(); + args.github.rest.pulls.get = async () => { + throw new Error('Not Found'); + }; + await assert.rejects(resolveBugServerTarget(args), /Not Found/); +}); diff --git a/.github/workflows/bug-server.yml b/.github/workflows/bug-server.yml index 6f2002701..440e14f2a 100644 --- a/.github/workflows/bug-server.yml +++ b/.github/workflows/bug-server.yml @@ -2,6 +2,16 @@ name: Bug Server CI # 这里业务方根据需求设置 on: + workflow_dispatch: + inputs: + pr_number: + description: 'PR number to test (including fork PRs)' + required: true + type: string + head_sha: + description: 'Reviewed PR head commit (full 40-character SHA)' + required: true + type: string push: branches: ['main'] pull_request: @@ -9,10 +19,11 @@ on: jobs: build: + if: github.event_name != 'workflow_dispatch' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node.js 24.x uses: actions/setup-node@v4 with: @@ -20,6 +31,9 @@ jobs: cache: 'npm' cache-dependency-path: './common/config/rush/pnpm-lock.yaml' + - name: Test manual dispatch validation + run: node --test .github/scripts/bug-server-dispatch.test.cjs + - name: Print All Github Environment Variables run: env @@ -48,3 +62,126 @@ jobs: env: BUG_SERVER_TOKEN: ${{ secrets.BUG_SERVER_TOKEN }} run: node ../../common/scripts/install-run-rushx.js ci + + resolve-manual-target: + if: github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + outputs: + sha: ${{ steps.target.outputs.sha }} + pr_number: ${{ steps.target.outputs.pr_number }} + head_ref: ${{ steps.target.outputs.head_ref }} + pr_url: ${{ steps.target.outputs.pr_url }} + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.workflow_sha }} + persist-credentials: false + sparse-checkout: .github/scripts + - name: Validate reviewed PR head + id: target + uses: actions/github-script@v8 + env: + PR_NUMBER: ${{ inputs.pr_number }} + HEAD_SHA: ${{ inputs.head_sha }} + with: + script: | + const { resolveBugServerTarget } = require('./.github/scripts/bug-server-dispatch.cjs'); + const target = await resolveBugServerTarget({ + github, context, + prNumber: process.env.PR_NUMBER, + headSha: process.env.HEAD_SHA + }); + core.setOutput('sha', target.sha); + core.setOutput('pr_number', target.prNumber); + core.setOutput('head_ref', target.headRef); + core.setOutput('pr_url', target.prUrl); + await core.summary + .addHeading('Bug Server manual test') + .addLink(`PR #${target.prNumber}`, target.prUrl) + .addRaw(`\n\nTested head: \`${target.sha}\`\n`) + .write(); + + build-manual-bundle: + needs: resolve-manual-target + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + steps: + # This runner executes PR code. Do not give it Bug Server secrets or shared caches. + - uses: actions/checkout@v4 + with: + ref: ${{ needs.resolve-manual-target.outputs.sha }} + persist-credentials: false + - name: Verify checkout + env: + EXPECTED_SHA: ${{ needs.resolve-manual-target.outputs.sha }} + run: test "$(git rev-parse HEAD)" = "$EXPECTED_SHA" + - uses: actions/setup-node@v4 + with: + node-version: 24.x + - name: Install native deps for node-canvas + run: | + sudo apt-get update + sudo apt-get install -y build-essential pkg-config libcairo2-dev libpango1.0-dev libpng-dev libjpeg-dev libgif-dev librsvg2-dev + - name: Install and build PR bundle + run: | + node common/scripts/install-run-rush.js update --bypass-policy + node common/scripts/install-run-rush.js install --bypass-policy + node common/scripts/install-run-rush.js build -t @internal/bugserver-trigger + - uses: actions/upload-artifact@v4 + with: + name: bug-server-manual-bundle + path: tools/bugserver-trigger/dist/index.js + if-no-files-found: error + retention-days: 7 + + submit-manual-bundle: + needs: [resolve-manual-target, build-manual-bundle] + runs-on: ubuntu-latest + timeout-minutes: 120 + permissions: + contents: read + steps: + # Use the immutable default-branch workflow commit, never scripts from the PR. + - uses: actions/checkout@v4 + with: + ref: ${{ github.workflow_sha }} + persist-credentials: false + sparse-checkout: tools/bugserver-trigger/scripts + - uses: actions/setup-node@v4 + with: + node-version: 24.x + - name: Install isolated trigger client dependencies + run: | + mkdir -p "$RUNNER_TEMP/bug-server-client" + npm install --prefix "$RUNNER_TEMP/bug-server-client" --ignore-scripts --no-audit --no-fund --package-lock=false \ + node-fetch@2.6.6 form-data@4.0.6 ts-node@10.9.0 typescript@4.9.5 + - uses: actions/download-artifact@v4 + with: + name: bug-server-manual-bundle + path: tools/bugserver-trigger/dist + - name: Trigger Bug Server for reviewed PR + working-directory: tools/bugserver-trigger + env: + BUG_SERVER_TOKEN: ${{ secrets.BUG_SERVER_TOKEN }} + NODE_PATH: ${{ runner.temp }}/bug-server-client/node_modules + TEST_SHA: ${{ needs.resolve-manual-target.outputs.sha }} + TEST_REF: refs/pull/${{ needs.resolve-manual-target.outputs.pr_number }}/head + TEST_BRANCH: ${{ needs.resolve-manual-target.outputs.head_ref }} + TEST_PR_URL: ${{ needs.resolve-manual-target.outputs.pr_url }} + run: | + if [ -z "$BUG_SERVER_TOKEN" ]; then + echo '::error::BUG_SERVER_TOKEN is not configured for this repository.' + exit 1 + fi + test -f dist/index.js + printf 'PR: %s\nTested head: `%s`\n' "$TEST_PR_URL" "$TEST_SHA" >> "$GITHUB_STEP_SUMMARY" + env GITHUB_SHA="$TEST_SHA" GITHUB_REF="$TEST_REF" GITHUB_HEAD_REF="$TEST_BRANCH" \ + node "$RUNNER_TEMP/bug-server-client/node_modules/ts-node/dist/bin.js" \ + --transpile-only --skip-project \ + --compiler-options '{"module":"CommonJS","moduleResolution":"node","esModuleInterop":true}' \ + scripts/trigger-test.ts diff --git a/docs/superpowers/plans/2026-09-17-bug-server-dispatch.md b/docs/superpowers/plans/2026-09-17-bug-server-dispatch.md index d8852e5d7..bf9fbe79c 100644 --- a/docs/superpowers/plans/2026-09-17-bug-server-dispatch.md +++ b/docs/superpowers/plans/2026-09-17-bug-server-dispatch.md @@ -19,25 +19,32 @@ Files: `.github/scripts/bug-server-dispatch.cjs`, `.github/scripts/bug-server-dispatch.test.cjs`. -- [ ] Write Node tests for valid fork PRs, invalid PR numbers, malformed SHAs, stale SHAs, wrong base repository and non-default workflow branches. -- [ ] Run `node --test .github/scripts/bug-server-dispatch.test.cjs` and confirm the missing module fails. -- [ ] Implement `resolveBugServerTarget({ github, context, prNumber, headSha })`, returning `{ prNumber, sha, headRef, prUrl }`. Validate locally before calling `github.rest.pulls.get`; compare the returned PR's repository and current head with the requested target. -- [ ] Rerun the tests. +- [x] Write Node tests for valid fork PRs, invalid PR numbers, malformed SHAs, stale SHAs, wrong base repository and non-default workflow branches. +- [x] Run `node --test .github/scripts/bug-server-dispatch.test.cjs` and confirm the missing module fails. +- [x] Implement `resolveBugServerTarget({ github, context, prNumber, headSha })`, returning `{ prNumber, sha, headRef, prUrl }`. Validate locally before calling `github.rest.pulls.get`; compare the returned PR's repository and current head with the requested target. +- [x] Rerun the tests. ## Task 2: Isolate build and submission Files: `.github/workflows/bug-server.yml`. -- [ ] Add the two string inputs, retain existing automatic build behind a non-dispatch condition, and add manual validation/build/submission jobs with read-only repository permissions. -- [ ] Checkout PR code by the validated SHA, verify `git rev-parse HEAD`, build with the existing Rush commands, and upload only the generated bundle. -- [ ] Checkout the trusted client by `github.workflow_sha` in the submission job. Install `node-fetch@2.6.6`, `form-data@4.0.6`, `ts-node@10.9.0`, and `typescript@4.9.5` outside the repository with lifecycle scripts disabled. -- [ ] Run the client through the isolated ts-node executable with explicit CommonJS/esModuleInterop compiler options and reviewed PR metadata; expose the secret only for this command. -- [ ] Run actionlint and simulate the trusted client against mocked API responses, verifying that the bundle is uploaded without execution. +- [x] Add the two string inputs, retain existing automatic build behind a non-dispatch condition, and add manual validation/build/submission jobs with read-only repository permissions. +- [x] Checkout PR code by the validated SHA, verify `git rev-parse HEAD`, build with the existing Rush commands, and upload only the generated bundle. +- [x] Checkout the trusted client by `github.workflow_sha` in the submission job. Install `node-fetch@2.6.6`, `form-data@4.0.6`, `ts-node@10.9.0`, and `typescript@4.9.5` outside the repository with lifecycle scripts disabled. +- [x] Run the client through the isolated ts-node executable with explicit CommonJS/esModuleInterop compiler options and reviewed PR metadata; expose the secret only for this command. +- [x] Run actionlint and simulate the trusted client against mocked API responses, verifying that the bundle is uploaded without execution. ## Task 3: Document and verify Files: `tools/bugserver-trigger/README.md`. -- [ ] Document UI and CLI invocation, default-branch availability, tested head versus merge semantics, metadata, and result logs. -- [ ] Review the final diff for secret exposure, event regressions and shell interpolation; run `git diff --check`. -- [ ] Record completed checks and deliver the local branch. Do not claim a live Bug Server run before the default-branch workflow exists. +- [x] Document UI and CLI invocation, default-branch availability, tested head versus merge semantics, metadata, and result logs. +- [x] Review the final diff for secret exposure, event regressions and shell interpolation; run `git diff --check`. +- [x] Record completed checks and deliver the local branch. Do not claim a live Bug Server run before the default-branch workflow exists. + +## Verification results + +- Node validation tests: 15 passed; also wired into the automatic Bug Server CI job. +- actionlint 1.7.12: passed. Updated the existing checkout v3 to v4 because actionlint rejects its retired runtime. +- Executed the workflow submission shell block in an isolated directory using the exact dependency versions: mock success, photo-test failure and missing-token cases all passed. The mock verified PR metadata and received a bundle that throws if executed; it was only uploaded. +- `git diff --check`: passed. No live Bug Server call was made. diff --git a/tools/bugserver-trigger/README.md b/tools/bugserver-trigger/README.md new file mode 100644 index 000000000..ed14a9ccc --- /dev/null +++ b/tools/bugserver-trigger/README.md @@ -0,0 +1,43 @@ +# Bug Server CI + +`scripts/trigger-test.ts` uploads `dist/index.js`, waits for an SCM build, starts the Bug Server photo tests, and waits for their results. It requires `BUG_SERVER_TOKEN`. + +## Manually test a PR + +After the manual workflow is merged into the repository's default branch (`develop`), maintainers with repository write access can open **Actions → Bug Server CI → Run workflow**. Select **develop**, then enter: + +- `pr_number`: the PR number, including PRs from external forks. +- `head_sha`: the full 40-character SHA of the PR head that you reviewed. + +The equivalent CLI command is: + +```sh +gh workflow run bug-server.yml \ + --repo VisActor/VRender \ + --ref develop \ + -f pr_number=2128 \ + -f head_sha=40be3619d1608aa1d5827f0a465704eeb036a7d3 +``` + +Use the currently reviewed PR head; the example SHA becomes invalid if that PR changes. The workflow rejects non-default workflow branches, malformed inputs and a SHA that differs from the PR's current head. It builds the exact requested **head commit**, not GitHub's generated merge commit. Updates after validation cannot change the commit being built. + +The run appears under Actions; this manual run does not automatically attach a check or comment to the external PR. Its summary records the PR URL and tested head. The **Trigger Bug Server for reviewed PR** step prints `scmVersion`, `bundleId`, and the result counts, which identify the run in Bug Server. A missing token, failed SCM build or failed photo test makes the job fail. + +## Execution boundaries + +The manual workflow uses three separate jobs: + +1. Validate the PR and reviewed SHA using the GitHub API. +2. Build the PR on a runner with read-only repository access, no retained checkout credentials, no shared cache and no Bug Server token. Upload only the generated bundle. +3. On a fresh runner, use the trigger script from the default-branch workflow commit. Install its dependencies separately with npm lifecycle scripts disabled, download the bundle as data, and pass the token only to the API client step. This job never executes the PR bundle or its package scripts. + +Existing push and pull-request automatic runs are unchanged. A fork PR's automatic run still cannot obtain repository secrets; use the manual entry for Bug Server validation. + +## Local validation + +From the repository root: + +```sh +node --test .github/scripts/bug-server-dispatch.test.cjs +actionlint .github/workflows/bug-server.yml +``` From a1d30899caa1257dea859ba456123d9f5e3dc02a Mon Sep 17 00:00:00 2001 From: xile611 Date: Thu, 17 Sep 2026 11:54:27 +0800 Subject: [PATCH 3/8] fix: restrict bug server workflow permissions and cache access --- .../bug-server-cache-policy-check.yml | 17 ++ .github/workflows/bug-server.yml | 9 +- .../2026-09-17-bug-server-security-fix.md | 180 ++++++++++++++++++ 3 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/bug-server-cache-policy-check.yml create mode 100644 docs/superpowers/plans/2026-09-17-bug-server-security-fix.md diff --git a/.github/workflows/bug-server-cache-policy-check.yml b/.github/workflows/bug-server-cache-policy-check.yml new file mode 100644 index 000000000..32f4d1ad0 --- /dev/null +++ b/.github/workflows/bug-server-cache-policy-check.yml @@ -0,0 +1,17 @@ +name: Bug Server cache policy check + +on: + push: + branches: [codex/bugserver-workflow-dispatch] + +permissions: {} + +jobs: + verify: + runs-on: ubuntu-latest + cache-mode: none + steps: + - name: Verify effective cache mode + run: | + printf 'cache mode: %s\n' "${ACTIONS_CACHE_MODE:-unset}" + test "${ACTIONS_CACHE_MODE:-}" = none diff --git a/.github/workflows/bug-server.yml b/.github/workflows/bug-server.yml index 440e14f2a..540639701 100644 --- a/.github/workflows/bug-server.yml +++ b/.github/workflows/bug-server.yml @@ -17,6 +17,9 @@ on: pull_request: branches: ['main', 'develop', 'dev/**'] +permissions: + contents: read + jobs: build: if: github.event_name != 'workflow_dispatch' @@ -108,10 +111,14 @@ jobs: needs: resolve-manual-target runs-on: ubuntu-latest timeout-minutes: 30 + cache-mode: none permissions: contents: read steps: - # This runner executes PR code. Do not give it Bug Server secrets or shared caches. + - name: Verify cache isolation + run: test "${ACTIONS_CACHE_MODE:-}" = none + # PR code runs without Bug Server secrets or cache access. + # cache-mode controls cache tokens independently of GITHUB_TOKEN permissions. - uses: actions/checkout@v4 with: ref: ${{ needs.resolve-manual-target.outputs.sha }} diff --git a/docs/superpowers/plans/2026-09-17-bug-server-security-fix.md b/docs/superpowers/plans/2026-09-17-bug-server-security-fix.md new file mode 100644 index 000000000..1eaa26970 --- /dev/null +++ b/docs/superpowers/plans/2026-09-17-bug-server-security-fix.md @@ -0,0 +1,180 @@ +# Bug Server 手动入口安全修复 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. +> +> 当前环境未安装上述执行技能。后续可在当前任务中顺序执行;本次仅制定计划,不修改 workflow、不触发 CI、不更新远端文档。 + +**Goal:** 修复 PR #2134 的默认分支缓存污染风险和自动构建 token 权限过大问题。 + +**Architecture:** 保留现有校验、构建、提交三个手动 jobs。通过 workflow 顶层 `permissions` 限制 GitHub API 权限,通过构建 job 的 `cache-mode` 独立限制缓存权限;这两类权限需要分别控制。 + +**Tech Stack:** GitHub Actions、Node.js 24、GitHub CodeQL、actionlint、GitHub CLI。 + +## Global Constraints + +- 审查基线:PR #2134,head `95514c67ed0c2bee3b1a45641596c168b48a05b2`。 +- 在现有 `codex/bugserver-workflow-dispatch` 分支追加修复,不重建功能或重写已有提交。 +- 保留输入 `pr_number`、`head_sha`、默认分支限制和固定 SHA 构建。 +- 保留独立 runner 和可信提交脚本;`BUG_SERVER_TOKEN` 仍只注入提交 step。 +- 只收紧本 workflow 权限;不修改仓库全局权限设置、发布流程和 Bug Server API。 +- 不用关闭扫描规则、隐藏告警或移除权限限制来让检查变绿。 +- 这是权限配置修复,不添加仅断言 YAML 文本的单测,也不重跑渲染库全量测试。 + +## 已核实的事实与方案选择 + +1. 仓库当前 `default_workflow_permissions` 为 `write`。旧 `build` job 没有显式权限,三个手动 jobs 已有只读权限。 +2. `workflow_dispatch` 在默认分支运行时默认拥有该分支的缓存写权限;不配置 cache action 不会撤销该权限。 +3. GitHub 官方文档支持 job 级别 `cache-mode: none`,由缓存 token 的作用域实施限制;只设置同名环境变量不等价。 +4. 当前最新版 actionlint 1.7.12 对该字段报 `unexpected key "cache-mode"`。已检查 CodeQL 主线的 `CachePoisoningQuery.qll`:其缓存写权限判断仍只看触发事件,没有考虑 `cache-mode`。 + +首选原生权限配置:改动集中,维护者使用方式不变。把 PR 构建搬到 `pull_request` 工作流、手动入口只消费其 artifact 也能隔离缓存,但需要新增运行记录和产物身份校验,不作为本次首选。仅增加 `permissions: contents: read` 无法修复缓存问题。 + +**兼容性处理原则:** GitHub 服务端与 runner 的实际支持需要先验证;不能承诺添加字段后现有 CodeQL 告警必然自动消失。扫描工具的兼容性问题与安全机制是否生效分别记录。 + +## Task 1:收紧两类权限 + +**Files:** +- Modify: `.github/workflows/bug-server.yml` + +**Interfaces:** +- Consumes: 原有事件、输入、job outputs、artifact 名称和提交脚本。 +- Produces: 所有 job 默认只有 `contents: read`;外部 PR 构建 job 没有缓存读写权限。 + +- [ ] **1. 复核执行时的 PR head 和工作区,防止覆盖后续改动。** + +```sh +git status --short +gh pr view 2134 --repo VisActor/VRender --json headRefOid,headRefName,baseRefName +``` + +- [ ] **2. 在 `on` 与 `jobs` 之间增加顶层权限。** + +```yaml +permissions: + contents: read +``` + +保留 `resolve-manual-target` 的 `contents: read`、`pull-requests: read`,以及另外两个手动 jobs 现有的 `contents: read`。旧 `build` 自动继承顶层只读权限;未声明的其他 API 权限不授予。 + +- [ ] **3. 在 `build-manual-bundle` 中增加 job 级缓存限制。** + +```yaml + cache-mode: none +``` + +将原来的缓存注释替换为: + +```yaml + # PR code runs without Bug Server secrets or cache access. + # cache-mode controls cache tokens independently of GITHUB_TOKEN permissions. +``` + +- [ ] **4. 在该 job 的 checkout 之前增加运行时检查。** + +```yaml + - name: Verify cache isolation + run: test "${ACTIONS_CACHE_MODE:-}" = none +``` + +如果 runner 没有报告 `none`,立即停止,不能继续执行 PR 代码。该检查用于确认平台应用配置,实际权限边界仍是 job 级 `cache-mode`,不是环境变量本身。 + +## Task 2:验证平台支持、扫描结果和功能 + +**Files:** +- Test: `.github/scripts/bug-server-dispatch.test.cjs`(已有测试,不修改) +- Temporary: `.github/workflows/bug-server-cache-policy-check.yml`(验收后删除) +- Modify: 本计划的验收记录 + +**Interfaces:** +- Consumes: Task 1 的 workflow 配置。 +- Produces: GitHub 原生解析与运行证据、权限日志、两条扫描告警的处理结果。 + +- [ ] **1. 运行现有校验,记录 actionlint 版本及完整诊断。** + +```sh +node --test .github/scripts/bug-server-dispatch.test.cjs +actionlint -version +actionlint .github/workflows/bug-server.yml +git diff --check +``` + +预期已有 15 项输入校验测试通过。若 actionlint 仍是 1.7.12,明确记录其对新字段的语法误报;不能把这次检查写成通过,也不能泛化忽略所有语法错误。其余诊断均需解决。 + +- [ ] **2. 在 PR 分支运行不包含 PR 代码和 secret 的平台探针。** + +临时文件的完整内容: + +```yaml +name: Bug Server cache policy check +on: + push: + branches: [codex/bugserver-workflow-dispatch] +permissions: {} +jobs: + verify: + runs-on: ubuntu-latest + cache-mode: none + steps: + - name: Verify effective cache mode + run: | + printf 'cache mode: %s\n' "${ACTIONS_CACHE_MODE:-unset}" + test "${ACTIONS_CACHE_MODE:-}" = none +``` + +在修复实现进入正常提交、推送阶段时运行该探针。它不 checkout、不安装依赖、不调用 Bug Server、不读写缓存。验收要求 GitHub 接受 YAML,且日志输出 `cache mode: none`。保留 run URL,再删除临时 workflow。 + +如果 GitHub 拒绝字段或 runner 不报告 `none`,该方案不具备落地条件,应停止合并;不要删除隔离配置继续执行外部 PR。后续改用 `pull_request` 构建 artifact、手动入口校验其来源后上传的方案,并单独完成该架构的实现计划。 + +- [ ] **3. 复查真实自动构建的权限和两条 CodeQL 告警。** + +```sh +gh pr checks 2134 --repo VisActor/VRender +gh api repos/VisActor/VRender/code-scanning/alerts/45 +gh api repos/VisActor/VRender/code-scanning/alerts/46 +``` + +在最新提交的 `build` job 的 Set up job 日志中,确认 `GITHUB_TOKEN Permissions` 没有写权限。确认缺失权限告警已修复;缓存告警若仍存在,核对该次分析使用的规则及提交 SHA。 + +对于尚未识别 `cache-mode` 的 CodeQL,记录官方权限语义、探针 run URL、实际 workflow 配置和规则源码证据。扫描仍失败时,不将 PR 描述成“全部检查通过”,不自动关闭告警;将残留扫描问题明确交付给维护者评审。若仓库合并规则要求该检查通过,解决工具识别问题或改用上述 PR artifact 方案后再合并,不绕过合并规则。 + +- [ ] **4. 合入默认分支后,用已完整审查的可信 PR head 做首次手动验收。** + +通过原有 Run workflow 表单输入该 PR 编号与完整 SHA。检查:校验通过、缓存隔离检查通过、固定 SHA 构建成功、artifact 上传/下载成功、可信脚本成功触发 Bug Server,summary 中 PR/SHA 正确。 + +这一步验证此前自动 PR CI 不会执行的三个手动 jobs。图片差异按 Bug Server 业务结果记录,与权限配置是否生效分别判断。验收失败时暂停手动入口的使用,修复后再为外部 PR 运行;不放宽权限作为回退。 + +## Task 3:同步维护说明并交付 + +**Files:** +- Modify: `tools/bugserver-trigger/README.md` +- Modify: `docs/superpowers/specs/2026-09-17-bug-server-dispatch-design.md` +- Append correction: `docs/superpowers/plans/2026-09-17-bug-server-dispatch.md` +- Update after verification: [VRender 日常维护文档](https://bytedance.larkoffice.com/wiki/RNbpwz9HZizi1WkQYcZcqnj6n92) + +**Interfaces:** +- Consumes: Task 2 的真实验证结果。 +- Produces: 与实现一致的权限说明和未完成项记录。 + +- [ ] **1. 用具体权限说明替换含糊的“无共享缓存”。** + +README 的构建边界使用以下说明: + +> Build the reviewed PR with read-only repository access and `cache-mode: none`, which denies cache reads and writes independently of `GITHUB_TOKEN`. Verify the runner reports this mode before checking out PR code. Keep checkout credentials disabled and the Bug Server token on the separate submission runner. + +设计文档和飞书文档说明: + +> 手动构建 job 显式禁止缓存读写,执行 PR 代码前确认该设置生效。GitHub API 权限和缓存权限分别控制;不配置缓存步骤并不等于没有缓存权限。 + +在原实现计划的验证结果后补记:此前单测、mock 和 actionlint 通过仅覆盖功能及旧语法检查,未验证缓存权限隔离;此次修复补齐这项边界。 + +- [ ] **2. 交付时列出改动和真实状态。** + +至少记录最新 commit、两条告警结果、自动构建权限日志、缓存模式探针 URL、首次真实手动运行结果。区分“合并前已验证”和“合并后待验证”,不把计划写成已完成结果。 + +## 参考依据 + +- [GitHub job 级 cache-mode](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idcache-mode) +- [GitHub 缓存权限与事件默认值](https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching#controlling-cache-access-with-cache-mode) +- [CodeQL 缓存写权限判断源码](https://github.com/github/codeql/blob/main/actions/ql/lib/codeql/actions/security/CachePoisoningQuery.qll) +- [缓存污染告警](https://github.com/VisActor/VRender/pull/2134#discussion_r4032878435) +- [缺失权限告警](https://github.com/VisActor/VRender/pull/2134#discussion_r4032878451) From f8fe1a07af0a7f05b0f2a5649d15e054fae992f2 Mon Sep 17 00:00:00 2001 From: xile611 Date: Thu, 17 Sep 2026 11:56:20 +0800 Subject: [PATCH 4/8] fix: verify cache mode in the JavaScript action environment --- .github/workflows/bug-server-cache-policy-check.yml | 10 +++++++--- .github/workflows/bug-server.yml | 9 ++++++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/workflows/bug-server-cache-policy-check.yml b/.github/workflows/bug-server-cache-policy-check.yml index 32f4d1ad0..527a9240c 100644 --- a/.github/workflows/bug-server-cache-policy-check.yml +++ b/.github/workflows/bug-server-cache-policy-check.yml @@ -12,6 +12,10 @@ jobs: cache-mode: none steps: - name: Verify effective cache mode - run: | - printf 'cache mode: %s\n' "${ACTIONS_CACHE_MODE:-unset}" - test "${ACTIONS_CACHE_MODE:-}" = none + uses: actions/github-script@v8 + with: + script: | + core.info(`Cache mode: ${process.env.ACTIONS_CACHE_MODE ?? 'unset'}`); + if (process.env.ACTIONS_CACHE_MODE !== 'none') { + core.setFailed('Expected cache-mode: none.'); + } diff --git a/.github/workflows/bug-server.yml b/.github/workflows/bug-server.yml index 540639701..137220b83 100644 --- a/.github/workflows/bug-server.yml +++ b/.github/workflows/bug-server.yml @@ -116,7 +116,14 @@ jobs: contents: read steps: - name: Verify cache isolation - run: test "${ACTIONS_CACHE_MODE:-}" = none + uses: actions/github-script@v8 + with: + script: | + if (process.env.ACTIONS_CACHE_MODE !== 'none') { + core.setFailed('PR builds require cache-mode: none.'); + } else { + core.info('Cache access is disabled for this job.'); + } # PR code runs without Bug Server secrets or cache access. # cache-mode controls cache tokens independently of GITHUB_TOKEN permissions. - uses: actions/checkout@v4 From a3d5f2e6e9844d096f1e5d039632248deeb61f42 Mon Sep 17 00:00:00 2001 From: xile611 Date: Thu, 17 Sep 2026 11:59:27 +0800 Subject: [PATCH 5/8] docs: record bug server security validation and remove probe --- .../bug-server-cache-policy-check.yml | 21 -------- .../plans/2026-09-17-bug-server-dispatch.md | 4 ++ .../2026-09-17-bug-server-security-fix.md | 50 ++++++++++++++----- .../2026-09-17-bug-server-dispatch-design.md | 5 +- tools/bugserver-trigger/README.md | 8 ++- 5 files changed, 52 insertions(+), 36 deletions(-) delete mode 100644 .github/workflows/bug-server-cache-policy-check.yml diff --git a/.github/workflows/bug-server-cache-policy-check.yml b/.github/workflows/bug-server-cache-policy-check.yml deleted file mode 100644 index 527a9240c..000000000 --- a/.github/workflows/bug-server-cache-policy-check.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Bug Server cache policy check - -on: - push: - branches: [codex/bugserver-workflow-dispatch] - -permissions: {} - -jobs: - verify: - runs-on: ubuntu-latest - cache-mode: none - steps: - - name: Verify effective cache mode - uses: actions/github-script@v8 - with: - script: | - core.info(`Cache mode: ${process.env.ACTIONS_CACHE_MODE ?? 'unset'}`); - if (process.env.ACTIONS_CACHE_MODE !== 'none') { - core.setFailed('Expected cache-mode: none.'); - } diff --git a/docs/superpowers/plans/2026-09-17-bug-server-dispatch.md b/docs/superpowers/plans/2026-09-17-bug-server-dispatch.md index bf9fbe79c..32777aad2 100644 --- a/docs/superpowers/plans/2026-09-17-bug-server-dispatch.md +++ b/docs/superpowers/plans/2026-09-17-bug-server-dispatch.md @@ -48,3 +48,7 @@ Files: `tools/bugserver-trigger/README.md`. - actionlint 1.7.12: passed. Updated the existing checkout v3 to v4 because actionlint rejects its retired runtime. - Executed the workflow submission shell block in an isolated directory using the exact dependency versions: mock success, photo-test failure and missing-token cases all passed. The mock verified PR metadata and received a bundle that throws if executed; it was only uploaded. - `git diff --check`: passed. No live Bug Server call was made. + +## Security verification correction (2026-09-17) + +The original tests, mock integration and actionlint run verified functional behavior and workflow syntax, but did not verify cache permissions. Omitting a cache action does not remove the default-branch cache-write capability of a `workflow_dispatch` run. PR #2134 therefore adds explicit workflow-level read-only API permissions and job-level `cache-mode: none`, verified before PR checkout. See the [security fix plan](2026-09-17-bug-server-security-fix.md) for runtime evidence and scanner compatibility limitations. diff --git a/docs/superpowers/plans/2026-09-17-bug-server-security-fix.md b/docs/superpowers/plans/2026-09-17-bug-server-security-fix.md index 1eaa26970..2714f14be 100644 --- a/docs/superpowers/plans/2026-09-17-bug-server-security-fix.md +++ b/docs/superpowers/plans/2026-09-17-bug-server-security-fix.md @@ -2,7 +2,7 @@ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. > -> 当前环境未安装上述执行技能。后续可在当前任务中顺序执行;本次仅制定计划,不修改 workflow、不触发 CI、不更新远端文档。 +> 当前环境未安装上述执行技能。用户已授权执行本计划,使用当前任务和仓库工具顺序完成;实际进度与证据记录在文末。 **Goal:** 修复 PR #2134 的默认分支缓存污染风险和自动构建 token 权限过大问题。 @@ -40,14 +40,14 @@ - Consumes: 原有事件、输入、job outputs、artifact 名称和提交脚本。 - Produces: 所有 job 默认只有 `contents: read`;外部 PR 构建 job 没有缓存读写权限。 -- [ ] **1. 复核执行时的 PR head 和工作区,防止覆盖后续改动。** +- [x] **1. 复核执行时的 PR head 和工作区,防止覆盖后续改动。** ```sh git status --short gh pr view 2134 --repo VisActor/VRender --json headRefOid,headRefName,baseRefName ``` -- [ ] **2. 在 `on` 与 `jobs` 之间增加顶层权限。** +- [x] **2. 在 `on` 与 `jobs` 之间增加顶层权限。** ```yaml permissions: @@ -56,7 +56,7 @@ permissions: 保留 `resolve-manual-target` 的 `contents: read`、`pull-requests: read`,以及另外两个手动 jobs 现有的 `contents: read`。旧 `build` 自动继承顶层只读权限;未声明的其他 API 权限不授予。 -- [ ] **3. 在 `build-manual-bundle` 中增加 job 级缓存限制。** +- [x] **3. 在 `build-manual-bundle` 中增加 job 级缓存限制。** ```yaml cache-mode: none @@ -69,15 +69,24 @@ permissions: # cache-mode controls cache tokens independently of GITHUB_TOKEN permissions. ``` -- [ ] **4. 在该 job 的 checkout 之前增加运行时检查。** +- [x] **4. 在该 job 的 checkout 之前增加运行时检查。** ```yaml - name: Verify cache isolation - run: test "${ACTIONS_CACHE_MODE:-}" = none + uses: actions/github-script@v8 + with: + script: | + if (process.env.ACTIONS_CACHE_MODE !== 'none') { + core.setFailed('PR builds require cache-mode: none.'); + } else { + core.info('Cache access is disabled for this job.'); + } ``` 如果 runner 没有报告 `none`,立即停止,不能继续执行 PR 代码。该检查用于确认平台应用配置,实际权限边界仍是 job 级 `cache-mode`,不是环境变量本身。 +执行中修正了检查载体:runner 的 `NodeScriptActionHandler` 会注入 `ACTIONS_CACHE_MODE`,普通 shell step 不会。不能用原计划的 shell 检查把变量未注入误判为平台不支持。 + ## Task 2:验证平台支持、扫描结果和功能 **Files:** @@ -89,7 +98,7 @@ permissions: - Consumes: Task 1 的 workflow 配置。 - Produces: GitHub 原生解析与运行证据、权限日志、两条扫描告警的处理结果。 -- [ ] **1. 运行现有校验,记录 actionlint 版本及完整诊断。** +- [x] **1. 运行现有校验,记录 actionlint 版本及完整诊断。** ```sh node --test .github/scripts/bug-server-dispatch.test.cjs @@ -100,7 +109,7 @@ git diff --check 预期已有 15 项输入校验测试通过。若 actionlint 仍是 1.7.12,明确记录其对新字段的语法误报;不能把这次检查写成通过,也不能泛化忽略所有语法错误。其余诊断均需解决。 -- [ ] **2. 在 PR 分支运行不包含 PR 代码和 secret 的平台探针。** +- [x] **2. 在 PR 分支运行不包含 PR 代码和 secret 的平台探针。** 临时文件的完整内容: @@ -116,9 +125,13 @@ jobs: cache-mode: none steps: - name: Verify effective cache mode - run: | - printf 'cache mode: %s\n' "${ACTIONS_CACHE_MODE:-unset}" - test "${ACTIONS_CACHE_MODE:-}" = none + uses: actions/github-script@v8 + with: + script: | + core.info(`Cache mode: ${process.env.ACTIONS_CACHE_MODE ?? 'unset'}`); + if (process.env.ACTIONS_CACHE_MODE !== 'none') { + core.setFailed('Expected cache-mode: none.'); + } ``` 在修复实现进入正常提交、推送阶段时运行该探针。它不 checkout、不安装依赖、不调用 Bug Server、不读写缓存。验收要求 GitHub 接受 YAML,且日志输出 `cache mode: none`。保留 run URL,再删除临时 workflow。 @@ -155,7 +168,7 @@ gh api repos/VisActor/VRender/code-scanning/alerts/46 - Consumes: Task 2 的真实验证结果。 - Produces: 与实现一致的权限说明和未完成项记录。 -- [ ] **1. 用具体权限说明替换含糊的“无共享缓存”。** +- [x] **1. 用具体权限说明替换含糊的“无共享缓存”。** README 的构建边界使用以下说明: @@ -178,3 +191,16 @@ README 的构建边界使用以下说明: - [CodeQL 缓存写权限判断源码](https://github.com/github/codeql/blob/main/actions/ql/lib/codeql/actions/security/CachePoisoningQuery.qll) - [缓存污染告警](https://github.com/VisActor/VRender/pull/2134#discussion_r4032878435) - [缺失权限告警](https://github.com/VisActor/VRender/pull/2134#discussion_r4032878451) + +## 执行记录(2026-09-17) + +- 权限修复提交:`a1d30899c`;JavaScript action 检查修正:`f8fe1a07a`。 +- [首轮探针](https://github.com/VisActor/VRender/actions/runs/35179960262):GitHub 接受配置,初始化日志为 `Cache mode: none`,但 shell 没有该变量。通过官方 [NodeScriptActionHandler 源码](https://github.com/actions/runner/blob/main/src/Runner.Worker/Handlers/NodeScriptActionHandler.cs) 确认注入边界,改用 JavaScript action 检查。 +- [修正后的平台探针](https://github.com/VisActor/VRender/actions/runs/35180071512):**通过**。runner `2.337.0`;初始化日志及 JavaScript action 均报告 `Cache mode: none`。探针没有执行 PR 代码、接触 Bug Server secret 或读写缓存;验证后删除临时 workflow。 +- Node 输入校验:15/15 通过。直接运行 workflow 中的隔离检查脚本,确认 `none` 放行,`read`、`write`、未注入变量均拒绝,共 4 个场景通过。 +- 推送钩子要求的 `rush test --only tag:package` 已通过;没有以跳过钩子的方式推送。 +- actionlint 1.7.12:仅有 `cache-mode` 未识别诊断,**不记为通过**。GitHub 原生解析和 runner 验证通过。 +- CodeQL 权限告警 #46:实例状态为 **fixed**。缓存告警 #45 在 `f8fe1a07a` 上仍为 **open**;其规则未考虑 `cache-mode`。没有忽略规则或关闭告警。 +- 已查询 develop 的传统 required status checks 和适用 rulesets:前者未启用,后者为空。未修改合并规则,也未合并 PR。 +- 首次真实手动链路仍需在修复合入默认分支后执行;平台探针不等于端到端 Bug Server 验收。 +- README、设计文档、原实现验证记录和飞书维护文档均已同步;飞书文档 revision 17 已回读确认。 diff --git a/docs/superpowers/specs/2026-09-17-bug-server-dispatch-design.md b/docs/superpowers/specs/2026-09-17-bug-server-dispatch-design.md index 2c766b8d8..5bd3a2563 100644 --- a/docs/superpowers/specs/2026-09-17-bug-server-dispatch-design.md +++ b/docs/superpowers/specs/2026-09-17-bug-server-dispatch-design.md @@ -14,11 +14,14 @@ 1. 只允许从默认分支运行手动入口。验证 job 从 workflow 的固定 SHA 检出可信校验脚本。 2. 校验 PR 编号、40 位十六进制 SHA,并通过 GitHub API 确认 SHA 等于该 PR 当前 head。错误在构建前终止。 -3. 构建 job 在独立 runner 中检出已验证的固定 SHA,不持有 Bug Server secret、不保留 checkout 凭据、不使用共享缓存。只上传 `tools/bugserver-trigger/dist/index.js`。 +3. 构建 job 使用 `cache-mode: none` 显式禁止缓存读写,并在检出 PR 代码前通过 JavaScript action 确认 runner 报告的模式为 `none`,否则终止。随后在独立 runner 中检出已验证的固定 SHA,不持有 Bug Server secret、不保留 checkout 凭据。只上传 `tools/bugserver-trigger/dist/index.js`。GitHub API 权限和缓存权限分别控制;不配置缓存步骤并不等于没有缓存权限。 4. 上传 job 在新的 runner 中检出 workflow SHA 对应的可信触发脚本。独立安装该脚本所需的固定版本依赖,不执行 PR 的安装脚本或产物。产物仅作为文件上传。 5. 仅调用触发脚本的 step 注入 `BUG_SERVER_TOKEN`。传给现有脚本的提交、PR ref、源分支信息来自验证 job,避免记录成 develop 的提交。 6. Actions summary 记录 PR 与测试 SHA;现有脚本继续输出 `scmVersion`、`bundleId` 和用例结果。 +7. workflow 顶层显式设置 `contents: read`,使原有自动构建也不再继承仓库默认写权限;仅 PR 校验 job 额外需要 `pull-requests: read`。 ## 验证 使用 Node 内置测试覆盖输入校验、默认分支限制、SHA 不匹配以及 fork PR 成功解析;actionlint 校验 workflow。以本地 mock HTTP 模拟上传、SCM 构建和图片测试,验证可信脚本的独立运行及元数据传递。线上触发需要入口合入默认分支后执行。 + +安全修复另行验证 GitHub 原生 `cache-mode` 配置、runner 实际模式和 token 权限。2026-09-17 的 actionlint 1.7.12 与 CodeQL 缓存污染规则尚未完整识别该字段,工具诊断与平台验证结果分别记录,详见 [安全修复计划](../plans/2026-09-17-bug-server-security-fix.md)。 diff --git a/tools/bugserver-trigger/README.md b/tools/bugserver-trigger/README.md index ed14a9ccc..8889e637a 100644 --- a/tools/bugserver-trigger/README.md +++ b/tools/bugserver-trigger/README.md @@ -28,10 +28,12 @@ The run appears under Actions; this manual run does not automatically attach a c The manual workflow uses three separate jobs: 1. Validate the PR and reviewed SHA using the GitHub API. -2. Build the PR on a runner with read-only repository access, no retained checkout credentials, no shared cache and no Bug Server token. Upload only the generated bundle. +2. Build the reviewed PR with read-only repository access and `cache-mode: none`, which denies cache reads and writes independently of `GITHUB_TOKEN`. A JavaScript action verifies the runner reports this mode before checking out PR code. Keep checkout credentials disabled and the Bug Server token on the separate submission runner. Upload only the generated bundle. 3. On a fresh runner, use the trigger script from the default-branch workflow commit. Install its dependencies separately with npm lifecycle scripts disabled, download the bundle as data, and pass the token only to the API client step. This job never executes the PR bundle or its package scripts. -Existing push and pull-request automatic runs are unchanged. A fork PR's automatic run still cannot obtain repository secrets; use the manual entry for Bug Server validation. +The workflow defaults to `contents: read`; only the PR validation job additionally requests `pull-requests: read`. Existing push and pull-request automatic runs retain their build and test steps with read-only repository permissions. A fork PR's automatic run still cannot obtain repository secrets; use the manual entry for Bug Server validation. + +If **Verify cache isolation** fails, stop and check the runner's cache-mode support. Do not set `ACTIONS_CACHE_MODE` yourself or remove the guard: the job-level `cache-mode` setting must restrict the cache token. The runner exposes the mode to JavaScript actions, not ordinary shell steps. ## Local validation @@ -41,3 +43,5 @@ From the repository root: node --test .github/scripts/bug-server-dispatch.test.cjs actionlint .github/workflows/bug-server.yml ``` + +As of 2026-09-17, actionlint 1.7.12 does not recognize `cache-mode`, and CodeQL's cache-poisoning rule does not account for it. Record these diagnostics separately from the GitHub runtime verification; do not describe them as passing or disable the security rule. See the [security fix plan](../../docs/superpowers/plans/2026-09-17-bug-server-security-fix.md) for evidence and remaining validation. From 46ad9c3f4a5f8556df3ee04c4827cdb2c3839094 Mon Sep 17 00:00:00 2001 From: xile611 Date: Thu, 17 Sep 2026 12:02:29 +0800 Subject: [PATCH 6/8] docs: record verified automatic build permissions --- .../plans/2026-09-17-bug-server-security-fix.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/superpowers/plans/2026-09-17-bug-server-security-fix.md b/docs/superpowers/plans/2026-09-17-bug-server-security-fix.md index 2714f14be..21415aebd 100644 --- a/docs/superpowers/plans/2026-09-17-bug-server-security-fix.md +++ b/docs/superpowers/plans/2026-09-17-bug-server-security-fix.md @@ -138,7 +138,7 @@ jobs: 如果 GitHub 拒绝字段或 runner 不报告 `none`,该方案不具备落地条件,应停止合并;不要删除隔离配置继续执行外部 PR。后续改用 `pull_request` 构建 artifact、手动入口校验其来源后上传的方案,并单独完成该架构的实现计划。 -- [ ] **3. 复查真实自动构建的权限和两条 CodeQL 告警。** +- [x] **3. 复查真实自动构建的权限和两条 CodeQL 告警。** ```sh gh pr checks 2134 --repo VisActor/VRender @@ -180,7 +180,7 @@ README 的构建边界使用以下说明: 在原实现计划的验证结果后补记:此前单测、mock 和 actionlint 通过仅覆盖功能及旧语法检查,未验证缓存权限隔离;此次修复补齐这项边界。 -- [ ] **2. 交付时列出改动和真实状态。** +- [x] **2. 交付时列出改动和真实状态。** 至少记录最新 commit、两条告警结果、自动构建权限日志、缓存模式探针 URL、首次真实手动运行结果。区分“合并前已验证”和“合并后待验证”,不把计划写成已完成结果。 @@ -200,7 +200,8 @@ README 的构建边界使用以下说明: - Node 输入校验:15/15 通过。直接运行 workflow 中的隔离检查脚本,确认 `none` 放行,`read`、`write`、未注入变量均拒绝,共 4 个场景通过。 - 推送钩子要求的 `rush test --only tag:package` 已通过;没有以跳过钩子的方式推送。 - actionlint 1.7.12:仅有 `cache-mode` 未识别诊断,**不记为通过**。GitHub 原生解析和 runner 验证通过。 -- CodeQL 权限告警 #46:实例状态为 **fixed**。缓存告警 #45 在 `f8fe1a07a` 上仍为 **open**;其规则未考虑 `cache-mode`。没有忽略规则或关闭告警。 +- CodeQL 权限告警 #46:实例状态为 **fixed**。缓存告警 #45 在 `a3d5f2e6e` 上仍为 **open**;其规则未考虑 `cache-mode`。没有忽略规则或关闭告警。 +- [自动构建启动日志](https://github.com/VisActor/VRender/actions/runs/35179962932/job/105069769361):`GITHUB_TOKEN Permissions` 仅有 `Contents: read`、`Metadata: read`,没有写权限。该 run 的构建步骤通过;后因新提交替代而取消,与另一旧提交的重复 CI 一同清理,最新提交的 CI 继续运行。 - 已查询 develop 的传统 required status checks 和适用 rulesets:前者未启用,后者为空。未修改合并规则,也未合并 PR。 - 首次真实手动链路仍需在修复合入默认分支后执行;平台探针不等于端到端 Bug Server 验收。 - README、设计文档、原实现验证记录和飞书维护文档均已同步;飞书文档 revision 17 已回读确认。 From ae7fc0926581218905a90a3838bfb5d65741f128 Mon Sep 17 00:00:00 2001 From: xile611 Date: Thu, 17 Sep 2026 14:04:29 +0800 Subject: [PATCH 7/8] fix: build Bug Server PR artifacts outside the default branch context --- .github/scripts/bug-server-dispatch.cjs | 64 +++++- .github/scripts/bug-server-dispatch.test.cjs | 209 +++++++++++++++++- .github/scripts/extract_bug_server_bundle.py | 33 +++ .../scripts/test_extract_bug_server_bundle.py | 78 +++++++ .github/workflows/bug-server-pr-bundle.yml | 42 ++++ .github/workflows/bug-server.yml | 79 +++---- .../plans/2026-09-17-bug-server-dispatch.md | 2 + .../2026-09-17-bug-server-pr-artifact.md | 50 +++++ .../2026-09-17-bug-server-security-fix.md | 2 + .../2026-09-17-bug-server-dispatch-design.md | 30 ++- tools/bugserver-trigger/README.md | 23 +- 11 files changed, 527 insertions(+), 85 deletions(-) create mode 100644 .github/scripts/extract_bug_server_bundle.py create mode 100644 .github/scripts/test_extract_bug_server_bundle.py create mode 100644 .github/workflows/bug-server-pr-bundle.yml create mode 100644 docs/superpowers/plans/2026-09-17-bug-server-pr-artifact.md diff --git a/.github/scripts/bug-server-dispatch.cjs b/.github/scripts/bug-server-dispatch.cjs index 401828e2b..815be0bec 100644 --- a/.github/scripts/bug-server-dispatch.cjs +++ b/.github/scripts/bug-server-dispatch.cjs @@ -25,11 +25,73 @@ async function resolveBugServerTarget({ github, context, prNumber, headSha }) { ); } + if (!pull.head.repo) { + throw new Error('The PR head repository no longer exists.'); + } + const workflowPath = '.github/workflows/bug-server-pr-bundle.yml'; + const { data: workflow } = await github.rest.actions.getWorkflow({ + ...context.repo, + workflow_id: 'bug-server-pr-bundle.yml' + }); + const runs = await github.paginate(github.rest.actions.listWorkflowRuns, { + ...context.repo, + workflow_id: workflow.id, + event: 'pull_request', + head_sha: sha, + status: 'success', + per_page: 100 + }); + const run = runs + .filter( + candidate => + candidate.workflow_id === workflow.id && + candidate.path === workflowPath && + candidate.event === 'pull_request' && + candidate.status === 'completed' && + candidate.conclusion === 'success' && + candidate.head_sha === sha && + candidate.head_branch === pull.head.ref && + candidate.repository?.id === pull.base.repo.id && + candidate.head_repository?.id === pull.head.repo.id && + // GitHub omits PR associations for fork runs. Repository, branch and SHA still bind the source. + (!candidate.pull_requests?.length || candidate.pull_requests.some(pr => pr.number === Number(prNumber))) + ) + .sort((a, b) => b.id - a.id)[0]; + if (!run) { + throw new Error( + `No successful PR bundle build for PR #${prNumber} at ${sha}. Wait for or re-run Bug Server PR Bundle.` + ); + } + const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, { + ...context.repo, + run_id: run.id, + per_page: 100 + }); + const matches = artifacts.filter(artifact => artifact.name === `bug-server-pr-${prNumber}-${sha}`); + if (matches.length !== 1) { + throw new Error(`Expected exactly one PR bundle artifact in run ${run.id}. Re-run Bug Server PR Bundle.`); + } + const artifact = matches[0]; + if (artifact.expired) { + throw new Error('The PR bundle artifact has expired. Re-run Bug Server PR Bundle.'); + } + if ( + artifact.workflow_run?.id !== run.id || + artifact.workflow_run.head_sha !== sha || + artifact.workflow_run.repository_id !== pull.base.repo.id || + artifact.workflow_run.head_repository_id !== pull.head.repo.id + ) { + throw new Error('Artifact provenance does not match the reviewed PR build.'); + } + return { prNumber: Number(prNumber), sha, headRef: pull.head.ref, - prUrl: pull.html_url + prUrl: pull.html_url, + runId: run.id, + runUrl: run.html_url, + artifactId: artifact.id }; } diff --git a/.github/scripts/bug-server-dispatch.test.cjs b/.github/scripts/bug-server-dispatch.test.cjs index fd016e811..4a3e6473a 100644 --- a/.github/scripts/bug-server-dispatch.test.cjs +++ b/.github/scripts/bug-server-dispatch.test.cjs @@ -12,12 +12,55 @@ function fixture(overrides = {}) { payload: { repository: { default_branch: 'develop' } } }; const pull = { - base: { repo: { full_name: 'VisActor/VRender' } }, - head: { sha, ref: 'feat/line-render-contribution', repo: { full_name: 'g1f9/VRender' } }, + base: { repo: { id: 1, full_name: 'VisActor/VRender' } }, + head: { sha, ref: 'feat/line-render-contribution', repo: { id: 2, full_name: 'g1f9/VRender' } }, html_url: 'https://github.com/VisActor/VRender/pull/2128' }; + const run = { + id: 100, + workflow_id: 50, + path: '.github/workflows/bug-server-pr-bundle.yml', + event: 'pull_request', + status: 'completed', + conclusion: 'success', + head_sha: sha, + head_branch: pull.head.ref, + repository: { id: 1 }, + head_repository: { id: 2 }, + pull_requests: [], + html_url: 'https://github.com/VisActor/VRender/actions/runs/100' + }; + const artifact = { + id: 200, + name: `bug-server-pr-2128-${sha}`, + expired: false, + workflow_run: { id: 100, repository_id: 1, head_repository_id: 2, head_sha: sha } + }; + const runs = [run]; + const artifacts = [artifact]; const github = { + paginate: async (method, params) => { + const { data } = await method(params); + return data.workflow_runs ?? data.artifacts; + }, rest: { + actions: { + getWorkflow: async params => { + assert.equal(params.workflow_id, 'bug-server-pr-bundle.yml'); + return { data: { id: 50, path: '.github/workflows/bug-server-pr-bundle.yml' } }; + }, + listWorkflowRuns: async params => { + assert.equal(params.workflow_id, 50); + assert.equal(params.head_sha, sha); + assert.equal(params.event, 'pull_request'); + assert.equal(params.status, 'success'); + return { data: { workflow_runs: runs } }; + }, + listWorkflowRunArtifacts: async params => { + assert.equal(params.run_id, 100); + return { data: { artifacts } }; + } + }, pulls: { get: async params => { calls.push(params); @@ -26,7 +69,15 @@ function fixture(overrides = {}) { } } }; - return { args: { github, context, prNumber: '2128', headSha: sha, ...overrides }, calls, pull }; + return { + args: { github, context, prNumber: '2128', headSha: sha, ...overrides }, + calls, + pull, + run, + artifact, + runs, + artifacts + }; } test('resolves the reviewed fork head, including source metadata', async () => { @@ -35,7 +86,10 @@ test('resolves the reviewed fork head, including source metadata', async () => { prNumber: 2128, sha, headRef: 'feat/line-render-contribution', - prUrl: 'https://github.com/VisActor/VRender/pull/2128' + prUrl: 'https://github.com/VisActor/VRender/pull/2128', + runId: 100, + runUrl: 'https://github.com/VisActor/VRender/actions/runs/100', + artifactId: 200 }); assert.deepEqual(calls, [{ owner: 'VisActor', repo: 'VRender', pull_number: 2128 }]); }); @@ -82,3 +136,150 @@ test('propagates API lookup failures without producing a build target', async () }; await assert.rejects(resolveBugServerTarget(args), /Not Found/); }); + +for (const [name, change] of [ + [ + 'wrong workflow', + run => { + run.workflow_id = 51; + } + ], + [ + 'wrong workflow path', + run => { + run.path = '.github/workflows/other.yml'; + } + ], + [ + 'wrong event', + run => { + run.event = 'workflow_dispatch'; + } + ], + [ + 'wrong run SHA', + run => { + run.head_sha = 'a'.repeat(40); + } + ], + [ + 'wrong base repository', + run => { + run.repository.id = 3; + } + ], + [ + 'wrong head repository', + run => { + run.head_repository.id = 3; + } + ], + [ + 'wrong source branch', + run => { + run.head_branch = 'another-branch'; + } + ], + [ + 'wrong PR association', + run => { + run.pull_requests = [{ number: 2135 }]; + } + ], + [ + 'failed build', + run => { + run.conclusion = 'failure'; + } + ], + [ + 'unfinished build', + run => { + run.status = 'in_progress'; + } + ] +]) { + test(`rejects artifact source with ${name}`, async () => { + const { args, run } = fixture(); + change(run); + await assert.rejects(resolveBugServerTarget(args), /No successful PR bundle build/); + }); +} + +test('accepts an explicit matching PR association', async () => { + const { args, run } = fixture(); + run.pull_requests = [{ number: 2128 }]; + assert.equal((await resolveBugServerTarget(args)).artifactId, 200); +}); + +test('rejects missing workflow runs', async () => { + const { args, runs } = fixture(); + runs.length = 0; + await assert.rejects(resolveBugServerTarget(args), /No successful PR bundle build/); +}); + +test('does not select an older run instead of the latest matching run', async () => { + const { args, run, runs } = fixture(); + runs.unshift({ ...run, id: 99 }); + assert.equal((await resolveBugServerTarget(args)).runId, 100); +}); + +for (const [name, change] of [ + [ + 'expired', + artifact => { + artifact.expired = true; + } + ], + [ + 'wrong run', + artifact => { + artifact.workflow_run.id = 101; + } + ], + [ + 'wrong head SHA', + artifact => { + artifact.workflow_run.head_sha = 'a'.repeat(40); + } + ], + [ + 'wrong base repository', + artifact => { + artifact.workflow_run.repository_id = 3; + } + ], + [ + 'wrong head repository', + artifact => { + artifact.workflow_run.head_repository_id = 3; + } + ] +]) { + test(`rejects ${name} artifact`, async () => { + const { args, artifact } = fixture(); + change(artifact); + await assert.rejects(resolveBugServerTarget(args), /Artifact provenance|expired/); + }); +} + +test('rejects an artifact from a different PR or SHA', async () => { + const { args, artifact } = fixture(); + artifact.name = `bug-server-pr-2135-${sha}`; + await assert.rejects(resolveBugServerTarget(args), /exactly one/); +}); + +test('rejects missing or ambiguous artifacts', async () => { + for (const count of [0, 2]) { + const { args, artifacts, artifact } = fixture(); + artifacts.splice(0, 1, ...Array(count).fill(artifact)); + await assert.rejects(resolveBugServerTarget(args), /exactly one/); + } +}); + +test('does not silently fall back when the newest run artifact has expired', async () => { + const { args, run, runs, artifact } = fixture(); + runs.push({ ...run, id: 99 }); + artifact.expired = true; + await assert.rejects(resolveBugServerTarget(args), /expired/); +}); diff --git a/.github/scripts/extract_bug_server_bundle.py b/.github/scripts/extract_bug_server_bundle.py new file mode 100644 index 000000000..43b51a7de --- /dev/null +++ b/.github/scripts/extract_bug_server_bundle.py @@ -0,0 +1,33 @@ +"""Read a PR artifact as data without trusting archive paths or file attributes.""" + +import stat +import sys +import zipfile +from pathlib import Path + +MAX_BUNDLE_BYTES = 64 * 1024 * 1024 + + +def extract_bundle(archive_path, destination): + with zipfile.ZipFile(archive_path) as archive: + entries = archive.infolist() + if len(entries) != 1 or entries[0].filename != 'index.js': + raise ValueError('The PR artifact must contain exactly one file named index.js.') + entry = entries[0] + file_type = stat.S_IFMT(entry.external_attr >> 16) + if entry.is_dir() or file_type not in (0, stat.S_IFREG): + raise ValueError('The PR bundle must be a regular file, not a link or directory.') + if entry.file_size > MAX_BUNDLE_BYTES: + raise ValueError('The PR bundle exceeds the 64 MiB limit.') + with archive.open(entry) as source: + data = source.read(MAX_BUNDLE_BYTES + 1) + if len(data) > MAX_BUNDLE_BYTES: + raise ValueError('The PR bundle exceeds the 64 MiB limit.') + destination = Path(destination) + destination.parent.mkdir(parents=True, exist_ok=True) + with destination.open('xb') as output: + output.write(data) + + +if __name__ == '__main__': + extract_bundle(sys.argv[1], sys.argv[2]) diff --git a/.github/scripts/test_extract_bug_server_bundle.py b/.github/scripts/test_extract_bug_server_bundle.py new file mode 100644 index 000000000..893af7068 --- /dev/null +++ b/.github/scripts/test_extract_bug_server_bundle.py @@ -0,0 +1,78 @@ +import stat +import tempfile +import unittest +import warnings +import zipfile +from pathlib import Path +from unittest.mock import patch + +from extract_bug_server_bundle import extract_bundle + + +class ExtractBundleTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.archive = self.root / 'bundle.zip' + self.destination = self.root / 'dist' / 'index.js' + + def archive_entries(self, entries): + with warnings.catch_warnings(): + warnings.simplefilter('ignore', UserWarning) + with zipfile.ZipFile(self.archive, 'w', zipfile.ZIP_DEFLATED) as archive: + for name, data in entries: + archive.writestr(name, data) + + def test_preserves_binary_content(self): + data = b'\x00\xffbundle\n' + self.archive_entries([('index.js', data)]) + extract_bundle(self.archive, self.destination) + self.assertEqual(self.destination.read_bytes(), data) + + def test_executable_text_is_only_data(self): + data = b'throw new Error("BUNDLE_MUST_NOT_EXECUTE");' + self.archive_entries([('index.js', data)]) + extract_bundle(self.archive, self.destination) + self.assertEqual(self.destination.read_bytes(), data) + + def test_rejects_unexpected_names_and_extra_files(self): + for entries in [ + [], [('index.js', b'a'), ('scripts/trigger-test.ts', b'evil')], + [('../scripts/trigger-test.ts', b'evil')], [('/tmp/index.js', b'evil')], + [('index.js', b'a'), ('index.js', b'b')], [('folder/index.js', b'a')], + ]: + with self.subTest(entries=entries): + self.archive_entries(entries) + with self.assertRaises(ValueError): + extract_bundle(self.archive, self.destination) + self.assertFalse(self.destination.exists()) + + def test_rejects_links_and_special_files(self): + for file_type in [stat.S_IFLNK, stat.S_IFDIR, stat.S_IFIFO, stat.S_IFCHR]: + with self.subTest(file_type=file_type): + entry = zipfile.ZipInfo('index.js') + entry.create_system = 3 + entry.external_attr = (file_type | 0o777) << 16 + self.archive_entries([(entry, b'../scripts/trigger-test.ts')]) + with self.assertRaises(ValueError): + extract_bundle(self.archive, self.destination) + + def test_rejects_oversized_bundle(self): + self.archive_entries([('index.js', b'x' * 1025)]) + with patch('extract_bug_server_bundle.MAX_BUNDLE_BYTES', 1024): + with self.assertRaises(ValueError): + extract_bundle(self.archive, self.destination) + self.assertFalse(self.destination.exists()) + + def test_does_not_overwrite_existing_file(self): + self.archive_entries([('index.js', b'new')]) + self.destination.parent.mkdir() + self.destination.write_bytes(b'original') + with self.assertRaises(FileExistsError): + extract_bundle(self.archive, self.destination) + self.assertEqual(self.destination.read_bytes(), b'original') + + +if __name__ == '__main__': + unittest.main() diff --git a/.github/workflows/bug-server-pr-bundle.yml b/.github/workflows/bug-server-pr-bundle.yml new file mode 100644 index 000000000..fa86e3e01 --- /dev/null +++ b/.github/workflows/bug-server-pr-bundle.yml @@ -0,0 +1,42 @@ +name: Bug Server PR Bundle + +on: + pull_request: + branches: ['main', 'develop', 'dev/**'] + +permissions: + contents: read + +jobs: + build-pr-bundle: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + # PR code runs only in the pull_request context; cache writes stay scoped to the PR. + # This workflow never receives the Bug Server token. + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false + - name: Verify checkout + env: + EXPECTED_SHA: ${{ github.event.pull_request.head.sha }} + run: test "$(git rev-parse HEAD)" = "$EXPECTED_SHA" + - uses: actions/setup-node@v4 + with: + node-version: 24.x + - name: Install native deps for node-canvas + run: | + sudo apt-get update + sudo apt-get install -y build-essential pkg-config libcairo2-dev libpango1.0-dev libpng-dev libjpeg-dev libgif-dev librsvg2-dev + - name: Install and build PR bundle + run: | + node common/scripts/install-run-rush.js update --bypass-policy + node common/scripts/install-run-rush.js install --bypass-policy + node common/scripts/install-run-rush.js build -t @internal/bugserver-trigger + - uses: actions/upload-artifact@v4 + with: + name: bug-server-pr-${{ github.event.pull_request.number }}-${{ github.event.pull_request.head.sha }} + path: tools/bugserver-trigger/dist/index.js + if-no-files-found: error + retention-days: 7 diff --git a/.github/workflows/bug-server.yml b/.github/workflows/bug-server.yml index 137220b83..384fa2d81 100644 --- a/.github/workflows/bug-server.yml +++ b/.github/workflows/bug-server.yml @@ -35,7 +35,9 @@ jobs: cache-dependency-path: './common/config/rush/pnpm-lock.yaml' - name: Test manual dispatch validation - run: node --test .github/scripts/bug-server-dispatch.test.cjs + run: | + node --test .github/scripts/bug-server-dispatch.test.cjs + python3 -m unittest discover -s .github/scripts -p 'test_extract_bug_server_bundle.py' - name: Print All Github Environment Variables run: env @@ -72,11 +74,13 @@ jobs: permissions: contents: read pull-requests: read + actions: read outputs: sha: ${{ steps.target.outputs.sha }} pr_number: ${{ steps.target.outputs.pr_number }} head_ref: ${{ steps.target.outputs.head_ref }} pr_url: ${{ steps.target.outputs.pr_url }} + artifact_id: ${{ steps.target.outputs.artifact_id }} steps: - uses: actions/checkout@v4 with: @@ -101,71 +105,30 @@ jobs: core.setOutput('pr_number', target.prNumber); core.setOutput('head_ref', target.headRef); core.setOutput('pr_url', target.prUrl); + core.setOutput('artifact_id', target.artifactId); await core.summary .addHeading('Bug Server manual test') .addLink(`PR #${target.prNumber}`, target.prUrl) + .addLink(`Source build ${target.runId}`, target.runUrl) .addRaw(`\n\nTested head: \`${target.sha}\`\n`) .write(); - build-manual-bundle: - needs: resolve-manual-target - runs-on: ubuntu-latest - timeout-minutes: 30 - cache-mode: none - permissions: - contents: read - steps: - - name: Verify cache isolation - uses: actions/github-script@v8 - with: - script: | - if (process.env.ACTIONS_CACHE_MODE !== 'none') { - core.setFailed('PR builds require cache-mode: none.'); - } else { - core.info('Cache access is disabled for this job.'); - } - # PR code runs without Bug Server secrets or cache access. - # cache-mode controls cache tokens independently of GITHUB_TOKEN permissions. - - uses: actions/checkout@v4 - with: - ref: ${{ needs.resolve-manual-target.outputs.sha }} - persist-credentials: false - - name: Verify checkout - env: - EXPECTED_SHA: ${{ needs.resolve-manual-target.outputs.sha }} - run: test "$(git rev-parse HEAD)" = "$EXPECTED_SHA" - - uses: actions/setup-node@v4 - with: - node-version: 24.x - - name: Install native deps for node-canvas - run: | - sudo apt-get update - sudo apt-get install -y build-essential pkg-config libcairo2-dev libpango1.0-dev libpng-dev libjpeg-dev libgif-dev librsvg2-dev - - name: Install and build PR bundle - run: | - node common/scripts/install-run-rush.js update --bypass-policy - node common/scripts/install-run-rush.js install --bypass-policy - node common/scripts/install-run-rush.js build -t @internal/bugserver-trigger - - uses: actions/upload-artifact@v4 - with: - name: bug-server-manual-bundle - path: tools/bugserver-trigger/dist/index.js - if-no-files-found: error - retention-days: 7 - submit-manual-bundle: - needs: [resolve-manual-target, build-manual-bundle] + needs: resolve-manual-target runs-on: ubuntu-latest timeout-minutes: 120 permissions: contents: read + actions: read steps: # Use the immutable default-branch workflow commit, never scripts from the PR. - uses: actions/checkout@v4 with: ref: ${{ github.workflow_sha }} persist-credentials: false - sparse-checkout: tools/bugserver-trigger/scripts + sparse-checkout: | + .github/scripts + tools/bugserver-trigger/scripts - uses: actions/setup-node@v4 with: node-version: 24.x @@ -174,10 +137,22 @@ jobs: mkdir -p "$RUNNER_TEMP/bug-server-client" npm install --prefix "$RUNNER_TEMP/bug-server-client" --ignore-scripts --no-audit --no-fund --package-lock=false \ node-fetch@2.6.6 form-data@4.0.6 ts-node@10.9.0 typescript@4.9.5 - - uses: actions/download-artifact@v4 + - name: Download reviewed PR artifact + uses: actions/github-script@v8 + env: + ARTIFACT_ID: ${{ needs.resolve-manual-target.outputs.artifact_id }} with: - name: bug-server-manual-bundle - path: tools/bugserver-trigger/dist + script: | + const archive = await github.rest.actions.downloadArtifact({ + ...context.repo, + artifact_id: Number(process.env.ARTIFACT_ID), + archive_format: 'zip' + }); + const fs = require('node:fs'); + const path = require('node:path'); + fs.writeFileSync(path.join(process.env.RUNNER_TEMP, 'bug-server-bundle.zip'), Buffer.from(archive.data)); + - name: Read bundle as data + run: python3 .github/scripts/extract_bug_server_bundle.py "$RUNNER_TEMP/bug-server-bundle.zip" tools/bugserver-trigger/dist/index.js - name: Trigger Bug Server for reviewed PR working-directory: tools/bugserver-trigger env: diff --git a/docs/superpowers/plans/2026-09-17-bug-server-dispatch.md b/docs/superpowers/plans/2026-09-17-bug-server-dispatch.md index 32777aad2..ced96a8ce 100644 --- a/docs/superpowers/plans/2026-09-17-bug-server-dispatch.md +++ b/docs/superpowers/plans/2026-09-17-bug-server-dispatch.md @@ -1,5 +1,7 @@ # Bug Server Manual Dispatch Implementation Plan +> Historical implementation record. The default-branch build described below has been replaced by the [PR artifact flow](2026-09-17-bug-server-pr-artifact.md); use the current README and design for maintenance. + > Execute inline in this task; the workflow design was approved in the conversation. The referenced superpowers execution skills are not installed, so implementation uses the available repository tools. **Goal:** Allow maintainers to test an external PR at a reviewed SHA without creating a temporary PR. diff --git a/docs/superpowers/plans/2026-09-17-bug-server-pr-artifact.md b/docs/superpowers/plans/2026-09-17-bug-server-pr-artifact.md new file mode 100644 index 000000000..e66f8fe06 --- /dev/null +++ b/docs/superpowers/plans/2026-09-17-bug-server-pr-artifact.md @@ -0,0 +1,50 @@ +# Bug Server PR Artifact Implementation Plan + +> Execute inline as the fallback in the approved security fix plan. The user reported that alert #45 still blocks the latest revision; this plan completes that repair without dismissing the alert. + +**Goal:** Remove PR code execution from the default-branch manual workflow and make the CodeQL security check pass. + +**Architecture:** A separate `pull_request` workflow builds the exact head with read-only repository permissions. The manual workflow retains PR number and reviewed SHA inputs, locates an immutable artifact using GitHub API provenance, and submits only its `index.js` bytes with the trusted client. No PR build scripts or downloaded code execute in the manual workflow. + +**Tech Stack:** GitHub Actions, Node.js tests, Python standard-library ZIP handling. + +## Constraints + +- Keep existing push / PR Bug Server behavior and the trusted client API protocol. +- Keep workflow-wide `contents: read`; grant `actions: read` only to manual artifact lookup/download jobs and `pull-requests: read` only to target validation. +- Remove the superseded `build-manual-bundle`, `cache-mode` and runtime-mode guard; isolation comes from the PR event's cache scope. +- Treat artifacts as untrusted bytes. Never extract archive paths or execute their content. +- Fork run API responses can have an empty `pull_requests` array: verified using PR #2128 run 35075478495. Bind provenance to workflow ID/path, event, repository IDs, source branch and exact run head SHA. If PR associations are present, they must include the requested PR. + +## Task 1: Resolve a PR artifact + +Files: `.github/scripts/bug-server-dispatch.cjs`, `.github/scripts/bug-server-dispatch.test.cjs`. + +- [x] Extend fixture tests to cover successful fork provenance, incorrect workflow/event/SHA/repository/branch/PR, unsuccessful builds, and missing/expired/ambiguous artifacts. Preserve all input validation tests. +- [x] Run `node --test .github/scripts/bug-server-dispatch.test.cjs`; confirm new tests fail before implementation. +- [x] Extend `resolveBugServerTarget({github, context, prNumber, headSha})` to return the existing target fields plus `{runId, runUrl, artifactId}`. Resolve `bug-server-pr-bundle.yml`, list successful PR runs for the reviewed SHA, select the latest matching run, and select exactly one non-expired artifact named `bug-server-pr-${prNumber}-${sha}` whose API provenance matches the run. +- [x] Run the tests again; all provenance rejection cases must pass. + +## Task 2: Move the build and safely consume the artifact + +Files: `.github/workflows/bug-server-pr-bundle.yml`, `.github/workflows/bug-server.yml`, `.github/scripts/extract_bug_server_bundle.py`, `.github/scripts/test_extract_bug_server_bundle.py`. + +- [x] Add a PR-only workflow for `main`, `develop`, `dev/**`, using checkout at `github.event.pull_request.head.sha`, disabled persisted credentials, Node 24, the existing native dependencies/Rush build and upload-artifact v4. Artifact retention: 7 days. No repository secrets or cache action. +- [x] Add ZIP tests for a valid binary bundle, executable text treated as bytes, path traversal, extra files, duplicate names, symlink entries, oversized payloads and existing output files. +- [x] Implement `extract_bundle(archive_path, destination)` with Python `zipfile`: require exactly one regular entry named `index.js`, limit the uncompressed bundle to 64 MiB, and write bytes to the explicit destination with exclusive creation. Do not call `extract` or `extractall`. +- [x] Remove the manual build job. Add trusted API artifact lookup outputs and download the selected artifact ID into a fixed temporary ZIP file. Run the trusted extraction script before the token-bearing submission step; keep the existing client command unchanged. +- [x] Run `python3 -m unittest discover -s .github/scripts -p 'test_extract_bug_server_bundle.py'`, the Node tests, actionlint on both workflows and `git diff --check`. + +## Task 3: Verify and document + +- [ ] Push the update to PR #2134, check CodeQL alert #45 and #46 on the new commit, and require the CodeQL check to pass without dismissals. +- [ ] Wait for the new PR-only bundle workflow to succeed. Invoke the trusted resolver against that real run, download its immutable artifact, verify single-file extraction, and verify the existing upload client with the local mock API. Do not execute the bundle. +- [ ] Update README, design, the previous security plan, PR description and the existing Lark maintenance section. Document that maintainers wait for `Bug Server PR Bundle` before dispatch; missing/expired artifacts require a fresh successful PR bundle run. Existing PRs may need a new PR event after the workflow is merged. +- [ ] Record separate results for security checks, artifact pipeline and the existing photo CI. End-to-end manual dispatch from the default branch remains a post-merge check. + +## Validation before push + +- Node resolver tests: 36 passed. +- Python archive tests: 6 tests passed, including multiple malicious-entry subcases. +- actionlint 1.7.12: both final workflows pass without ignored diagnostics. +- `git diff --check`: passed. diff --git a/docs/superpowers/plans/2026-09-17-bug-server-security-fix.md b/docs/superpowers/plans/2026-09-17-bug-server-security-fix.md index 21415aebd..a9aa46155 100644 --- a/docs/superpowers/plans/2026-09-17-bug-server-security-fix.md +++ b/docs/superpowers/plans/2026-09-17-bug-server-security-fix.md @@ -1,5 +1,7 @@ # Bug Server 手动入口安全修复 Implementation Plan +> **已被替代:** 本文保留第一轮 `cache-mode` 修复及验证记录。该方案未消除最新 CodeQL 告警,不再作为最终实现;当前方案见 [PR artifact 修复计划](2026-09-17-bug-server-pr-artifact.md)。默认分支手动流程现改为只消费 PR 工作流的产物,不执行 PR 构建代码。 + > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. > > 当前环境未安装上述执行技能。用户已授权执行本计划,使用当前任务和仓库工具顺序完成;实际进度与证据记录在文末。 diff --git a/docs/superpowers/specs/2026-09-17-bug-server-dispatch-design.md b/docs/superpowers/specs/2026-09-17-bug-server-dispatch-design.md index 5bd3a2563..603c95698 100644 --- a/docs/superpowers/specs/2026-09-17-bug-server-dispatch-design.md +++ b/docs/superpowers/specs/2026-09-17-bug-server-dispatch-design.md @@ -1,27 +1,25 @@ # Bug Server 手动触发设计 -## 目标与已确认方案 +## 目标 -维护者无需创建临时 PR,即可通过 `Bug Server CI` 的 `workflow_dispatch` 验证外部 PR。用户已确认输入 PR 编号与完整 head SHA,并要求构建和持有 token 的上传过程分离。本次从 `develop` 创建 `codex/bugserver-workflow-dispatch` 实现。 +维护者输入 PR 编号和已 review 的完整 head SHA,即可测试外部 PR。PR 构建在 `pull_request` 上下文中完成,默认分支的手动入口只校验和上传产物。 -## 取舍 +## 数据流与权限边界 -- 采用独立的手动验证、构建、上传 jobs,保留已有 push / pull_request 自动流程。 -- 不改用 `pull_request_target` 执行外部 PR 代码。 -- 不在单个 job 中先构建再注入 token:构建期间启动的进程可能继续存在。 +1. `bug-server-pr-bundle.yml` 仅由 `pull_request` 触发。在只读仓库权限、无持久化 checkout 凭据、无 Bug Server token 的 runner 上检出准确 head SHA,执行 Rush 构建。缓存写入作用域属于该 PR,不属于默认分支。产物名为 `bug-server-pr--`,保留 7 天。 +2. `bug-server.yml` 的手动入口只允许默认分支,保留 `pr_number`、`head_sha`。可信脚本校验输入、base 仓库及 PR 当前 head,再从指定 PR bundle workflow 查找成功运行。 +3. 来源校验绑定 workflow ID/路径、事件、运行状态、base/head 仓库 ID、源分支及 run head SHA。fork 的运行记录可能没有 PR 列表,不能因此拒绝所有外部 PR;如列表存在则还需匹配 PR 编号。 +4. 选择最新匹配运行中唯一且未过期的命名产物,复核 artifact API 的 run ID、仓库 ID 与 SHA。失败时要求先成功运行 PR bundle 工作流,不回退到其他提交或较旧运行。 +5. 提交 job 只检出 `github.workflow_sha` 对应的可信脚本。通过 artifact ID 下载 ZIP,只接受一个名为 `index.js` 的普通文件,最大 64 MiB。可信 Python 脚本只把文件字节写入固定位置,不按 ZIP 路径解压,不执行产物。 +6. 可信 TypeScript 客户端的依赖独立安装且禁用 lifecycle scripts。仅最后的 API 调用 step 注入 `BUG_SERVER_TOKEN`;PR 元数据与产物来源由可信校验 job 提供。summary 记录 PR、SHA 和来源构建。 +7. 两个 workflow 默认 `contents: read`;查询 PR 需要 `pull-requests: read`,查询/下载 artifact 需要 `actions: read`。原有 push / pull_request 自动 Bug Server 步骤保持原来的构建和测试行为。 -## 数据流与边界 +## 维护者操作变化 -1. 只允许从默认分支运行手动入口。验证 job 从 workflow 的固定 SHA 检出可信校验脚本。 -2. 校验 PR 编号、40 位十六进制 SHA,并通过 GitHub API 确认 SHA 等于该 PR 当前 head。错误在构建前终止。 -3. 构建 job 使用 `cache-mode: none` 显式禁止缓存读写,并在检出 PR 代码前通过 JavaScript action 确认 runner 报告的模式为 `none`,否则终止。随后在独立 runner 中检出已验证的固定 SHA,不持有 Bug Server secret、不保留 checkout 凭据。只上传 `tools/bugserver-trigger/dist/index.js`。GitHub API 权限和缓存权限分别控制;不配置缓存步骤并不等于没有缓存权限。 -4. 上传 job 在新的 runner 中检出 workflow SHA 对应的可信触发脚本。独立安装该脚本所需的固定版本依赖,不执行 PR 的安装脚本或产物。产物仅作为文件上传。 -5. 仅调用触发脚本的 step 注入 `BUG_SERVER_TOKEN`。传给现有脚本的提交、PR ref、源分支信息来自验证 job,避免记录成 develop 的提交。 -6. Actions summary 记录 PR 与测试 SHA;现有脚本继续输出 `scmVersion`、`bundleId` 和用例结果。 -7. workflow 顶层显式设置 `contents: read`,使原有自动构建也不再继承仓库默认写权限;仅 PR 校验 job 额外需要 `pull-requests: read`。 +先等 `Bug Server PR Bundle` 对该 SHA 构建成功,再运行手动入口。产物缺失或过期时重跑 bundle 工作流。新增工作流之前的旧 PR 需要更新或重新打开以触发新 PR 事件;重跑旧定义不能生成新工作流。fork Actions 首次运行可能需要维护者批准。 ## 验证 -使用 Node 内置测试覆盖输入校验、默认分支限制、SHA 不匹配以及 fork PR 成功解析;actionlint 校验 workflow。以本地 mock HTTP 模拟上传、SCM 构建和图片测试,验证可信脚本的独立运行及元数据传递。线上触发需要入口合入默认分支后执行。 +Node 测试覆盖输入与产物来源校验。Python 测试覆盖正常字节、可执行文本仅作为数据、路径穿越、额外文件、重复文件、链接/特殊文件、体积限制和禁止覆盖目标文件。actionlint 与 CodeQL 必须通过,不以关闭告警作为修复。真实 PR bundle 的查找、下载、读取及 mock 客户端上传在合并前验证;默认分支完整手动测试在合并后验收。 -安全修复另行验证 GitHub 原生 `cache-mode` 配置、runner 实际模式和 token 权限。2026-09-17 的 actionlint 1.7.12 与 CodeQL 缓存污染规则尚未完整识别该字段,工具诊断与平台验证结果分别记录,详见 [安全修复计划](../plans/2026-09-17-bug-server-security-fix.md)。 +此前的 `cache-mode: none` 已在平台验证,但未完成扫描验收;最终方案移除默认分支内的 PR 构建,不再依赖该配置。 diff --git a/tools/bugserver-trigger/README.md b/tools/bugserver-trigger/README.md index 8889e637a..6eafaa8f0 100644 --- a/tools/bugserver-trigger/README.md +++ b/tools/bugserver-trigger/README.md @@ -4,7 +4,7 @@ ## Manually test a PR -After the manual workflow is merged into the repository's default branch (`develop`), maintainers with repository write access can open **Actions → Bug Server CI → Run workflow**. Select **develop**, then enter: +After the workflows are merged into the repository's default branch (`develop`), wait for **Bug Server PR Bundle** to succeed for the reviewed PR head. Fork runs may need a maintainer's approval. Then maintainers with repository write access can open **Actions → Bug Server CI → Run workflow**. Select **develop**, then enter: - `pr_number`: the PR number, including PRs from external forks. - `head_sha`: the full 40-character SHA of the PR head that you reviewed. @@ -21,19 +21,19 @@ gh workflow run bug-server.yml \ Use the currently reviewed PR head; the example SHA becomes invalid if that PR changes. The workflow rejects non-default workflow branches, malformed inputs and a SHA that differs from the PR's current head. It builds the exact requested **head commit**, not GitHub's generated merge commit. Updates after validation cannot change the commit being built. -The run appears under Actions; this manual run does not automatically attach a check or comment to the external PR. Its summary records the PR URL and tested head. The **Trigger Bug Server for reviewed PR** step prints `scmVersion`, `bundleId`, and the result counts, which identify the run in Bug Server. A missing token, failed SCM build or failed photo test makes the job fail. +The run appears under Actions; this manual run does not automatically attach a check or comment to the external PR. Its summary records the PR URL, tested head and source build run. The **Trigger Bug Server for reviewed PR** step prints `scmVersion`, `bundleId`, and the result counts, which identify the run in Bug Server. A missing token, failed SCM build or failed photo test makes the job fail. -## Execution boundaries +The manual entry consumes an existing PR bundle; it does not build PR code. Artifacts are retained for 7 days. If the build or artifact is missing, failed or expired, approve/wait for/re-run **Bug Server PR Bundle** before dispatching again. For a PR opened before this workflow was introduced, update or reopen the PR to trigger a new PR event; re-running an old workflow definition does not create the new bundle workflow. -The manual workflow uses three separate jobs: +## Execution boundaries -1. Validate the PR and reviewed SHA using the GitHub API. -2. Build the reviewed PR with read-only repository access and `cache-mode: none`, which denies cache reads and writes independently of `GITHUB_TOKEN`. A JavaScript action verifies the runner reports this mode before checking out PR code. Keep checkout credentials disabled and the Bug Server token on the separate submission runner. Upload only the generated bundle. -3. On a fresh runner, use the trigger script from the default-branch workflow commit. Install its dependencies separately with npm lifecycle scripts disabled, download the bundle as data, and pass the token only to the API client step. This job never executes the PR bundle or its package scripts. +1. **Bug Server PR Bundle** runs only on `pull_request`, builds the exact head with read-only repository permissions, disabled persisted checkout credentials and no Bug Server token. Any cache writes are confined to the PR scope. It uploads `bug-server-pr--`. +2. The manual **resolve-manual-target** job validates the current PR head and source workflow ID/path, PR event, successful run, repository IDs, source branch and run SHA. It requires one non-expired artifact with matching GitHub API provenance. Fork runs can omit PR associations; the repository/branch/SHA checks still bind the source. +3. **submit-manual-bundle** uses scripts from the immutable default-branch workflow commit. It downloads the selected artifact ID and accepts only a single regular `index.js` entry, up to 64 MiB. The trusted extractor writes bytes to a fixed path without extracting archive paths. The client only uploads those bytes; it never executes the bundle or PR package scripts. -The workflow defaults to `contents: read`; only the PR validation job additionally requests `pull-requests: read`. Existing push and pull-request automatic runs retain their build and test steps with read-only repository permissions. A fork PR's automatic run still cannot obtain repository secrets; use the manual entry for Bug Server validation. +Both workflows default to `contents: read`. Manual lookup and download jobs also need `actions: read`, and target validation needs `pull-requests: read`. The Bug Server token is injected only into the final API client step. Existing push and pull-request automatic runs retain their build and test behavior with read-only repository permissions. A fork PR's automatic Bug Server run still cannot obtain repository secrets; use the manual entry for Bug Server validation. -If **Verify cache isolation** fails, stop and check the runner's cache-mode support. Do not set `ACTIONS_CACHE_MODE` yourself or remove the guard: the job-level `cache-mode` setting must restrict the cache token. The runner exposes the mode to JavaScript actions, not ordinary shell steps. +The default-branch manual workflow does not check out or build PR code. This replaces the earlier `cache-mode` approach and does not require scanner exceptions. ## Local validation @@ -41,7 +41,6 @@ From the repository root: ```sh node --test .github/scripts/bug-server-dispatch.test.cjs -actionlint .github/workflows/bug-server.yml +python3 -m unittest discover -s .github/scripts -p 'test_extract_bug_server_bundle.py' +actionlint .github/workflows/bug-server.yml .github/workflows/bug-server-pr-bundle.yml ``` - -As of 2026-09-17, actionlint 1.7.12 does not recognize `cache-mode`, and CodeQL's cache-poisoning rule does not account for it. Record these diagnostics separately from the GitHub runtime verification; do not describe them as passing or disable the security rule. See the [security fix plan](../../docs/superpowers/plans/2026-09-17-bug-server-security-fix.md) for evidence and remaining validation. From dac694ecd84345dcccb87729b2f75a3f8912c3b6 Mon Sep 17 00:00:00 2001 From: xile611 Date: Thu, 17 Sep 2026 14:10:44 +0800 Subject: [PATCH 8/8] docs: record Bug Server artifact security verification --- .../2026-09-17-bug-server-pr-artifact.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/docs/superpowers/plans/2026-09-17-bug-server-pr-artifact.md b/docs/superpowers/plans/2026-09-17-bug-server-pr-artifact.md index e66f8fe06..5cb1f2821 100644 --- a/docs/superpowers/plans/2026-09-17-bug-server-pr-artifact.md +++ b/docs/superpowers/plans/2026-09-17-bug-server-pr-artifact.md @@ -37,10 +37,10 @@ Files: `.github/workflows/bug-server-pr-bundle.yml`, `.github/workflows/bug-serv ## Task 3: Verify and document -- [ ] Push the update to PR #2134, check CodeQL alert #45 and #46 on the new commit, and require the CodeQL check to pass without dismissals. -- [ ] Wait for the new PR-only bundle workflow to succeed. Invoke the trusted resolver against that real run, download its immutable artifact, verify single-file extraction, and verify the existing upload client with the local mock API. Do not execute the bundle. -- [ ] Update README, design, the previous security plan, PR description and the existing Lark maintenance section. Document that maintainers wait for `Bug Server PR Bundle` before dispatch; missing/expired artifacts require a fresh successful PR bundle run. Existing PRs may need a new PR event after the workflow is merged. -- [ ] Record separate results for security checks, artifact pipeline and the existing photo CI. End-to-end manual dispatch from the default branch remains a post-merge check. +- [x] Push the update to PR #2134, check CodeQL alert #45 and #46 on the new commit, and require the CodeQL check to pass without dismissals. +- [x] Wait for the new PR-only bundle workflow to succeed. Invoke the trusted resolver against that real run, download its immutable artifact, verify single-file extraction, and verify the existing upload client with the local mock API. Do not execute the bundle. +- [x] Update README, design, the previous security plan, PR description and the existing Lark maintenance section. Document that maintainers wait for `Bug Server PR Bundle` before dispatch; missing/expired artifacts require a fresh successful PR bundle run. Existing PRs may need a new PR event after the workflow is merged. +- [x] Record separate results for security checks, artifact pipeline and the existing photo CI. End-to-end manual dispatch from the default branch remains a post-merge check. ## Validation before push @@ -48,3 +48,14 @@ Files: `.github/workflows/bug-server-pr-bundle.yml`, `.github/workflows/bug-serv - Python archive tests: 6 tests passed, including multiple malicious-entry subcases. - actionlint 1.7.12: both final workflows pass without ignored diagnostics. - `git diff --check`: passed. + +## GitHub and integration validation + +Implementation commit: `ae7fc0926581218905a90a3838bfb5d65741f128`. + +- [CodeQL check](https://github.com/VisActor/VRender/runs/105095209881): `success`, no new alerts. Both Actions and JavaScript/TypeScript analyses passed. Alerts #45 and #46 have PR instance state `fixed`; neither was dismissed. +- [PR bundle run 35188318138](https://github.com/VisActor/VRender/actions/runs/35188318138): `success`. Runner initialization confirms `Contents: read` and `Metadata: read`. Its cache mode is `write` in the PR event's cache scope, not the default-branch scope. +- The production resolver and workflow download script were executed locally against the real GitHub API. They selected artifact `10483685493` from that run, bound to PR #2134 and the exact implementation SHA. +- The trusted ZIP reader produced a single 3,210,456-byte bundle, SHA-256 `43e2b49b5edbf3fc1bbc52759b6844ab6608848ec97da666322a510e73f2b79e`. The trusted upload client passed success, photo-failure and missing-token scenarios with a local mock API, which checked that the uploaded bundle bytes were preserved. A separate throwing-JavaScript fixture was also uploaded as data without execution. +- Required pre-push package tests passed. Existing automatic unit/photo CI runs were still running when this record was written; their results are separate from the verified artifact pipeline. +- README, design notes, superseded-plan notices, PR description and Lark maintenance document were updated to the artifact workflow. No merge or default-branch dispatch was performed. The first live manual Bug Server run remains a post-merge check.