From 5ceb1cc0765770682b452c50e5d8b8b45f63e3e4 Mon Sep 17 00:00:00 2001 From: modusensus Date: Thu, 24 Sep 2026 12:27:08 +0800 Subject: [PATCH] docs: CI integration recipe for the exit-code PR gate --- README.md | 3 ++ docs/ci-integration-zh.md | 104 ++++++++++++++++++++++++++++++++++++ docs/ci-integration.md | 107 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 214 insertions(+) create mode 100644 docs/ci-integration-zh.md create mode 100644 docs/ci-integration.md diff --git a/README.md b/README.md index 6d470c0..5398671 100644 --- a/README.md +++ b/README.md @@ -306,6 +306,9 @@ This project tries to be explicit about the difference. Current state: condemn; that is not a statement about either project. See [`docs/case-study-crewai.md`](https://github.com/slow-stack/euthyna/blob/main/docs/case-study-crewai.md) and [`docs/case-study-axe-core.md`](https://github.com/slow-stack/euthyna/blob/main/docs/case-study-axe-core.md). +- **A CI recipe, by running it on GitHub.** The exit-code gate and the workflow wiring are + documented in [`docs/ci-integration.md`](https://github.com/slow-stack/euthyna/blob/main/docs/ci-integration.md); + the workflow snippet has not been exercised as a live Actions run. --- diff --git a/docs/ci-integration-zh.md b/docs/ci-integration-zh.md new file mode 100644 index 0000000..5465258 --- /dev/null +++ b/docs/ci-integration-zh.md @@ -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`)。 diff --git a/docs/ci-integration.md b/docs/ci-integration.md new file mode 100644 index 0000000..c42fd3d --- /dev/null +++ b/docs/ci-integration.md @@ -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 +``` + +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.