mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-10-26 16:55:08 +00:00
Merge branch 'master' into work/restore-subagent-end-turn
This commit is contained in:
commit
83a51c5d65
8 files changed
+216
-66
No files matched your search
+18
-18
@@ -2,21 +2,21 @@
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-10-20-weighted-approval-status-gate-with-check-run-label.md
|
||||
/agent-note-weighted-approval-keeps-its-commit-status-gate-and-adds-a-decision-check-run:
|
||||
en: a1f0f9fd657c7d35
|
||||
zh: e8fa02fb5bde6075
|
||||
/agent-note-weighted-approval-keeps-its-commit-status-gate-and-adds-a-decision-check-run/problem:
|
||||
en: 8e854872edfc0729
|
||||
zh: 6f3ed495fcb690a4
|
||||
/agent-note-weighted-approval-keeps-its-commit-status-gate-and-adds-a-decision-check-run/decision:
|
||||
en: 03de94946d7532d7
|
||||
zh: cf2f585f715d3295
|
||||
/agent-note-weighted-approval-keeps-its-commit-status-gate-and-adds-a-decision-check-run/verification:
|
||||
en: 8298cff64cce6467
|
||||
zh: a121d509c2302c00
|
||||
/agent-note-weighted-approval-keeps-its-commit-status-gate-and-adds-a-decision-check-run/alternatives-considered:
|
||||
en: 197255cd82aabef7
|
||||
zh: dc03f4bf77150f22
|
||||
/agent-note-weighted-approval-keeps-its-commit-status-gate-and-adds-a-decision-check-run/consequences:
|
||||
en: 25e1ac1c8cab089a
|
||||
zh: 84e10cccb1b9eda7
|
||||
/agent-note-weighted-approval-keeps-its-commit-status-gate-and-publishes-the-decision-label-from-an-app:
|
||||
en: 1ea2e345d6bc1058
|
||||
zh: 65f872a0606660b8
|
||||
/agent-note-weighted-approval-keeps-its-commit-status-gate-and-publishes-the-decision-label-from-an-app/problem:
|
||||
en: f2dc92b64d32bd68
|
||||
zh: 7f24eaaa49a5776f
|
||||
/agent-note-weighted-approval-keeps-its-commit-status-gate-and-publishes-the-decision-label-from-an-app/decision:
|
||||
en: 5f7ebd56d43445cf
|
||||
zh: 869e144309b85c28
|
||||
/agent-note-weighted-approval-keeps-its-commit-status-gate-and-publishes-the-decision-label-from-an-app/verification:
|
||||
en: c80da76f52dac191
|
||||
zh: 1f7f8458211e1f5b
|
||||
/agent-note-weighted-approval-keeps-its-commit-status-gate-and-publishes-the-decision-label-from-an-app/alternatives-considered:
|
||||
en: e3b37e68b3e76d05
|
||||
zh: bb6c917c3fc6b685
|
||||
/agent-note-weighted-approval-keeps-its-commit-status-gate-and-publishes-the-decision-label-from-an-app/consequences:
|
||||
en: 79a97b43e268dd04
|
||||
zh: b85691eac68c313f
|
||||
+15
-11
@@ -1,4 +1,4 @@
|
||||
# Agent Note: Weighted approval keeps its commit status gate and adds a decision check run
|
||||
# Agent Note: Weighted approval keeps its commit status gate and publishes the decision label from an App
|
||||
|
||||
Status: implemented
|
||||
|
||||
@@ -6,32 +6,36 @@ English | [中文](2026-10-20-weighted-approval-status-gate-with-check-run-label
|
||||
|
||||
## Problem
|
||||
|
||||
The `weighted approval` commit status is the record this repository's branch rules require, and below the approval threshold it stays `pending`. A commit status has no action-required state, so readers cannot tell an approval wait from running work. Moving the gate to a check run does not work: after that change, the check run completed as `success` on the pull-request head and both the REST API and `gh pr checks` listed it, while the merge box kept reporting the same `weighted approval decision` context as `Expected — Waiting for status to be reported`, and two pull requests needed an administrator bypass. That change was reverted.
|
||||
The `weighted approval` commit status is the record this repository's branch rules require, and below the approval threshold it stays `pending`. A commit status has no action-required state, so readers cannot tell an approval wait from running work.
|
||||
|
||||
A check run carries that state, but GitHub attaches a check run created through the API to the earliest Actions check suite on the head, and a check suite created by a `pull_request_target` run is not associated with the pull request. Measured on 2026-10-20 and 2026-10-21: when the earliest suite belonged to a `pull_request` workflow the merge box listed the label, and when it belonged to the `pull_request_target` publisher it listed none, while the Checks page listed it either way; which suite is earliest is a race among the workflows a push starts. Moving the gate to a required check run published with the job token hit the same race: the merge box reported the context as `Expected — Waiting for status to be reported` while the REST API and `gh pr checks` listed it complete, so two pull requests needed an administrator bypass; that change was reverted.
|
||||
|
||||
## Decision
|
||||
|
||||
The publisher keeps writing the `weighted approval` commit status exactly as before: `pending` while the policy is unmet, `success` when it holds, and `error` when setup or evaluation fails. Branch rules keep requiring that status. That behavior, including keeping an unmet policy `pending`, is what the [archived decision](../../archived/process/2026-09-09-blocked-weighted-approvals-remain-pending.md) records; this change adds the label beside it instead of changing the gate.
|
||||
|
||||
The publisher additionally writes a `weighted approval decision` check run for the same evaluation: `action_required` while the policy is unmet, `success` when it holds, and `failure` when setup or evaluation fails. The check run's title carries the score or the blocker. Branch rules do not require it, so it never blocks a merge; it names the blocked state beside the status that does.
|
||||
The publisher writes the `weighted approval decision` check run with a token minted from a dedicated approval App (`actions/create-github-app-token` with `vars.DSH_APPROVAL_APP_CLIENT_ID`, `secrets.DSH_APPROVAL_APP_PRIVATE_KEY`, and `vars.DSH_APPROVAL_APP_SLUG`, scoped to `checks: write`). The check suite then belongs to the App and is associated with the pull-request head, so the merge box lists the label without depending on which Actions workflow started first. The conclusion is `action_required` while the policy is unmet, `success` when it holds, and `failure` when setup or evaluation fails; the title carries the score or the blocker. Branch rules do not require the check run, so it never blocks a merge.
|
||||
|
||||
A head keeps one GitHub Actions run with the check-run name: the publisher updates the head's existing run and creates one only when none exists. The setup phase creates the run as `in_progress` on a head without one and deals with a published success by rewriting its conclusion to `action_required`, because GitHub accepts but does not apply a return to `in_progress`. A failed check-run write is logged and never changes the status, so a token without the checks permission, or a check-run API failure, leaves the gate as the status wrote it. The publisher identifies its own runs by the `external_id` marker it sets on creation, which excludes check runs that workflow jobs produce for a same-named job. It revokes a published `success` before dependency setup, because GitHub accepts but does not apply a return to `in_progress`.
|
||||
The status and the check run are written through separate API clients: the status gate uses the job token, and only the check-run calls use the App token. A failed App token step or check-run write is logged and never changes the status. When the token or the slug is unavailable the publisher keeps the job token and updates `github-actions` runs only, so the gate keeps working and the label stays on the Checks page; the job token cannot update an App run, so such a head keeps the App run it already has.
|
||||
|
||||
The publisher selects the check runs created by the publishing token by `app.slug`: the App token matches the App's runs and the job token matches `github-actions` runs, because neither token can update a run the other created. It updates every selected run of that app and creates one only when none exists; job-token runs written by earlier evaluations stay untouched and can coexist with the App run on a head created before the App was configured. The setup phase creates the run as `in_progress` on a head without one and deals with a published success by rewriting its conclusion to `action_required`, because GitHub accepts but does not apply a return to `in_progress`. Runs the publisher creates carry the `external_id` marker it sets on creation.
|
||||
|
||||
## Verification
|
||||
|
||||
[Approval policy tests](../../../../.github/review-ownership/check-approval.test.mjs) pin the unchanged status states, the check-run conclusion for a met and an unmet policy, the phase writes, revoking a published success through its conclusion, one run per head, and invalid list responses. [Workflow tests](../../../../scripts/ci-workflow.spec.ts) pin the `checks: write` and `statuses: write` permissions and the setup phase commands.
|
||||
[Approval policy tests](../../../../.github/review-ownership/check-approval.test.mjs) pin the unchanged status states, the check-run conclusion for a met and an unmet policy, the phase writes, revoking a published success through its conclusion, one run per head, invalid list responses, the status fallback when the label write fails, and the split between the status client and the check-run client. [Workflow tests](../../../../scripts/ci-workflow.spec.ts) pin the `checks: write` and `statuses: write` permissions, the App token step and its single secret reference, the `DECISION_TOKEN` wiring, and the setup phase commands.
|
||||
|
||||
The check-run limit was measured on 2026-10-20 with the reverted attempt on this repository: the `weighted approval decision` check run completed as `success` on the pull-request head, the REST API and `gh pr checks` listed it, and the merge box kept reporting the same context as expected.
|
||||
The check-run association limit was measured on 2026-10-20 with the reverted and the previous attempts on this repository: a required check run published with the job token stayed `Expected` in the merge box, and a non-required check run published with the job token was absent from the merge box's check list while the Checks page listed it.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Require the check run instead of the commit status.** Measured above: the ruleset does not match a check run published through the default-branch workflow's token, so the gate cannot be satisfied.
|
||||
**Publish the label with the job token and accept the Checks page only.** That is the state this change replaces; the merge box never lists the label, so the conversation page shows only the pending status.
|
||||
|
||||
**Publish the commit status as `failure` while the policy is unmet.** The status would complete, but this changes the required record's meaning and would report an unmet, reversible condition as a malfunction.
|
||||
**Require the App check run instead of the commit status.** The App check run is associated with the pull request, so it could carry the gate. That needs a ruleset change to the App's integration and a migration for open heads; this change keeps the proven status gate and only moves the label onto the App.
|
||||
|
||||
**Publish the commit status as `failure` while the policy is unmet.** The status would complete, but this changes the required record's meaning and reports an unmet, reversible condition as a malfunction.
|
||||
|
||||
**Publish only the check run and leave the status untouched.** The status would keep its last state from an earlier evaluation, so the gate and the label could disagree.
|
||||
|
||||
**Publish the required check run from a dedicated GitHub App.** A check run whose app the ruleset names would let the required entry carry the action-required conclusion itself. The repository has no App with the checks permission; with one, the label could move onto the required record and the status could be retired.
|
||||
|
||||
## Consequences
|
||||
|
||||
A job that ends between the `pending` write and the fallback step, such as a lost runner or a cancellation that skips the final step, leaves the status `pending`; that is the original symptom of this status, not a regression from the added label. While reviewers are pending, the merge box reports `weighted approval` as pending (the gate) and the `weighted approval decision` entry as `action_required`, with the score or blocker in its title; `gh pr checks` lists both. A published success is revoked by rewriting the check run's conclusion, and the status follows the unchanged policy states.
|
||||
The merge box now lists `weighted approval` (the gate, pending while reviewers are pending) and `weighted approval decision` (`action_required`, with the score or blocker in its title). If the App token cannot be minted, the label falls back to the job token and stays on the Checks page only; the gate is unaffected. A job that ends between the `pending` write and the fallback step, such as a lost runner or a cancellation that skips the final step, leaves the status `pending`; that is the original symptom of this status, not a regression.
|
||||
+14
-10
@@ -1,4 +1,4 @@
|
||||
# Agent Note: Weighted approval 保留其 commit status 门禁并新增决策 check run
|
||||
# Agent Note: Weighted approval 保留 commit status 门禁,并由 App 发布决策标签
|
||||
|
||||
Status: implemented
|
||||
|
||||
@@ -6,32 +6,36 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
`weighted approval` commit status 是本仓库分支规则要求的记录,在批准阈值下方时保持 `pending`。commit status 没有 action-required 状态,读者无法区分等待批准与正在运行的工作。把门禁改成 check run 并不可行:该改动后 check run 在 PR head 上以 `success` 完成,REST API 与 `gh pr checks` 都能列出它,而合并框始终把同一上下文报告为 `Expected — Waiting for status to be reported`,两个 PR 需要管理员绕过才能合并。该改动已回退。
|
||||
`weighted approval` commit status 是本仓库分支规则要求的记录,在批准阈值下方时保持 `pending`。commit status 没有 action-required 状态,读者无法区分等待批准与正在运行的工作。
|
||||
|
||||
check run 可以承载这个状态,但 GitHub 会把通过 API 创建的 check run 挂到该 head 上最早的 Actions check suite,而 `pull_request_target` 的 run 创建的 check suite 不与该 PR 关联。2026-10-20 与 2026-10-21 实测:最早 suite 属于 `pull_request` workflow 时合并框会列出该标签,属于 `pull_request_target` 的 publisher 时合并框一条都不列,而 Checks 页两种情况下都能列出;哪个 suite 最早是同一次 push 触发的多个 workflow 之间的竞态。把门禁改成用 job token 发布的必需 check run 也撞上同一个竞态:合并框把该上下文报告为 `Expected — Waiting for status to be reported`,而 REST API 与 `gh pr checks` 都显示已完成,两个 PR 需要管理员绕过,该改动已回退。
|
||||
|
||||
## 决定
|
||||
|
||||
publisher 继续完全按原样写 `weighted approval` commit status:策略未满足时为 `pending`,满足时为 `success`,安装或评估失败时为 `error`。分支规则继续要求该 status。该行为——包括未满足的策略保持 `pending`——正是[已归档决定](../../archived/process/2026-09-09-blocked-weighted-approvals-remain-pending.md)记录的;本次变更在该门禁旁边新增标签,而不改变门禁。
|
||||
|
||||
publisher 为同一次评估额外写一个 `weighted approval decision` check run:策略未满足时为 `action_required`,满足时为 `success`,安装或评估失败时为 `failure`。check run 的标题携带分数或 blocker。分支规则不要求它,因此它从不阻止合并;它在会阻止合并的那条 status 旁边命名被阻止的状态。
|
||||
publisher 用从专用批准 App 铸造的 token(`actions/create-github-app-token`,使用 `vars.DSH_APPROVAL_APP_CLIENT_ID`、`secrets.DSH_APPROVAL_APP_PRIVATE_KEY` 与 `vars.DSH_APPROVAL_APP_SLUG`,权限限定为 `checks: write`)写 `weighted approval decision` check run。check suite 因此归 App 所有并与 PR head 关联,合并框会列出该标签,不再取决于哪个 Actions workflow 先启动。策略未满足时结论为 `action_required`,满足时为 `success`,安装或评估失败时为 `failure`;标题携带分数或 blocker。分支规则不要求该 check run,因此它从不阻止合并。
|
||||
|
||||
每个 head 只保留一个同名 GitHub Actions run:publisher 更新 head 上已有的 run,仅在不存在时创建。setup 阶段对没有 run 的 head 以 `in_progress` 创建,并把已发布的 success 的 conclusion 改写为 `action_required`,因为 GitHub 接受但不执行改回 `in_progress` 的更新。check run 写入失败会被记录,且从不改变 status,因此没有 checks 权限的 token 或 check-run API 失败时,门禁保持 status 写入的结果。publisher 用创建时设置的 `external_id` 标记识别自己的 run,从而排除 workflow job 为同名 job 产生的 check run。它在依赖安装前撤销已发布的 `success`,因为 GitHub 接受但不执行改回 `in_progress` 的更新。
|
||||
status 与 check run 通过两个独立的 API client 写入:status 门禁使用 job token,只有 check-run 调用使用 App token。token 步骤失败或 check-run 写入失败会被记录,且从不改变 status,因此 App 权限缺失时门禁照常工作,标签只出现在 Checks 页。
|
||||
|
||||
publisher 按 `app.slug` 选择由当前发布 token 创建的 run:App token 只匹配 App 的 run,job token 只匹配 `github-actions` 的 run,因为两种 token 都无法更新对方创建的 run。它更新所选 app 的所有同名 run,仅在不存在时创建;App 配置之前由 job token 写入的 run 保持不变,可能与 App run 在同一个 head 上共存。setup 阶段对没有 run 的 head 以 `in_progress` 创建,并把已发布的 success 的 conclusion 改写为 `action_required`,因为 GitHub 接受但不执行改回 `in_progress` 的更新。publisher 创建的 run 带创建时设置的 `external_id` 标记。
|
||||
|
||||
## 验证
|
||||
|
||||
[批准策略测试](../../../../.github/review-ownership/check-approval.test.mjs)锁定未改动的 status 状态、策略满足与未满足两种情况下 check run 的 conclusion、阶段写入、通过 conclusion 撤销已发布的成功、每个 head 只有一个 run,以及无效的列表响应。[工作流测试](../../../../scripts/ci-workflow.spec.ts)锁定 `checks: write` 与 `statuses: write` 权限以及 setup 阶段命令。
|
||||
[批准策略测试](../../../../.github/review-ownership/check-approval.test.mjs)锁定未改动的 status 状态、策略满足与未满足两种情况下 check run 的 conclusion、阶段写入、通过 conclusion 撤销已发布的成功、每个 head 只有一个 run、无效的列表响应、标签写入失败时 status 的兜底,以及 status client 与 check-run client 的分离。[工作流测试](../../../../scripts/ci-workflow.spec.ts)锁定 `checks: write` 与 `statuses: write` 权限、App token 步骤及其唯一的密钥引用、`DECISION_TOKEN` 的接线,以及 setup 阶段命令。
|
||||
|
||||
check run 的限制于 2026-10-20 在本仓库用被回退的尝试实测:`weighted approval decision` check run 在 PR head 上以 `success` 完成,REST API 与 `gh pr checks` 都能列出它,而合并框始终把同一个 `weighted approval decision` 上下文报告为 expected。
|
||||
check run 的关联限制于 2026-10-20 在本仓库用回退的尝试与上一版尝试实测:用 job token 发布的必需 check run 在合并框里保持 `Expected`;用 job token 发布的非必需 check run 不在合并框的检查列表中,而 Checks 页能列出它。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**把 check run 作为必需检查。** 见上面的实测:ruleset 不匹配默认分支 workflow 的 token 发布的 check run,门禁无法满足。
|
||||
**用 job token 发布标签、只接受 Checks 页可见。** 这正是本次变更要替换的状态;合并框永远不列该标签,会话页只显示 pending 的 status。
|
||||
|
||||
**把 App check run 设为必需检查来替代 commit status。** App check run 已关联 PR,可以承载门禁。但那需要把 ruleset 改成该 App 的 integration 并为开放 head 做迁移;本次保留已验证的 status 门禁,只把标签移到 App。
|
||||
|
||||
**策略未满足时把 commit status 发布为 `failure`。** status 会完成,但改变了必需记录的含义,并把尚未满足且可撤销的条件报告为故障。
|
||||
|
||||
**只发布 check run 而不动 status。** status 会保留上一次评估的状态,门禁与标签可能不一致。
|
||||
|
||||
**由专用 GitHub App 发布必需 check run。** 由 ruleset 指定 app 的 check run 可以让必需条目本身携带 action-required 结论。本仓库没有具备 checks 权限的 App;有了之后可以把标签移到必需记录上并停用 status。
|
||||
|
||||
## 后果
|
||||
|
||||
job 在 `pending` 写入与兜底步骤之间结束(例如 runner 丢失,或取消导致最后一步未执行)会让 status 保持 `pending`;这是该 status 既有的表现,不是新增标签带来的回归。评审未完成时,合并框把 `weighted approval` 报告为 pending(门禁),并把 `weighted approval decision` 条目报告为 `action_required`,标题里是分数或 blocker;`gh pr checks` 会列出两者。已发布的 success 通过改写 check run 的 conclusion 撤销,status 继续按未改动的策略状态变化。
|
||||
合并框现在同时列出 `weighted approval`(门禁,评审未完成时为 pending)与 `weighted approval decision`(`action_required`,标题里是分数或 blocker)。App token 步骤失败或 check-run 写入失败会被记录,且从不改变 status。缺少 token 或 slug 时 publisher 保留 job token、只更新 `github-actions` 的 run,因此门禁照常工作、标签留在 Checks 页;job token 无法更新 App run,这类 head 会保留已有的 App run。job 在 `pending` 写入与兜底步骤之间结束(例如 runner 丢失,或取消导致最后一步未执行)会让 status 保持 `pending`;这是该 status 既有的表现,不是新增标签带来的回归。
|
||||
@@ -16,7 +16,7 @@ The [`weighted-approval` workflow](../workflows/weighted-approval.yml) publishes
|
||||
|
||||
## Approval scoring
|
||||
|
||||
The weighted approval workflow exposes two pull-request checks. The `weighted approval publisher` Actions job reports whether evaluation and status publication completed, while the `weighted approval` commit status carries the approval decision on the pull request head. Branch rules must require only the commit status with GitHub Actions as its expected source; a context-only requirement can accept a same-named status from another integration. The publisher marks the head pending before Python setup or lexer installation, so failed setup or an interrupted history fetch cannot leave a previous success in place. Setup and evaluation failures publish an error status. A completed evaluation returns `pending` below two approval points, while the pull request is a draft, or while a write-capable reviewer has an effective `CHANGES_REQUESTED` review; the blocker keeps the status pending even when counted approvals reach the threshold. It returns `success` only when the threshold is met, the pull request is ready, and no such blocker exists. If evaluation fails, the publisher writes an `error` status. The publisher also writes a `weighted approval decision` check run that reports the same decision as a conclusion — `action_required` while the policy is unmet, `success` when it holds, and `failure` on setup or evaluation failures — with the score or blocker in its title. Branch rules do not require that check run, so it never blocks a merge; it names the blocked state beside the status that gates the merge. A failed check-run write is logged and never changes the status.
|
||||
The weighted approval workflow exposes two pull-request checks. The `weighted approval publisher` Actions job reports whether evaluation and status publication completed, while the `weighted approval` commit status carries the approval decision on the pull request head. Branch rules must require only the commit status with GitHub Actions as its expected source; a context-only requirement can accept a same-named status from another integration. The publisher marks the head pending before Python setup or lexer installation, so failed setup or an interrupted history fetch cannot leave a previous success in place. Setup and evaluation failures publish an error status. A completed evaluation returns `pending` below two approval points, while the pull request is a draft, or while a write-capable reviewer has an effective `CHANGES_REQUESTED` review; the blocker keeps the status pending even when counted approvals reach the threshold. It returns `success` only when the threshold is met, the pull request is ready, and no such blocker exists. If evaluation fails, the publisher writes an `error` status. The publisher also writes a `weighted approval decision` check run that reports the same decision as a conclusion — `action_required` while the policy is unmet, `success` when it holds, and `failure` on setup or evaluation failures — with the score or blocker in its title. Branch rules do not require that check run, so it never blocks a merge; it names the blocked state beside the status that gates the merge. The check run is published with a token minted from a dedicated approval App (`DSH_APPROVAL_APP_CLIENT_ID`, `DSH_APPROVAL_APP_PRIVATE_KEY`, `DSH_APPROVAL_APP_SLUG`, scope `checks: write`): GitHub attaches a check run created with the job token to the earliest Actions check suite on the head, and when that suite belongs to this `pull_request_target` workflow the merge box omits it, so the App identity makes the association stable. The App publishes only runs whose `app.slug` is the App; job-token runs from earlier evaluations stay untouched and can coexist on a head. When the token or slug is missing, the publisher falls back to the job token and updates job-token runs only, which keeps the label on the Checks page. The App token step runs with `continue-on-error`, so a missing App configuration fails only that step and leaves the job-token fallback; a failed check-run write is logged by the publisher and never changes the status.
|
||||
|
||||
Reviewers whose calculated base repository permission is `write` or `admin` count. The [approval policy](approval-policy.json) gives `@07akioni`, `@imccyu`, `@tianyicui`, `@tianyicui-bot`, `@turtle1999`, and `@turtle2099` two points each; every other write-capable reviewer gets one point. The pull-request author’s own review and reviewers without write permission do not count.
|
||||
|
||||
@@ -44,7 +44,7 @@ Each sender contributes at most once, using their own fixed weight or production
|
||||
|
||||
## Security
|
||||
|
||||
All actions in the status-writing job are pinned to commit SHAs. The job checks out only the repository default branch. It does not check out or execute pull-request code and does not use repository secrets. Only when there is no blocker, reviewer points plus author credit are insufficient, and an approval has the policy’s default weight, it fetches complete history using the job token, passes Git objects to the trusted classifier as data, and resolves commit authors in batches of 50. Fetch credentials exist only in the Git child environment. Missing history, parsing failures, or incomplete author queries fail evaluation rather than producing a partial score. The review-event workflow has no `GITHUB_TOKEN` permissions and passes only a decimal pull-request number in its run title. The publisher accepts only successful `pull_request_review` runs from the review-event workflow file, identified by `workflow_run.path`; GitHub can populate `workflow_run.name` with the expanded run title. The publisher rejects an invalid run title and a number that does not resolve to the workflow run's current pull-request head. Pull-request reviews and comments are treated as API data and escaped in logs. All publisher triggers and steps, including dependency setup, have pull-request write permission; the policy uses it to dismiss eligible senders' prior decision reviews and request recipients. Every evaluation verifies command authors and latest editors by immutable account ID through GraphQL, matching the body to the REST comment history before counting points or acting. Missing or inconsistent editor metadata fails evaluation. Non-author edits are ignored. Only active eligible delegations are reconciled; superseded or revoked commands have no effects. Dismissal failures publish an error; request failures remain separate from the approval decision.
|
||||
All actions in the status-writing job are pinned to commit SHAs. The job checks out only the repository default branch. It does not check out or execute pull-request code. Its only secret is the dedicated approval App private key, used to mint a token scoped to `checks: write` for the decision check run; the status gate keeps using the job token. Only when there is no blocker, reviewer points plus author credit are insufficient, and an approval has the policy’s default weight, it fetches complete history using the job token, passes Git objects to the trusted classifier as data, and resolves commit authors in batches of 50. Fetch credentials exist only in the Git child environment. Missing history, parsing failures, or incomplete author queries fail evaluation rather than producing a partial score. The review-event workflow has no `GITHUB_TOKEN` permissions and passes only a decimal pull-request number in its run title. The publisher accepts only successful `pull_request_review` runs from the review-event workflow file, identified by `workflow_run.path`; GitHub can populate `workflow_run.name` with the expanded run title. The publisher rejects an invalid run title and a number that does not resolve to the workflow run's current pull-request head. Pull-request reviews and comments are treated as API data and escaped in logs. All publisher triggers and steps, including dependency setup, have pull-request write permission; the policy uses it to dismiss eligible senders' prior decision reviews and request recipients. Every evaluation verifies command authors and latest editors by immutable account ID through GraphQL, matching the body to the REST comment history before counting points or acting. Missing or inconsistent editor metadata fails evaluation. Non-author edits are ignored. Only active eligible delegations are reconciled; superseded or revoked commands have no effects. Dismissal failures publish an error; request failures remain separate from the approval decision.
|
||||
|
||||
Approval policy changes take effect only after they merge into the default branch. This prevents an untrusted pull request from changing the program or policy for its own run.
|
||||
|
||||
|
||||
@@ -308,10 +308,10 @@ export async function evaluateApproval({ event, policySource, api, getOwnership
|
||||
|
||||
/**
|
||||
* Evaluate and publish the required commit status, publishing an error status when evaluation fails.
|
||||
* @param {{event: unknown, policySource: string, api: (path: string, options?: {method?: string, body?: unknown}) => Promise<unknown>, runUrl: string, write?: (line: string) => void, getOwnership?: typeof productionOwnership, getMergedCount?: typeof countMergedAuthorPulls}} options Runtime inputs.
|
||||
* @param {{event: unknown, policySource: string, api: (path: string, options?: {method?: string, body?: unknown}) => Promise<unknown>, checkApi?: (path: string, options?: {method?: string, body?: unknown}) => Promise<unknown>, checkAppSlug?: string, runUrl: string, write?: (line: string) => void, getOwnership?: typeof productionOwnership, getMergedCount?: typeof countMergedAuthorPulls}} options Runtime inputs; checkApi publishes the decision check run, defaulting to api, and checkAppSlug selects the runs that client can update.
|
||||
* @returns {Promise<Awaited<ReturnType<typeof evaluateApproval>>>} Published approval decision.
|
||||
*/
|
||||
export async function runApprovalCheck({ event, policySource, api, runUrl, getOwnership = productionOwnership, getMergedCount = countMergedAuthorPulls, write = line => process.stdout.write(`${line}\n`) }) {
|
||||
export async function runApprovalCheck({ event, policySource, api, checkApi = api, checkAppSlug = 'github-actions', runUrl, getOwnership = productionOwnership, getMergedCount = countMergedAuthorPulls, write = line => process.stdout.write(`${line}\n`) }) {
|
||||
const pull = pullRequestFromEvent(event)
|
||||
await publishStatus(api, pull, 'pending', 'Evaluating approval points.', runUrl)
|
||||
let result
|
||||
@@ -334,7 +334,7 @@ export async function runApprovalCheck({ event, policySource, api, runUrl, getOw
|
||||
}
|
||||
} catch (error) {
|
||||
await publishStatus(api, pull, 'error', 'Approval evaluation failed.', runUrl)
|
||||
await tryPublishCheck(api, pull, 'failure', 'Approval evaluation failed.', runUrl, write)
|
||||
await tryPublishCheck(checkApi, pull, 'failure', 'Approval evaluation failed.', runUrl, write, checkAppSlug)
|
||||
throw error
|
||||
}
|
||||
write(result.authorCredit
|
||||
@@ -346,7 +346,7 @@ export async function runApprovalCheck({ event, policySource, api, runUrl, getOw
|
||||
writeList(write, 'Blocking change requests', result.blockers.map(login => `@${login}`))
|
||||
writeList(write, 'Ignored reviewers without write access', result.ignoredReviewers.map(login => `@${login}`))
|
||||
await publishStatus(api, pull, result.state, result.description, runUrl)
|
||||
await tryPublishCheck(api, pull, result.state === 'success' ? 'success' : 'action_required', result.description, runUrl, write)
|
||||
await tryPublishCheck(checkApi, pull, result.state === 'success' ? 'success' : 'action_required', result.description, runUrl, write, checkAppSlug)
|
||||
write(`Published ${JSON.stringify(STATUS_CONTEXT)} status ${JSON.stringify(result.state)}.`)
|
||||
try {
|
||||
await requestDelegatedReviews(result, api, write)
|
||||
@@ -358,15 +358,15 @@ export async function runApprovalCheck({ event, policySource, api, runUrl, getOw
|
||||
|
||||
/**
|
||||
* Revoke a previous success before dependency installation, or report its failure.
|
||||
* @param {{event: unknown, api: (path: string, options: object) => Promise<unknown>, runUrl: string, phase: string, write?: (line: string) => void}} options Publication inputs.
|
||||
* @param {{event: unknown, api: (path: string, options: object) => Promise<unknown>, checkApi?: (path: string, options: object) => Promise<unknown>, checkAppSlug?: string, runUrl: string, phase: string, write?: (line: string) => void}} options Publication inputs; checkApi publishes the decision check run and checkAppSlug selects the runs it can update.
|
||||
* @returns {Promise<void>} Completion of the status write and the corresponding check-run write.
|
||||
*/
|
||||
export async function publishApprovalPhase({ event, api, runUrl, phase, write = line => process.stdout.write(`${line}\n`) }) {
|
||||
export async function publishApprovalPhase({ event, api, checkApi = api, checkAppSlug = 'github-actions', runUrl, phase, write = line => process.stdout.write(`${line}\n`) }) {
|
||||
const pull = pullRequestFromEvent(event)
|
||||
if (phase === 'pending') {
|
||||
await publishStatus(api, pull, 'pending', 'Preparing approval evaluation.', runUrl)
|
||||
try {
|
||||
await revokeApproval(api, pull, runUrl)
|
||||
await revokeApproval(checkApi, pull, runUrl, checkAppSlug)
|
||||
} catch (error) {
|
||||
write(`Decision check run not revoked; the commit status is unchanged: ${JSON.stringify(error instanceof Error ? error.message : String(error))}`)
|
||||
}
|
||||
@@ -374,7 +374,7 @@ export async function publishApprovalPhase({ event, api, runUrl, phase, write =
|
||||
}
|
||||
if (phase === 'error') {
|
||||
await publishStatus(api, pull, 'error', 'Approval setup or evaluation failed.', runUrl)
|
||||
return tryPublishCheck(api, pull, 'failure', 'Approval setup or evaluation failed.', runUrl, write)
|
||||
return tryPublishCheck(checkApi, pull, 'failure', 'Approval setup or evaluation failed.', runUrl, write, checkAppSlug)
|
||||
}
|
||||
throw new Error('invalid approval setup phase')
|
||||
}
|
||||
@@ -486,20 +486,20 @@ async function reviewerPermission(api, repository, login) {
|
||||
// The required gate is the commit status; this check run reports the same decision as a conclusion
|
||||
// label and is not required. A failed label write is reported and never changes the gate, including
|
||||
// for a replayed workflow whose token has no checks permission.
|
||||
async function tryPublishCheck(api, pull, conclusion, title, runUrl, write) {
|
||||
async function tryPublishCheck(checkApi, pull, conclusion, title, runUrl, write, appSlug) {
|
||||
try {
|
||||
await publishCheck(api, pull, conclusion, title, runUrl)
|
||||
await publishCheck(checkApi, pull, conclusion, title, runUrl, appSlug)
|
||||
} catch (error) {
|
||||
write(`Decision check run not published; the commit status is unchanged: ${JSON.stringify(error instanceof Error ? error.message : String(error))}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function publishCheck(api, pull, conclusion, title, runUrl) {
|
||||
async function publishCheck(checkApi, pull, conclusion, title, runUrl, appSlug) {
|
||||
requireHttpsRunUrl(runUrl)
|
||||
const body = { status: 'completed', conclusion, details_url: runUrl, output: { title, summary: `${title} [Workflow run](${runUrl})` } }
|
||||
const runs = await decisionRuns(api, pull)
|
||||
const runs = await decisionRuns(checkApi, pull, appSlug)
|
||||
if (runs.length === 0) {
|
||||
await api(`/repos/${pull.repository}/check-runs`, {
|
||||
await checkApi(`/repos/${pull.repository}/check-runs`, {
|
||||
method: 'POST',
|
||||
body: { name: CHECK_NAME, head_sha: pull.headSha, external_id: CHECK_RUN_MARKER, ...body },
|
||||
})
|
||||
@@ -507,21 +507,21 @@ async function publishCheck(api, pull, conclusion, title, runUrl) {
|
||||
}
|
||||
for (const run of runs) {
|
||||
if (!Number.isSafeInteger(run.id)) throw new Error('check run has no valid id')
|
||||
await api(`/repos/${pull.repository}/check-runs/${run.id}`, { method: 'PATCH', body })
|
||||
await checkApi(`/repos/${pull.repository}/check-runs/${run.id}`, { method: 'PATCH', body })
|
||||
}
|
||||
}
|
||||
|
||||
// A head without a decision starts in progress; a published success becomes action_required, so an
|
||||
// interrupted evaluation cannot leave a success that its current inputs no longer support.
|
||||
async function revokeApproval(api, pull, runUrl) {
|
||||
async function revokeApproval(checkApi, pull, runUrl, appSlug) {
|
||||
requireHttpsRunUrl(runUrl)
|
||||
const title = 'Preparing approval evaluation.'
|
||||
const output = { title, summary: `${title} [Workflow run](${runUrl})` }
|
||||
const runs = await decisionRuns(api, pull)
|
||||
const runs = await decisionRuns(checkApi, pull, appSlug)
|
||||
if (runs.length === 0) {
|
||||
await api(`/repos/${pull.repository}/check-runs`, {
|
||||
await checkApi(`/repos/${pull.repository}/check-runs`, {
|
||||
method: 'POST',
|
||||
body: { name: CHECK_NAME, head_sha: pull.headSha, status: 'in_progress', details_url: runUrl, output },
|
||||
body: { name: CHECK_NAME, head_sha: pull.headSha, external_id: CHECK_RUN_MARKER, status: 'in_progress', details_url: runUrl, output },
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -529,7 +529,7 @@ async function revokeApproval(api, pull, runUrl) {
|
||||
if (!Number.isSafeInteger(run.id)) throw new Error('check run has no valid id')
|
||||
// The label is not a gate; only a published success has to stop claiming the policy holds.
|
||||
if (run.conclusion !== 'success') continue
|
||||
await api(`/repos/${pull.repository}/check-runs/${run.id}`, {
|
||||
await checkApi(`/repos/${pull.repository}/check-runs/${run.id}`, {
|
||||
method: 'PATCH',
|
||||
body: { status: 'completed', conclusion: 'action_required', details_url: runUrl, output },
|
||||
})
|
||||
@@ -537,13 +537,14 @@ async function revokeApproval(api, pull, runUrl) {
|
||||
}
|
||||
|
||||
// A completed check run never returns to in_progress: GitHub answers 200 and leaves it completed.
|
||||
async function decisionRuns(api, pull) {
|
||||
const listed = await api(`/repos/${pull.repository}/commits/${pull.headSha}/check-runs?check_name=${encodeURIComponent(CHECK_NAME)}&filter=all&per_page=100`)
|
||||
async function decisionRuns(checkApi, pull, appSlug) {
|
||||
const listed = await checkApi(`/repos/${pull.repository}/commits/${pull.headSha}/check-runs?check_name=${encodeURIComponent(CHECK_NAME)}&filter=all&per_page=100`)
|
||||
if (!isRecord(listed) || !Array.isArray(listed.check_runs) || listed.total_count !== listed.check_runs.length) {
|
||||
throw new Error('check-run list response is invalid or incomplete')
|
||||
}
|
||||
return listed.check_runs.filter(run => isRecord(run) && isRecord(run.app) && run.app.slug === 'github-actions'
|
||||
&& (run.external_id === CHECK_RUN_MARKER || run.external_id === ''))
|
||||
// Only runs created by the publishing token can be updated: the job token cannot patch an App run
|
||||
// and the App token cannot patch an Actions run, so the slug selects the runs this token owns.
|
||||
return listed.check_runs.filter(run => isRecord(run) && isRecord(run.app) && run.app.slug === appSlug)
|
||||
}
|
||||
|
||||
function requireHttpsRunUrl(runUrl) {
|
||||
@@ -640,6 +641,17 @@ async function main() {
|
||||
token: process.env.GITHUB_TOKEN ?? '',
|
||||
apiUrl: process.env.GITHUB_API_URL,
|
||||
})
|
||||
// The decision check run is published by the dedicated approval App so GitHub associates its check
|
||||
// suite with the pull request instead of whichever Actions suite started first. Both the token and
|
||||
// the App slug must be configured; otherwise the publisher keeps the job token, whose runs only
|
||||
// show on the Checks page.
|
||||
const appToken = process.env.DECISION_TOKEN
|
||||
const appSlug = process.env.DECISION_APP_SLUG
|
||||
const useApp = Boolean(appToken && appSlug)
|
||||
const checkApi = useApp
|
||||
? createGitHubApi({ token: appToken, apiUrl: process.env.GITHUB_API_URL })
|
||||
: api
|
||||
const checkAppSlug = useApp ? appSlug : 'github-actions'
|
||||
if (isRecord(event) && isRecord(event.issue)) {
|
||||
event = await approvalEventFromComment({ event, api })
|
||||
} else if (isRecord(event) && isRecord(event.workflow_run)) {
|
||||
@@ -653,7 +665,7 @@ async function main() {
|
||||
}
|
||||
const phase = process.argv[2]
|
||||
if (phase) {
|
||||
await publishApprovalPhase({ event, api, runUrl: process.env.GITHUB_RUN_URL ?? '', phase })
|
||||
await publishApprovalPhase({ event, api, checkApi, checkAppSlug, runUrl: process.env.GITHUB_RUN_URL ?? '', phase })
|
||||
if (phase === 'pending' && process.env.GITHUB_OUTPUT) appendFileSync(process.env.GITHUB_OUTPUT, 'active=true\n')
|
||||
return
|
||||
}
|
||||
@@ -661,6 +673,8 @@ async function main() {
|
||||
event,
|
||||
policySource,
|
||||
api,
|
||||
checkApi,
|
||||
checkAppSlug,
|
||||
runUrl: process.env.GITHUB_RUN_URL ?? '',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -497,6 +497,92 @@ test('publishes setup phases without evaluating or installing dependencies', asy
|
||||
await assert.rejects(publishApprovalPhase({ ...options, phase: 'success' }), /invalid approval setup phase/u)
|
||||
})
|
||||
|
||||
test('updates the App-owned run instead of creating another', async () => {
|
||||
const calls = []
|
||||
const appRuns = [{ id: 42, app: { slug: 'dsh-weighted-approval' }, conclusion: 'success', external_id: 'weighted-approval-decision' }]
|
||||
await runPublisher({
|
||||
event: pullRequestEvent(), policySource, runUrl: 'https://github.example/run/1', write: () => {},
|
||||
checkAppSlug: 'dsh-weighted-approval',
|
||||
getOwnership: async () => ({ totalLines: 0, reviewerLines: {} }),
|
||||
getMergedCount: async () => 0,
|
||||
api: async (path, options = {}) => {
|
||||
if (path.includes('/reviews?')) return [review('writer', 'APPROVED')]
|
||||
if (path.includes('/collaborators/writer/permission')) return { permission: 'write' }
|
||||
if (path.includes('/comments?')) return []
|
||||
if (path.includes('/statuses/')) return {}
|
||||
throw new Error(`unexpected status API path ${path}`)
|
||||
},
|
||||
checkApi: async (path, options = {}) => {
|
||||
calls.push({ path, method: options.method, body: options.body })
|
||||
if (isCheckRunsList(path)) return { total_count: appRuns.length, check_runs: appRuns }
|
||||
if (path.includes('/check-runs')) return {}
|
||||
throw new Error(`unexpected check API path ${path}`)
|
||||
},
|
||||
})
|
||||
assert.deepEqual(calls.map(call => call.path), [
|
||||
`/repos/deepseek-harness/deepseek-harness/commits/${HEAD_SHA}/check-runs?check_name=weighted%20approval%20decision&filter=all&per_page=100`,
|
||||
'/repos/deepseek-harness/deepseek-harness/check-runs/42',
|
||||
])
|
||||
assert.equal(calls[1].method, 'PATCH')
|
||||
assert.equal(calls[1].body.name, undefined)
|
||||
})
|
||||
|
||||
test('ignores job-token runs when the App token publishes', async () => {
|
||||
const calls = []
|
||||
const jobRuns = [{ id: 7, app: { slug: 'github-actions' }, conclusion: 'success', external_id: 'weighted-approval-decision' }]
|
||||
await runPublisher({
|
||||
event: pullRequestEvent(), policySource, runUrl: 'https://github.example/run/1', write: () => {},
|
||||
checkAppSlug: 'dsh-weighted-approval',
|
||||
getOwnership: async () => ({ totalLines: 0, reviewerLines: {} }),
|
||||
getMergedCount: async () => 0,
|
||||
api: async (path) => {
|
||||
if (path.includes('/reviews?')) return [review('writer', 'APPROVED')]
|
||||
if (path.includes('/collaborators/writer/permission')) return { permission: 'write' }
|
||||
if (path.includes('/comments?')) return []
|
||||
if (path.includes('/statuses/')) return {}
|
||||
throw new Error(`unexpected status API path ${path}`)
|
||||
},
|
||||
checkApi: async (path, options = {}) => {
|
||||
calls.push({ path, method: options.method, body: options.body })
|
||||
if (isCheckRunsList(path)) return { total_count: jobRuns.length, check_runs: jobRuns }
|
||||
if (path.includes('/check-runs')) return {}
|
||||
throw new Error(`unexpected check API path ${path}`)
|
||||
},
|
||||
})
|
||||
assert.equal(calls.at(-1).method, 'POST')
|
||||
assert.equal(calls.at(-1).body.name, 'weighted approval decision')
|
||||
assert.equal(calls.at(-1).body.external_id, 'weighted-approval-decision')
|
||||
})
|
||||
|
||||
test('publishes the check run through the App token when one is provided', async () => {
|
||||
const statusCalls = []
|
||||
const checkCalls = []
|
||||
await runPublisher({
|
||||
event: pullRequestEvent(), policySource, runUrl: 'https://github.example/run/1', write: () => {},
|
||||
getOwnership: async () => ({ totalLines: 0, reviewerLines: {} }),
|
||||
getMergedCount: async () => 0,
|
||||
api: async (path, options = {}) => {
|
||||
statusCalls.push({ path, body: options.body })
|
||||
if (path.includes('/reviews?')) return [review('writer', 'APPROVED')]
|
||||
if (path.includes('/collaborators/writer/permission')) return { permission: 'write' }
|
||||
if (path.includes('/comments?')) return []
|
||||
if (path.includes('/statuses/')) return {}
|
||||
throw new Error(`unexpected status API path ${path}`)
|
||||
},
|
||||
checkApi: async (path, options = {}) => {
|
||||
checkCalls.push({ path, body: options.body })
|
||||
if (isCheckRunsList(path)) return emptyCheckRuns
|
||||
if (path.includes('/check-runs')) return {}
|
||||
throw new Error(`unexpected check API path ${path}`)
|
||||
},
|
||||
})
|
||||
assert.equal(statusCalls.at(-1).body.context, 'weighted approval')
|
||||
assert.equal(checkCalls.at(-1).body.name, 'weighted approval decision')
|
||||
assert.equal(checkCalls.at(-1).body.conclusion, 'action_required')
|
||||
assert.ok(statusCalls.every(call => !call.path.includes('/check-runs')))
|
||||
assert.ok(checkCalls.every(call => !call.path.includes('/statuses/')))
|
||||
})
|
||||
|
||||
test('keeps the status gate when the label write fails', async () => {
|
||||
const writes = []
|
||||
const states = []
|
||||
@@ -561,6 +647,7 @@ test('revokes a published success check run and updates one run per head', async
|
||||
`/repos/deepseek-harness/deepseek-harness/statuses/${HEAD_SHA}`,
|
||||
`/repos/deepseek-harness/deepseek-harness/commits/${HEAD_SHA}/check-runs?check_name=weighted%20approval%20decision&filter=all&per_page=100`,
|
||||
'/repos/deepseek-harness/deepseek-harness/check-runs/7',
|
||||
'/repos/deepseek-harness/deepseek-harness/check-runs/10',
|
||||
])
|
||||
for (const call of calls.slice(2)) {
|
||||
assert.equal(call.method, 'PATCH')
|
||||
|
||||
@@ -37,10 +37,26 @@ jobs:
|
||||
with:
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
persist-credentials: false
|
||||
# The decision check run is published by a dedicated approval App so GitHub associates its
|
||||
# check suite with the pull request and no other workflow owns that identity; the job token
|
||||
# alone keeps the run out of the merge box. The App holds only Checks write. A missing App
|
||||
# permission leaves the token empty and the publisher falls back to the job token.
|
||||
- name: Create decision check token
|
||||
id: decision-token
|
||||
continue-on-error: true
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
|
||||
with:
|
||||
client-id: ${{ vars.DSH_APPROVAL_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.DSH_APPROVAL_APP_PRIVATE_KEY }}
|
||||
owner: deepseek-harness
|
||||
repositories: deepseek-harness
|
||||
permission-checks: write
|
||||
- name: Revoke previous approval status
|
||||
id: revoke
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
DECISION_TOKEN: ${{ steps.decision-token.outputs.token }}
|
||||
DECISION_APP_SLUG: ${{ vars.DSH_APPROVAL_APP_SLUG }}
|
||||
GITHUB_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
run: node .github/review-ownership/check-approval.mjs pending
|
||||
# SECURITY: dependency setup runs in the status-writing job.
|
||||
@@ -57,11 +73,15 @@ jobs:
|
||||
if: steps.revoke.outputs.active == 'true'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
DECISION_TOKEN: ${{ steps.decision-token.outputs.token }}
|
||||
DECISION_APP_SLUG: ${{ vars.DSH_APPROVAL_APP_SLUG }}
|
||||
GITHUB_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
run: node .github/review-ownership/check-approval.mjs
|
||||
- name: Publish approval setup failure
|
||||
if: failure() && steps.revoke.outputs.active == 'true'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
DECISION_TOKEN: ${{ steps.decision-token.outputs.token }}
|
||||
DECISION_APP_SLUG: ${{ vars.DSH_APPROVAL_APP_SLUG }}
|
||||
GITHUB_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
run: node .github/review-ownership/check-approval.mjs error
|
||||
@@ -1371,6 +1371,27 @@ describe('Weighted approval workflow', () => {
|
||||
'persist-credentials': false,
|
||||
},
|
||||
})
|
||||
const token = steps.find(step => step.id === 'decision-token')
|
||||
expect(token).toMatchObject({
|
||||
'continue-on-error': true,
|
||||
uses: 'actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1',
|
||||
with: {
|
||||
'client-id': '${{ vars.DSH_APPROVAL_APP_CLIENT_ID }}',
|
||||
'private-key': '${{ secrets.DSH_APPROVAL_APP_PRIVATE_KEY }}',
|
||||
owner: 'deepseek-harness',
|
||||
repositories: 'deepseek-harness',
|
||||
'permission-checks': 'write',
|
||||
},
|
||||
})
|
||||
const tokenIndex = steps.indexOf(token!)
|
||||
expect(tokenIndex).toBeGreaterThan(steps.indexOf(checkout!))
|
||||
expect(tokenIndex).toBeLessThan(steps.findIndex(step => step.id === 'revoke'))
|
||||
for (const step of steps.filter(step => typeof step.run === 'string' && step.run.startsWith('node .github/review-ownership/check-approval.mjs'))) {
|
||||
expect(step.env).toMatchObject({
|
||||
DECISION_TOKEN: '${{ steps.decision-token.outputs.token }}',
|
||||
DECISION_APP_SLUG: '${{ vars.DSH_APPROVAL_APP_SLUG }}',
|
||||
})
|
||||
}
|
||||
const setupIndex = steps.findIndex(step => typeof step.uses === 'string' && step.uses.startsWith('actions/setup-python@'))
|
||||
expect(steps[setupIndex]?.if).toBe("steps.revoke.outputs.active == 'true'")
|
||||
expect(steps[setupIndex]?.uses).toBe('actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1')
|
||||
@@ -1409,7 +1430,7 @@ describe('Weighted approval workflow', () => {
|
||||
expect(record?.run).toBe("echo 'Recorded a weighted approval review event.'")
|
||||
expect(recordSteps).toHaveLength(1)
|
||||
expect(JSON.stringify(publisher)).not.toContain('github.event.pull_request.head')
|
||||
expect(JSON.stringify(publisher)).not.toContain('secrets.')
|
||||
expect(JSON.stringify(publisher).match(/secrets\.[A-Za-z0-9_]+/gu)).toEqual(['secrets.DSH_APPROVAL_APP_PRIVATE_KEY'])
|
||||
expect(JSON.stringify(reviewEvent)).not.toContain('github.token')
|
||||
expect(JSON.stringify(reviewEvent)).not.toContain('secrets.')
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user