From 01cb8432046147f99109c5b2d62c8247c0b730fb Mon Sep 17 00:00:00 2001 From: heimanba <371510756@qq.com> Date: Sun, 19 Jul 2026 14:38:58 +0800 Subject: [PATCH 1/2] [policy] feat(maintainers): add risk-based PR evidence gate Change-Id: I6fca63510c1953053a0feddd77b4bdff29c64191 --- .github/pull_request_template.md | 10 +++- .github/workflows/ci.yml | 22 ++++++++- CONTRIBUTING.md | 4 ++ docs/README.md | 1 + docs/contributing/maintainer-review.md | 28 +++++++++++ scripts/pr-evidence.test.ts | 27 ++++++++++ scripts/pr-evidence.ts | 68 ++++++++++++++++++++++++++ 7 files changed, 157 insertions(+), 3 deletions(-) create mode 100644 docs/contributing/maintainer-review.md create mode 100644 scripts/pr-evidence.test.ts create mode 100644 scripts/pr-evidence.ts diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 177bae0..6529b1c 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,4 +1,4 @@ -## What +## Summary @@ -33,6 +33,12 @@ - [ ] New provider follows the six-file structure, if applicable - [ ] No new runtime dependencies added without justification +## Behavior / risk + + + ## Validation - + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 97e2ef7..d3affcd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,6 +78,8 @@ jobs: scripts/lint-changed.sh scripts/verify.ts scripts/verify.test.ts + scripts/pr-evidence.ts + scripts/pr-evidence.test.ts scripts/release/ scripts/setup-githooks.sh ' @@ -115,6 +117,23 @@ jobs: echo "Add '[policy]' to a commit message to acknowledge changing the verification harness itself." exit 1 + maintainer-evidence: + name: Maintainer evidence + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + fetch-depth: 0 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: + bun-version: "1.3.5" + - name: Require evidence only for high-risk changes + env: + BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }} + HEAD_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + PR_BODY: ${{ github.event.pull_request.body }} + run: bun scripts/pr-evidence.ts + package-compatibility: name: Package compatibility / Node ${{ matrix.node }} runs-on: ubuntu-latest @@ -138,7 +157,7 @@ jobs: gate: name: Gate runs-on: ubuntu-latest - needs: [prepare-verification, verify, policy-surface, package-compatibility] + needs: [prepare-verification, verify, policy-surface, maintainer-evidence, package-compatibility] if: always() steps: - name: Check results @@ -146,6 +165,7 @@ jobs: if [ "${{ needs.prepare-verification.result }}" != "success" ] || \ [ "${{ needs.verify.result }}" != "success" ] || \ [ "${{ needs.policy-surface.result }}" != "success" ] || \ + [ "${{ needs.maintainer-evidence.result }}" != "success" ] || \ [ "${{ needs.package-compatibility.result }}" != "success" ]; then echo "One or more required checks failed." exit 1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bf01e89..af6e792 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,6 +39,10 @@ Every PR must satisfy: 2. At least one maintainer approval 3. No unresolved review comments +We keep the contribution path open: documentation, examples, and ordinary small changes do not need a design document or a special PR label. Changes to provider contracts, configuration/schema parsing, release/package files, or GitHub workflows have a small additional requirement: fill in the **Behavior / risk** and **Validation** sections of the PR description. This lets maintainers review the contract and its evidence before reading an implementation diff. + +Maintainers should enable the repository settings that make the same policy effective at merge time: require the `Gate` and `Analyze TypeScript` checks, require one approving review, dismiss stale approvals, require approval of the latest push, and require resolved conversations. CODEOWNERS review and a merge queue are intentionally not required for routine contributions at the current project stage. + ## Reporting bugs and requesting features Open an issue using the templates under [.github/ISSUE_TEMPLATE](./.github/ISSUE_TEMPLATE). For anything security-related, do **not** open a public issue — follow [SECURITY.md](./SECURITY.md) instead. diff --git a/docs/README.md b/docs/README.md index 4383de5..ba4f649 100644 --- a/docs/README.md +++ b/docs/README.md @@ -64,6 +64,7 @@ For external contributors. | Doc | What's inside | |-----|---------------| | [Contributing overview](../CONTRIBUTING.md) | Dev setup, merge requirements, code style. | +| [Maintainer review](./contributing/maintainer-review.md) | Evidence-based PR review and merge gates. | | [Development](./contributing/development.md) | Clone, install, run, test, package boundaries. | | [Provider development](./contributing/provider-development.md) | The six files a new provider must implement. | | [Release](./contributing/release.md) | npm publishing workflow and Trusted Publishing. | diff --git a/docs/contributing/maintainer-review.md b/docs/contributing/maintainer-review.md new file mode 100644 index 0000000..52b9f6c --- /dev/null +++ b/docs/contributing/maintainer-review.md @@ -0,0 +1,28 @@ +# Maintainer review + +OpenAgentPack welcomes exploratory and AI-assisted contributions. The merge bar is based on evidence, not on who wrote the code or how it was produced. + +## Review in this order + +1. Read **Summary**, then **Behavior / risk** and **Validation** when present. +2. Confirm that CI covers the claimed behavior. A passing test is useful only if it would fail when the stated contract regresses. +3. Read the diff around provider, configuration, credentials, release, and workflow boundaries. These paths have the greatest blast radius. +4. Resolve every conversation, then approve only the latest commit. + +## Gate levels + +| Change | Automated gate | Human review | +| --- | --- | --- | +| Documentation, examples, small isolated fixes | `Gate` | Scope and user-facing accuracy | +| CLI or SDK behavior | `Gate` | Contract plus a regression test or reproduction | +| Provider contracts, config/schema, release/package files, workflows | `Gate` plus CodeQL | Behavior/risk and validation evidence; inspect compatibility and security impact | + +The PR evidence check deliberately applies only to the third row. It does not require a design document, a linked Issue, or an AI-use declaration. + +## GitHub settings + +Configure `main` with these required checks: `Gate` and `Analyze TypeScript`. Require one approving review, dismiss stale approvals, require approval of the most recent push, require conversations to be resolved, and require branches to be up to date. Keep linear history enabled. Do not require CODEOWNERS review or a merge queue until review volume makes them worthwhile. + +## Fast disposition + +If a PR does not yet provide enough evidence, ask for one focused thing: a failing regression test, a runnable reproduction, or a clear statement of the intended behavior. If the author cannot provide it, keep the PR open as a Draft or close it with an invitation to reopen when the evidence is available. diff --git a/scripts/pr-evidence.test.ts b/scripts/pr-evidence.test.ts new file mode 100644 index 0000000..7e91ae9 --- /dev/null +++ b/scripts/pr-evidence.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, test } from "bun:test"; +import { requiresMaintainerEvidence, validateMaintainerEvidence } from "./pr-evidence.ts"; + +describe("pull-request maintainer evidence", () => { + test("does not add a merge barrier for ordinary contribution paths", () => { + expect(requiresMaintainerEvidence(["README.md", "packages/cli/src/program.ts"])).toBe(false); + expect(validateMaintainerEvidence("", ["README.md"])).toBeUndefined(); + }); + + test("asks for behavior and validation evidence on high-risk paths", () => { + const files = ["packages/sdk/src/internal/providers/interface.ts"]; + expect(requiresMaintainerEvidence(files)).toBe(true); + expect(validateMaintainerEvidence("## Summary\nA change", files)).toContain("## Behavior / risk"); + }); + + test("accepts concrete evidence for high-risk changes", () => { + const body = `## Summary +Add a provider capability. + +## Behavior / risk +Existing provider configuration remains unchanged; the new capability is opt-in. + +## Validation +\`bun test packages/sdk/tests/unit/provider-conformance.test.ts\``; + expect(validateMaintainerEvidence(body, [".github/workflows/release.yml"])).toBeUndefined(); + }); +}); diff --git a/scripts/pr-evidence.ts b/scripts/pr-evidence.ts new file mode 100644 index 0000000..217b3e5 --- /dev/null +++ b/scripts/pr-evidence.ts @@ -0,0 +1,68 @@ +import { existsSync } from "node:fs"; + +const highRiskPathPatterns = [ + /^\.github\/workflows\//, + /^scripts\/release\//, + /^(?:package\.json|bun\.lock)$/, + /^packages\/sdk\/src\/internal\/(?:parser\/|types\/(?:config|session)\.ts|providers\/(?:interface|capabilities|registry|base-client)\.ts)/, +]; + +export function requiresMaintainerEvidence(files: readonly string[]): boolean { + return files.some((file) => highRiskPathPatterns.some((pattern) => pattern.test(file))); +} + +function sectionHasContent(body: string, heading: string): boolean { + const escapedHeading = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const match = body.match(new RegExp(`^## ${escapedHeading}[ \\t]*\\r?\\n([\\s\\S]*?)(?=^## |$)`, "mi")); + if (!match) return false; + return ( + match[1] + .replace(//g, "") + .replace(/- \[ \] .*/g, "") + .trim().length > 0 + ); +} + +export function validateMaintainerEvidence(body: string, files: readonly string[]): string | undefined { + if (!requiresMaintainerEvidence(files)) return undefined; + const missing = ["Behavior / risk", "Validation"].filter((heading) => !sectionHasContent(body, heading)); + if (missing.length === 0) return undefined; + return `High-risk changes require a non-empty ${missing.map((heading) => `## ${heading}`).join(" and ")} section.`; +} + +function changedFiles(base: string, head: string): string[] { + const result = Bun.spawnSync(["git", "diff", "--name-only", "--diff-filter=ACMR", base, head], { + stdout: "pipe", + stderr: "pipe", + }); + if (result.exitCode !== 0) throw new Error(result.stderr.toString().trim() || "Could not read changed files."); + return result.stdout.toString().split("\n").filter(Boolean); +} + +function main(): number { + if (process.env.GITHUB_EVENT_NAME !== "pull_request") { + console.log("Not a pull request; maintainer-evidence check skipped."); + return 0; + } + const base = process.env.BASE_SHA; + const head = process.env.HEAD_SHA; + if (!base || !head || !existsSync(".git")) + throw new Error("BASE_SHA and HEAD_SHA are required for pull-request evidence checks."); + const files = changedFiles(base, head); + const error = validateMaintainerEvidence(process.env.PR_BODY ?? "", files); + if (!error) { + console.log( + requiresMaintainerEvidence(files) + ? "Maintainer evidence supplied." + : "Low-risk change; maintainer evidence is optional.", + ); + return 0; + } + console.error(`::error::${error}`); + console.error( + "Describe the user-visible behavior or compatibility risk, then give the test command, test case, or manual reproduction used to validate it.", + ); + return 1; +} + +if (import.meta.main) process.exit(main()); From 7355ff6a9a31e6b4729cd46ec4e5430fc10cced7 Mon Sep 17 00:00:00 2001 From: heimanba <371510756@qq.com> Date: Sun, 19 Jul 2026 15:17:11 +0800 Subject: [PATCH 2/2] fix(ci): accept spaced PR evidence sections Change-Id: I8a21b384a5d0a18f8069becce1464cefaa1250ae --- scripts/pr-evidence.test.ts | 11 +++++++++++ scripts/pr-evidence.ts | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/scripts/pr-evidence.test.ts b/scripts/pr-evidence.test.ts index 7e91ae9..48c654d 100644 --- a/scripts/pr-evidence.test.ts +++ b/scripts/pr-evidence.test.ts @@ -18,10 +18,21 @@ describe("pull-request maintainer evidence", () => { Add a provider capability. ## Behavior / risk + Existing provider configuration remains unchanged; the new capability is opt-in. ## Validation + \`bun test packages/sdk/tests/unit/provider-conformance.test.ts\``; expect(validateMaintainerEvidence(body, [".github/workflows/release.yml"])).toBeUndefined(); }); + + test("does not treat HTML comments as evidence", () => { + const body = `## Behavior / risk +/g, "") + .replace(/|$)/g, "") .replace(/- \[ \] .*/g, "") .trim().length > 0 );