-
Notifications
You must be signed in to change notification settings - Fork 0
docs: CI integration recipe for the exit-code PR gate #17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| # 把 euthyna 用作 PR 门禁(GitHub Actions) | ||
|
|
||
| 中文原文 · [English](ci-integration.md) | ||
|
|
||
| > **本文是什么**:一份可直接粘贴的配方,让 pull request 接受 `euthyna audit` 门禁, | ||
| > 并说明门禁变红时每个退出码的含义。它只使用 CLI 已经发布的退出码契约——不解释任何 | ||
| > flag,不解析任何报告。 | ||
| > | ||
| > **局限**:history 生产者读 git,所以需要 `fetch-depth: 0`(或至少 `--base` 可达)。 | ||
| > dependency 生产者读 lockfile;它回答的是 manifest 锁定了什么,而不是被锁版本有没有 | ||
| > CVE——那个裁定按设计留给 agent 层(见 [`fact-contract-zh.md`](fact-contract-zh.md) §6.3)。 | ||
|
|
||
| ## 门禁,一步版 | ||
|
|
||
| 把它放进想加门禁的仓库,存为 `.github/workflows/audit.yml`: | ||
|
|
||
| ```yaml | ||
| name: euthyna audit | ||
|
|
||
| on: | ||
| pull_request: | ||
|
|
||
| jobs: | ||
| audit: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 0 # 历史归属要遍历真实的 git 对象 | ||
| - uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: 24 | ||
| - name: 变更面审计(删除代码来源 + 依赖锁定版本) | ||
| run: npx --yes euthyna@latest audit --base "origin/${{ github.base_ref }}" --head HEAD | ||
| ``` | ||
|
|
||
| 这就是整个门禁。退出码按 CLI 发布的契约解读: | ||
|
|
||
| | 退出码 | 在 PR 门禁里的含义 | 合并按钮该做什么 | | ||
| |---|---|---| | ||
| | `0` | 已测量,没有 security 分类的发现 | 直接合并 | | ||
| | `10` | 已测量;至少一条 fact 属于 security 分类(删除代码来自安全修复、`--pickaxe` 下的重新引入) | 合并前由人读一遍报告——这是设计中的停留点,不是 bug | | ||
| | `1` | 用法错误(`--base` 写错、flag 错) | 修 workflow,不是修 PR | | ||
| | `2` | 完全无法测量(找不到 lockfile **不是**这种情况——缺数据按未评估报告,不是致命错) | **`2` 绝不合并**;「没测成」不等于「干净」 | | ||
|
|
||
| GitHub 把任何非零退出当作步骤失败,所以 `0` 显示绿,`10`、`1`、`2` 都显示红。如果希望 | ||
| `10`(有发现)显示绿但留下评论而不是挡合并,捕获后翻译: | ||
|
|
||
| ```yaml | ||
| - name: 变更面审计(删除代码来源 + 依赖锁定版本) | ||
| run: | | ||
| npx --yes euthyna@latest audit --base "origin/${{ github.base_ref }}" --head HEAD | ||
| continue-on-error: true | ||
| id: audit | ||
| - name: 执行契约(10 = 留给人工审阅,其余非零失败) | ||
| if: always() | ||
| run: | | ||
| code=${{ steps.audit.outcome == 'success' && 0 || 1 }} | ||
| if [ "$code" -eq 0 ]; then exit 0; fi | ||
| echo "euthyna audit 未测成干净(或发现了 security 分类的历史);见上一步。" >&2 | ||
| exit 1 | ||
| ``` | ||
|
|
||
| `ponytail:` 两步版用 shell 重述了一遍退出码,把 `10` 和 `2` 混在一起;一步版才是诚实的 | ||
| 门禁。只有当 `10` 不该挡合并时才用两步版。 | ||
|
|
||
| ## 把报告挂到 PR 上 | ||
|
|
||
| 运行会把人类可读的报告打进步骤日志。要作为附件挂到 PR 上: | ||
|
|
||
| ```yaml | ||
| - name: 变更面审计 | ||
| run: | | ||
| npx --yes euthyna@latest audit --base "origin/${{ github.base_ref }}" --head HEAD \ | ||
| | tee audit-report.txt | ||
| continue-on-error: true | ||
| id: audit | ||
| - uses: actions/upload-artifact@v4 | ||
| if: always() | ||
| with: | ||
| name: euthyna-audit-report | ||
| path: audit-report.txt | ||
| ``` | ||
|
|
||
| 下游消费方(评论机器人、裁定 agent)要读 fact 而不是散文时,加 `--json` 并把 artifact | ||
| 指向 JSON 文件。 | ||
|
|
||
| ## 这个门禁回答什么、不回答什么 | ||
|
|
||
| - **回答**:这个 PR 里哪些被删除的行来自被分类为 `security` 或 `fix` 的提交(origins | ||
| 默认开启——归到最初引入者,而不是最后修改者),manifest 锁定了哪些 lockfile 依赖、 | ||
| 位置在哪。 | ||
| - **不回答**:任何东西是不是漏洞。`10` 的意思是*存在可供裁定的材料*;六道门的裁定 | ||
| 发生在 agent 层(`euthyna gate`),这个 workflow 刻意不跑它——workflow 产不出门禁 | ||
| 要求的证据纪律,假装能产就是在制造裁定。 | ||
|
|
||
| ## 附注 | ||
|
|
||
| - `--pickaxe` 增加「被删除又加回」检测,代价是对每条删除行做一次 `git log -S` 探测 | ||
| (有上限);当威胁模型是「修过的东西被改回来」时开启。 | ||
| - `github.base_ref` 是 PR 的目标分支,所以常规场景下 `origin/main` 就是正确的 `--base`; | ||
| 叠 PR(stacked PR)需要真实的分叉点。 | ||
| - `npx euthyna@latest` 什么都没锁;跟着 npm 的 `latest` 标签漂移的门禁,其结论跨时间 | ||
| 不可比。在意一致性就钉住版本(`euthyna@0.5.0`)。 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| # Running euthyna as a PR gate (GitHub Actions) | ||
|
|
||
| English · [中文](ci-integration-zh.md) | ||
|
|
||
| > **What this document is**: a copy-paste recipe for gating a pull request on `euthyna audit`, | ||
| > plus what each exit code means when the gate goes red. It uses only the exit-code contract the | ||
| > CLI already publishes — no flags are interpreted, no report is parsed. | ||
| > | ||
| > **Limitations**: the history producer reads git, so it needs `fetch-depth: 0` (or at least | ||
| > `--base` reachable). The dependency producer reads lockfiles; it answers what the manifest | ||
| > pins, not whether a pinned version has a CVE — that adjudication is left to the agent layer, | ||
| > by design (see [`fact-contract.md`](fact-contract.md) §6.3). | ||
|
|
||
| ## The gate, one step | ||
|
|
||
| Put this in the repository that wants the gate, as `.github/workflows/audit.yml`: | ||
|
|
||
| ```yaml | ||
| name: euthyna audit | ||
|
|
||
| on: | ||
| pull_request: | ||
|
|
||
| jobs: | ||
| audit: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 0 # history attribution walks real git objects | ||
| - uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: 24 | ||
| - name: Change-surface audit (deleted-code provenance + dependency pins) | ||
| run: npx --yes euthyna@latest audit --base "origin/${{ github.base_ref }}" --head HEAD | ||
| ``` | ||
|
|
||
| That is the whole gate. Read the exit code the way the CLI publishes it: | ||
|
|
||
| | Exit | Meaning in a PR gate | What the merge button should do | | ||
| |---|---|---| | ||
| | `0` | Measured, nothing security-classified found | Merge freely | | ||
| | `10` | Measured; at least one fact is security-classified (deleted code from a security fix, a reintroduction under `--pickaxe`) | A human reads the report before merging — this is the intended stop, not a bug | | ||
| | `1` | Usage error (bad `--base`, wrong flag) | Fix the workflow, not the PR | | ||
| | `2` | Could not measure at all (no lockfile is *not* this — missing data is reported, not fatal) | **Never merge on `2`**; "failed to measure" is not "clean" | | ||
|
|
||
| Because GitHub treats any non-zero exit as a failed step, `0` shows green and `10`, `1`, `2` all | ||
| show red. If you want `10` (something found) to show green-but-commented instead, capture and | ||
| translate: | ||
|
|
||
| ```yaml | ||
| - name: Change-surface audit (deleted-code provenance + dependency pins) | ||
| run: | | ||
| npx --yes euthyna@latest audit --base "origin/${{ github.base_ref }}" --head HEAD | ||
| continue-on-error: true | ||
| id: audit | ||
| - name: Enforce the contract (10 = review, anything else non-zero fails) | ||
| if: always() | ||
| run: | | ||
| code=${{ steps.audit.outcome == 'success' && 0 || 1 }} | ||
| if [ "$code" -eq 0 ]; then exit 0; fi | ||
| echo "euthyna audit did not measure clean (or found security-classified history); see the step above." >&2 | ||
| exit 1 | ||
| ``` | ||
|
|
||
| `ponytail:` the two-step form re-implements the exit code in shell to blur `10` vs `2`; the | ||
| one-step form is the honest gate. Use the two-step form only when `10` must not block merges. | ||
|
|
||
| ## Reading the report in the PR | ||
|
|
||
| The run prints the human-readable report to the step log. To attach it to the PR instead: | ||
|
|
||
| ```yaml | ||
| - name: Change-surface audit | ||
| run: | | ||
| npx --yes euthyna@latest audit --base "origin/${{ github.base_ref }}" --head HEAD \ | ||
| | tee audit-report.txt | ||
| continue-on-error: true | ||
| id: audit | ||
| - uses: actions/upload-artifact@v4 | ||
| if: always() | ||
| with: | ||
| name: euthyna-audit-report | ||
| path: audit-report.txt | ||
|
Comment on lines
+75
to
+84
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (broader_impact): The artifact-upload recipe pipes Triggers: When this artifact snippet is used as the audit step in a gating workflow. Suggested fix: Enable |
||
| ``` | ||
|
|
||
| Add `--json` and point the artifact at the JSON file when a downstream consumer (a comment bot, | ||
| an adjudicator agent) should read facts rather than prose. | ||
|
|
||
| ## What the gate does and does not answer | ||
|
|
||
| - **Does**: which deleted lines in this PR come from commits classified `security` or `fix` | ||
| (origins on by default — the first introducer, not the last modifier), which lockfile | ||
| dependencies the manifest pins and where. | ||
| - **Does not**: decide whether anything is a vulnerability. `10` means *there is material for | ||
| adjudication*; the six-gate verdict happens in the agent layer (`euthyna gate`), which this | ||
| workflow deliberately does not run — a workflow cannot produce the evidence discipline the | ||
| gates demand, and pretending otherwise would manufacture verdicts. | ||
|
|
||
| ## Notes | ||
|
|
||
| - `--pickaxe` adds reintroduced-line detection at a cost of one `git log -S` probe per deleted | ||
| line (capped); enable it when regressions-of-fixed-code are the threat model. | ||
| - `github.base_ref` is the PR's target branch, so `origin/main` is the right `--base` for the | ||
| usual case; a stack of PRs needs the real fork point instead. | ||
| - `npx euthyna@latest` pins nothing; a gate that drifts with npm's `latest` tag is a gate whose | ||
| verdicts are not comparable across time. Pin it (`euthyna@0.5.0`) when consistency matters. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (bug_risk): The optional two-step workflow cannot distinguish exit code
10from exit codes1and2: every non-successful audit outcome is converted to1, so the enforcement step fails for flagged audits as well as measurement or usage errors. This contradicts the stated purpose of allowing10not to block merges.Triggers: When a repository copies the two-step variant intending security-classified findings (
10) to remain mergeable.Suggested fix: Capture the audit process exit code explicitly, for example by writing
$?toGITHUB_OUTPUTbefore enablingcontinue-on-error, then allow only code10and fail for1or2.