mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-10-26 16:55:08 +00:00
fix(ci): preserve trusted legacy policy during preflight rollout
This commit is contained in:
1 parent
108c87008b
commit
2eeab3e65f
10 files changed
+78
-10
No files matched your search
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-09-07-selective-issue-policy-evaluation.md
|
||||
2026-09-07-selective-issue-policy-evaluation.md: 72f217a64a841eff99d3314e52dd6e6f547d4a4f
|
||||
2026-09-07-selective-issue-policy-evaluation.zh.md: 0caca4554f2f29493b9c778675a53125256fa94d
|
||||
2026-09-07-selective-issue-policy-evaluation.md: a044e1d12c64d1736c0520d8a3f73713f22294d4
|
||||
2026-09-07-selective-issue-policy-evaluation.zh.md: 0c4c8d27d214a861f14cd3455f22a1007bf95bab
|
||||
@@ -14,6 +14,8 @@ Lifecycle events have a separate cost: an approval, comment, push, label change,
|
||||
|
||||
[Issue policy](../../../../.github/workflows/issue-policy.yml) keeps its required job and trusted default-branch implementation. Enforcement eligibility precedes reference reads and Project App token creation: draft PRs, Bot/App authors, and human PRs with neither review requests nor submitted reviews do not require policy validation.
|
||||
|
||||
The workflow checks the trusted checkout for a selective-preflight capability marker before invoking the command. A checkout without the marker uses full legacy validation for human PRs and preserves the legacy Bot/App exemption. This supports PR workflow YAML running against default-branch code that lacks preflight; execution errors never select the fallback.
|
||||
|
||||
Eligible PRs resolve references through repository REST reads. Informational references prove Issue identity without Project access. Only actual Issues named by resolving references require Project Priority reads; a PR number cannot satisfy the Issue requirement or cause a Project query. [The owner reference](../../../../.github/issue-management/README.md) defines metadata validation and failure behavior.
|
||||
|
||||
[Issue lifecycle](../../../../.github/workflows/issue-lifecycle.yml) subscribes to status-relevant PR events and filters title-only edits. It does not subscribe to PR pushes or label changes, or Issue assignment changes. Its job condition rejects approved/commented reviews before runner allocation. Changes-requested reviews retain their status command.
|
||||
|
||||
@@ -14,6 +14,8 @@ Status: implemented
|
||||
|
||||
[Issue policy](../../../../.github/workflows/issue-policy.yml)保留必需 job 与受信任的默认分支实现。强制范围判定先于引用读取与 Project App token 创建:草稿 PR、Bot/App 作者,以及既无评审请求也无已提交评审的人类 PR 均不需要策略校验。
|
||||
|
||||
工作流在调用命令前检查受信任检出中的选择性预检能力标记。缺少标记的检出对人类 PR 执行完整旧版校验,并保留旧版 Bot/App 豁免。这支持 PR 工作流 YAML 与缺少预检功能的默认分支代码配合执行;执行错误不会触发回退。
|
||||
|
||||
强制范围内的 PR 通过仓库 REST 读取解析引用。信息型引用无需 Project 访问即可证明 Issue 身份。只有解决型引用指向的实际 Issue 需要读取 Project Priority;PR 编号既不能满足 Issue 引用要求,也不会引发 Project 查询。[所属参考文档](../../../../.github/issue-management/README.zh.md)定义元数据校验与失败行为。
|
||||
|
||||
[Issue lifecycle](../../../../.github/workflows/issue-lifecycle.yml)订阅与状态相关的 PR 事件,并过滤仅标题编辑。它不订阅 PR 推送、PR 标签变更或 Issue 指派变更。job 条件在 runner 分配前排除 approved/commented 评审。请求修改的评审保留其状态命令。
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .github/issue-management/README.md
|
||||
README.md: b81d8a8255d4fee664b10fd3795aefc71540b4c4
|
||||
README.zh.md: ea3e251aa53b9de979c1c30d3e7008001af4285d
|
||||
README.md: cd064f28510143ce8e0e5e791b7bd00a0c9cc307
|
||||
README.zh.md: a7901a033a71d27ba01cc3bb69a03b3fd8072c24
|
||||
@@ -25,6 +25,8 @@ Contributors can link Issues as context without coupling pull-request validation
|
||||
|
||||
[Issue policy](../workflows/issue-policy.yml) applies to non-draft, human-authored PRs with a requested review or submitted review. Exempt PRs finish successfully without resolving Issue references, minting a Project App token, or querying ProjectV2. Eligibility uses live repository state before expensive reads; the required job remains present for subscribed events. Final validation re-reads live state: preflight is not a cached verdict or an exemption for metadata edits.
|
||||
|
||||
Selective preflight requires [selective-preflight.json](selective-preflight.json) in the trusted checkout. Without that marker, the workflow preserves legacy behavior: human PRs receive a Project token and full legacy validation; Bot/App PRs skip both. A failed supported preflight fails the job rather than falling back.
|
||||
|
||||
Eligible PRs need at least one same-repository Issue reference, exactly one canonical `kind/*`, at least one `area/*`, and at most one `p0`–`p3` label. Unsupported kinds, retired aliases, and `source/*` labels fail validation; [label taxonomy](../../.agents/notes/implemented/process/2026-08-08-unified-github-label-taxonomy.md) owns their meanings.
|
||||
|
||||
- Informational references, such as `Refs #3624`, establish context. Validation uses REST to distinguish Issues from PR numbers and does not read their Project fields. An informational-only PR can carry its own Priority without matching the referenced Issue.
|
||||
|
||||
@@ -25,6 +25,8 @@ description: "面向仓库维护者的 Issue 策略强制范围、Project 访问
|
||||
|
||||
[Issue policy](../workflows/issue-policy.yml)适用于已请求评审或已有评审、非草稿且由人类创建的 PR。豁免 PR 成功结束,不解析 Issue 引用、不签发 Project App token,也不查询 ProjectV2。工作流在昂贵读取前根据仓库实时状态判断强制范围;订阅事件仍保留必需 job。最终校验重新读取实时状态:预检不是缓存结论,也不是元数据编辑的豁免。
|
||||
|
||||
选择性预检要求受信任的检出中存在 [selective-preflight.json](selective-preflight.json)。缺少该标记时,工作流保留旧版行为:人类 PR 获取 Project token 并执行完整旧版校验;Bot/App PR 跳过两者。受支持的预检执行失败时,job 失败而不回退。
|
||||
|
||||
强制范围内的 PR 至少需要一个同仓库 Issue 引用、恰好一个规范的 `kind/*`、至少一个 `area/*`,以及最多一个 `p0`–`p3` 标签。不支持的 kind、退役别名和 `source/*` 标签会使校验失败;[标签分类](../../.agents/notes/implemented/process/2026-08-08-unified-github-label-taxonomy.zh.md)定义其含义。
|
||||
|
||||
- 信息型引用(如 `Refs #3624`)提供背景。校验通过 REST 区分 Issue 与 PR 编号,不读取这些引用的 Project 字段。仅含信息型引用的 PR 可以使用自己的 Priority,无需匹配所引用的 Issue。
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { mkdtempSync, readFileSync, readdirSync, rmSync } from 'node:fs'
|
||||
import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import test from 'node:test'
|
||||
@@ -903,13 +904,56 @@ test('keeps trusted preflight before token minting and required policy unconditi
|
||||
assert.doesNotMatch(source, /pull_request\.head|pull_request_target/)
|
||||
assert.ok(steps[1].includes('id: preflight'))
|
||||
assert.ok(steps[1].includes('GITHUB_TOKEN: ${{ github.token }}'))
|
||||
assert.ok(steps[1].includes('run: node .github/issue-management/policy.mjs pr-preflight'))
|
||||
assert.ok(steps[1].includes('node .github/issue-management/policy.mjs pr-preflight'))
|
||||
assert.ok(steps[1].includes('if [ -f .github/issue-management/selective-preflight.json ]; then'))
|
||||
assert.doesNotMatch(steps[1], /secrets\.|PROJECT_TOKEN|if:/)
|
||||
assert.ok(steps[2].includes("if: ${{ steps.preflight.outputs.needs-project == 'true' }}"))
|
||||
assert.ok(steps[2].includes('permission-organization-projects: read'))
|
||||
assert.ok(steps[3].includes('PROJECT_TOKEN: ${{ steps.app-token.outputs.token }}'))
|
||||
assert.ok(steps[3].includes('run: node .github/issue-management/policy.mjs pr'))
|
||||
assert.ok(!steps[3].includes('if:'))
|
||||
assert.ok(steps[3].includes("if: ${{ steps.preflight.outputs.legacy-automated != 'true' }}"))
|
||||
})
|
||||
|
||||
test('runs trusted rollout selection with absent and present capability markers', { skip: process.platform === 'win32' ? 'The policy workflow executes under hosted Ubuntu bash' : false }, (t) => {
|
||||
const directory = mkdtempSync(join(tmpdir(), 'dsh-policy-rollout-'))
|
||||
t.after(() => rmSync(directory, { recursive: true, force: true }))
|
||||
const source = readFileSync(new URL('../workflows/issue-policy.yml', import.meta.url), 'utf8')
|
||||
const script = source.split(' run: |\n')[1].split(' - name: Create Project read token')[0]
|
||||
.split('\n').map((line) => line.slice(10)).join('\n')
|
||||
assert.deepEqual(JSON.parse(readFileSync(new URL('./selective-preflight.json', import.meta.url), 'utf8')), { version: 1 })
|
||||
const cases = [
|
||||
{ name: 'legacy human draft', type: 'User', draft: true, marker: false, expected: 'legacy-automated=false\nneeds-project=true\n' },
|
||||
{ name: 'legacy human ready', type: 'User', draft: false, marker: false, expected: 'legacy-automated=false\nneeds-project=true\n' },
|
||||
{ name: 'legacy bot', type: 'Bot', marker: false, expected: 'legacy-automated=true\nneeds-project=false\n' },
|
||||
{ name: 'legacy app', type: 'App', marker: false, expected: 'legacy-automated=true\nneeds-project=false\n' },
|
||||
{ name: 'modern exempt', type: 'Bot', marker: true, expected: 'exempt=true\nneeds-project=false\n' },
|
||||
{ name: 'modern failure', type: 'User', marker: true, failure: true, expected: '' },
|
||||
]
|
||||
for (const [index, fixture] of cases.entries()) {
|
||||
const cwd = join(directory, String(index))
|
||||
const policyDirectory = join(cwd, '.github', 'issue-management')
|
||||
mkdirSync(policyDirectory, { recursive: true })
|
||||
const eventPath = join(cwd, 'event.json')
|
||||
const outputPath = join(cwd, 'output')
|
||||
writeFileSync(eventPath, JSON.stringify({ pull_request: { user: { type: fixture.type }, draft: fixture.draft } }))
|
||||
writeFileSync(outputPath, '')
|
||||
if (fixture.marker) writeFileSync(join(policyDirectory, 'selective-preflight.json'), '{"version":1}\n')
|
||||
writeFileSync(join(policyDirectory, 'policy.mjs'), fixture.marker && !fixture.failure
|
||||
? "import fs from 'node:fs'; if (process.argv[2] !== 'pr-preflight') throw Error('wrong command'); fs.appendFileSync(process.env.GITHUB_OUTPUT, 'exempt=true\\nneeds-project=false\\n')\n"
|
||||
: "throw new Error('preflight unavailable or failed')\n")
|
||||
const result = spawnSync('bash', ['--noprofile', '--norc', '-eo', 'pipefail', '-c', script], {
|
||||
cwd,
|
||||
env: { PATH: process.env.PATH, GITHUB_EVENT_PATH: eventPath, GITHUB_OUTPUT: outputPath },
|
||||
encoding: 'utf8',
|
||||
timeout: 30_000,
|
||||
})
|
||||
assert.equal(result.error, undefined, fixture.name)
|
||||
assert.equal(result.signal, null, fixture.name)
|
||||
assert.equal(result.status, fixture.failure ? 1 : 0, fixture.name + ': ' + result.stderr)
|
||||
assert.equal(readFileSync(outputPath, 'utf8'), fixture.expected, fixture.name)
|
||||
if (fixture.marker) assert.doesNotMatch(result.stdout, /preserving legacy/)
|
||||
else assert.match(result.stdout, /preserving legacy policy enforcement/)
|
||||
}
|
||||
})
|
||||
|
||||
test('allocates lifecycle runners only for relevant reviews and PR body edits', () => {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{ "version": 1 }
|
||||
@@ -25,7 +25,19 @@ jobs:
|
||||
id: preflight
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: node .github/issue-management/policy.mjs pr-preflight
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -f .github/issue-management/selective-preflight.json ]; then
|
||||
node .github/issue-management/policy.mjs pr-preflight
|
||||
else
|
||||
node --input-type=module <<'NODE'
|
||||
import fs from 'node:fs'
|
||||
const event = JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, 'utf8'))
|
||||
const automated = ['Bot', 'App'].includes(event.pull_request.user.type)
|
||||
fs.appendFileSync(process.env.GITHUB_OUTPUT, `legacy-automated=${automated}\nneeds-project=${!automated}\n`)
|
||||
console.log('Trusted policy has no selective preflight; preserving legacy policy enforcement.')
|
||||
NODE
|
||||
fi
|
||||
- name: Create Project read token
|
||||
id: app-token
|
||||
if: ${{ steps.preflight.outputs.needs-project == 'true' }}
|
||||
@@ -39,6 +51,7 @@ jobs:
|
||||
permission-organization-projects: read
|
||||
# Re-read current state even after an exempt preflight; never replace a failure with an edit skip.
|
||||
- name: Validate pull request
|
||||
if: ${{ steps.preflight.outputs.legacy-automated != 'true' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
PROJECT_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
@@ -981,10 +981,12 @@ describe('Issue lifecycle workflow', () => {
|
||||
const tokenStep = steps.find(step => step.name === 'Create Project read token')
|
||||
const validateStep = steps.find(step => step.name === 'Validate pull request')
|
||||
const preflightStep = steps.find(step => step.id === 'preflight')
|
||||
expect(preflightStep).toMatchObject({ run: 'node .github/issue-management/policy.mjs pr-preflight' })
|
||||
expect(preflightStep).toMatchObject({ shell: 'bash' })
|
||||
expect(preflightStep?.run).toContain('if [ -f .github/issue-management/selective-preflight.json ]; then')
|
||||
expect(preflightStep?.run).toContain('node .github/issue-management/policy.mjs pr-preflight')
|
||||
expect(preflightStep?.if).toBeUndefined()
|
||||
expect(policyJob.if).toBeUndefined()
|
||||
expect(validateStep?.if).toBeUndefined()
|
||||
expect(validateStep?.if).toBe("${{ steps.preflight.outputs.legacy-automated != 'true' }}")
|
||||
|
||||
expect(tokenStep).toMatchObject({
|
||||
id: 'app-token',
|
||||
|
||||
Reference in new issue
Block a user