mergeproof 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mergeproof-0.2.0/.gitignore +13 -0
- mergeproof-0.2.0/CHANGELOG.md +34 -0
- mergeproof-0.2.0/LICENSE +21 -0
- mergeproof-0.2.0/PKG-INFO +536 -0
- mergeproof-0.2.0/README.md +505 -0
- mergeproof-0.2.0/action.yml +85 -0
- mergeproof-0.2.0/docs/logo-wordmark.svg +20 -0
- mergeproof-0.2.0/docs/logo.png +0 -0
- mergeproof-0.2.0/docs/logo.svg +31 -0
- mergeproof-0.2.0/docs/overview.svg +36 -0
- mergeproof-0.2.0/docs/pipeline.svg +32 -0
- mergeproof-0.2.0/docs/pr-comment.svg +53 -0
- mergeproof-0.2.0/examples/README.md +20 -0
- mergeproof-0.2.0/examples/github-workflow.yml +30 -0
- mergeproof-0.2.0/examples/mcp-server/README.md +38 -0
- mergeproof-0.2.0/examples/mcp-server/mergeproof.yaml +79 -0
- mergeproof-0.2.0/examples/mcp-server/scenarios/fix-awaiting-review.json +49 -0
- mergeproof-0.2.0/examples/mcp-server/scenarios/fix-verified.json +54 -0
- mergeproof-0.2.0/examples/mcp-server/scenarios/fix-without-evidence.json +49 -0
- mergeproof-0.2.0/examples/plugins/mergeproof-langfuse/README.md +37 -0
- mergeproof-0.2.0/examples/plugins/mergeproof-langfuse/pyproject.toml +24 -0
- mergeproof-0.2.0/examples/plugins/mergeproof-langfuse/src/mergeproof_langfuse/__init__.py +65 -0
- mergeproof-0.2.0/examples/plugins/mergeproof-langfuse/tests/test_langfuse.py +45 -0
- mergeproof-0.2.0/examples/python-library/README.md +16 -0
- mergeproof-0.2.0/examples/python-library/mergeproof.yaml +32 -0
- mergeproof-0.2.0/examples/python-library/scenarios/complete.json +18 -0
- mergeproof-0.2.0/examples/python-library/scenarios/missing-tests.json +11 -0
- mergeproof-0.2.0/examples/python-library/scenarios/pending-ci.json +15 -0
- mergeproof-0.2.0/examples/web-service/README.md +17 -0
- mergeproof-0.2.0/examples/web-service/mergeproof.yaml +47 -0
- mergeproof-0.2.0/examples/web-service/scenarios/auth-change-awaiting-security.json +39 -0
- mergeproof-0.2.0/examples/web-service/scenarios/migration-missing-rollback.json +28 -0
- mergeproof-0.2.0/examples/web-service/scenarios/ui-change-complete.json +30 -0
- mergeproof-0.2.0/pyproject.toml +106 -0
- mergeproof-0.2.0/src/mergeproof/__init__.py +32 -0
- mergeproof-0.2.0/src/mergeproof/__main__.py +4 -0
- mergeproof-0.2.0/src/mergeproof/checks/__init__.py +4 -0
- mergeproof-0.2.0/src/mergeproof/checks/agent_verdict.py +88 -0
- mergeproof-0.2.0/src/mergeproof/checks/base.py +71 -0
- mergeproof-0.2.0/src/mergeproof/checks/body.py +46 -0
- mergeproof-0.2.0/src/mergeproof/checks/ci_job.py +74 -0
- mergeproof-0.2.0/src/mergeproof/checks/evidence_field.py +81 -0
- mergeproof-0.2.0/src/mergeproof/checks/evidence_links.py +128 -0
- mergeproof-0.2.0/src/mergeproof/checks/files.py +47 -0
- mergeproof-0.2.0/src/mergeproof/checks/human_verified.py +65 -0
- mergeproof-0.2.0/src/mergeproof/checks/labels.py +44 -0
- mergeproof-0.2.0/src/mergeproof/checks/registry.py +66 -0
- mergeproof-0.2.0/src/mergeproof/checks/shell.py +61 -0
- mergeproof-0.2.0/src/mergeproof/checks/tests_changed.py +72 -0
- mergeproof-0.2.0/src/mergeproof/cli.py +384 -0
- mergeproof-0.2.0/src/mergeproof/context.py +79 -0
- mergeproof-0.2.0/src/mergeproof/engine.py +86 -0
- mergeproof-0.2.0/src/mergeproof/evidence.py +90 -0
- mergeproof-0.2.0/src/mergeproof/mcp_server.py +105 -0
- mergeproof-0.2.0/src/mergeproof/patterns.py +67 -0
- mergeproof-0.2.0/src/mergeproof/policy.py +135 -0
- mergeproof-0.2.0/src/mergeproof/providers/__init__.py +0 -0
- mergeproof-0.2.0/src/mergeproof/providers/git.py +98 -0
- mergeproof-0.2.0/src/mergeproof/providers/github.py +238 -0
- mergeproof-0.2.0/src/mergeproof/render/__init__.py +20 -0
- mergeproof-0.2.0/src/mergeproof/render/agent.py +71 -0
- mergeproof-0.2.0/src/mergeproof/render/junit.py +52 -0
- mergeproof-0.2.0/src/mergeproof/render/markdown.py +174 -0
- mergeproof-0.2.0/src/mergeproof/render/rdjson.py +33 -0
- mergeproof-0.2.0/src/mergeproof/render/text.py +38 -0
- mergeproof-0.2.0/src/mergeproof/report.py +131 -0
- mergeproof-0.2.0/src/mergeproof/telemetry.py +48 -0
- mergeproof-0.2.0/src/mergeproof/verifiers/__init__.py +3 -0
- mergeproof-0.2.0/src/mergeproof/verifiers/base.py +39 -0
- mergeproof-0.2.0/src/mergeproof/verifiers/http.py +32 -0
- mergeproof-0.2.0/tests/__init__.py +0 -0
- mergeproof-0.2.0/tests/integration/__init__.py +0 -0
- mergeproof-0.2.0/tests/integration/conftest.py +90 -0
- mergeproof-0.2.0/tests/integration/test_examples.py +56 -0
- mergeproof-0.2.0/tests/integration/test_mcp.py +130 -0
- mergeproof-0.2.0/tests/integration/test_pipeline.py +58 -0
- mergeproof-0.2.0/tests/integration/test_plugins.py +26 -0
- mergeproof-0.2.0/tests/unit/__init__.py +0 -0
- mergeproof-0.2.0/tests/unit/conftest.py +55 -0
- mergeproof-0.2.0/tests/unit/test_checks.py +286 -0
- mergeproof-0.2.0/tests/unit/test_cli.py +204 -0
- mergeproof-0.2.0/tests/unit/test_engine.py +140 -0
- mergeproof-0.2.0/tests/unit/test_evidence.py +50 -0
- mergeproof-0.2.0/tests/unit/test_patterns.py +29 -0
- mergeproof-0.2.0/tests/unit/test_policy.py +62 -0
- mergeproof-0.2.0/tests/unit/test_providers.py +222 -0
- mergeproof-0.2.0/tests/unit/test_render.py +174 -0
- mergeproof-0.2.0/tests/unit/test_telemetry.py +46 -0
- mergeproof-0.2.0/tests/unit/test_verifiers.py +30 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/). From 0.2.0 on, entries are generated
|
|
5
|
+
by release-please from Conventional Commit messages.
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.2.0] - 2026-09-13
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Plumbing commands `context`, `report`, `comment` and `template`; contexts and reports are JSON and pipe between them.
|
|
13
|
+
- `mergeproof mcp`: an MCP server (stdio) exposing explain, check, evidence_block, validate_policy, list_checks and agent_instructions.
|
|
14
|
+
- `mergeproof[otel]`: the MCP server exports OpenTelemetry spans over OTLP/HTTP when `OTEL_EXPORTER_OTLP_ENDPOINT` is set (Langfuse, Jaeger, Phoenix, any collector).
|
|
15
|
+
- `evidence.links`: vendor-neutral before/after link pairs with pluggable verifiers (`mergeproof.verifiers` entry-point group); `http` verifier built in.
|
|
16
|
+
- `files.changed` check.
|
|
17
|
+
- Reporting through the commit status (`--status`) and a Check Run with file annotations (`--check-run`); the action enables both by default.
|
|
18
|
+
- `-f junit` and `-f rdjson` renderings, written by the action, so test-result reporters and reviewdog can present the gate.
|
|
19
|
+
- Container image `ghcr.io/aryamanz29/mergeproof` with the CLI and MCP server: `:edge` on every push to main, `:X.Y.Z`, `:X.Y`, `:X` and `:latest` on release tags.
|
|
20
|
+
- Example projects with scenario fixtures that the integration suite executes, and a `mergeproof-langfuse` verifier plugin.
|
|
21
|
+
- Fixed exit codes: 0 pass or warn, 1 fail, 2 pending, 3 usage or policy error.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- PR comment redesigned: logo, status badge and links in the header, one plain sentence, one table with a Status column in words, numbered next steps only when something is missing, and no comment at all when no rule applies.
|
|
25
|
+
- `ci.job_passed` considers only the newest check run per name, so runs cancelled by a newer push no longer count as failures.
|
|
26
|
+
- `mergeproof checks` marks checks that only produce results in GitHub mode.
|
|
27
|
+
|
|
28
|
+
### Removed
|
|
29
|
+
- Braintrust-specific trace check (`evidence.traces`). Use `evidence.links` with a verifier plugin.
|
|
30
|
+
|
|
31
|
+
## [0.1.0] - 2026-09-12
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
- First cut: policy file, built-in checks, `check` / `explain` / `agent-prompt`, composite GitHub Action.
|
mergeproof-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aryaman Bhushan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: mergeproof
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Evidence gates for pull requests: declare what a change must prove, enforce it in CI, explain it to agents.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Aryamanz29/mergeproof
|
|
6
|
+
Project-URL: Issues, https://github.com/Aryamanz29/mergeproof/issues
|
|
7
|
+
Project-URL: Changelog, https://github.com/Aryamanz29/mergeproof/blob/main/CHANGELOG.md
|
|
8
|
+
Author: Aryaman Bhushan
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agents,ci,evidence,github-actions,guardrails,mcp,pull-request
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
20
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: httpx<1,>=0.27
|
|
23
|
+
Requires-Dist: pydantic<3,>=2.6
|
|
24
|
+
Requires-Dist: pyyaml>=6.0
|
|
25
|
+
Provides-Extra: mcp
|
|
26
|
+
Requires-Dist: mcp<3,>=2.1.1; extra == 'mcp'
|
|
27
|
+
Provides-Extra: otel
|
|
28
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http<2,>=1.30; extra == 'otel'
|
|
29
|
+
Requires-Dist: opentelemetry-sdk<2,>=1.30; extra == 'otel'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
<p align="center">
|
|
33
|
+
<img src="docs/logo.svg" alt="mergeproof" width="128">
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
<h1 align="center">mergeproof</h1>
|
|
37
|
+
|
|
38
|
+
<p align="center"><strong>Proof before merge.</strong> Pull requests earn their merge with evidence, not claims.</p>
|
|
39
|
+
|
|
40
|
+
<p align="center">
|
|
41
|
+
|
|
42
|
+
[](https://github.com/Aryamanz29/mergeproof/actions/workflows/ci.yml)
|
|
43
|
+
[](https://pypi.org/project/mergeproof/)
|
|
44
|
+
[](https://pypi.org/project/mergeproof/)
|
|
45
|
+
[](LICENSE)
|
|
46
|
+
|
|
47
|
+
</p>
|
|
48
|
+
|
|
49
|
+
`mergeproof` is an evidence gate for pull requests. One YAML file says what a change must
|
|
50
|
+
*prove* before it merges: tests for the modules it touched, a green integration job,
|
|
51
|
+
before/after links from a live environment, a human who actually opened those links. CI
|
|
52
|
+
enforces it. Coding agents read the same file, so the process stops living in review comments.
|
|
53
|
+
|
|
54
|
+

|
|
55
|
+
|
|
56
|
+
## Why
|
|
57
|
+
|
|
58
|
+
Agents are good at claiming outcomes. "Added tests, verified on staging" costs nothing to
|
|
59
|
+
write. Checklists don't help; a checkbox is self-attestation. What holds up is an artifact
|
|
60
|
+
somebody else can open: a changed test file, a trace id that resolves, a screenshot pair, a
|
|
61
|
+
sign-off tied to the exact commit. `mergeproof` checks those, verifies them against their
|
|
62
|
+
source when it can, and leaves the last word to a person who is not the author.
|
|
63
|
+
|
|
64
|
+
## Install
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
pip install mergeproof # or: uv tool install mergeproof
|
|
68
|
+
pip install 'mergeproof[mcp]' # adds the MCP server for coding agents
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Or run it without installing anything. `uvx` fetches the package into a cache on first use;
|
|
72
|
+
the container image on GHCR bundles Python, git and the `mcp` and `otel` extras:
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
uvx --from 'mergeproof[mcp]' mergeproof explain
|
|
76
|
+
docker run --rm -v "$PWD":/repo ghcr.io/aryamanz29/mergeproof check --local
|
|
77
|
+
docker run -i --rm -v "$PWD":/repo ghcr.io/aryamanz29/mergeproof mcp # MCP over stdio, -i is required
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Image tags follow the release tag: `v1.2.3` publishes `:1.2.3`, `:1.2`, `:1` and `:latest`; `:edge` tracks `main`. The image expects the repository mounted at `/repo`. For an MCP client, the same command goes in
|
|
81
|
+
the settings file: `"command": "docker", "args": ["run", "-i", "--rm", "-v", "/path/to/repo:/repo", "ghcr.io/aryamanz29/mergeproof", "mcp"]`.
|
|
82
|
+
|
|
83
|
+
## Five-minute tour
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
mergeproof init # writes a starter mergeproof.yaml
|
|
87
|
+
mergeproof checks # what checks exist and what they take
|
|
88
|
+
mergeproof explain # what must the current diff prove, what is missing right now
|
|
89
|
+
mergeproof template # the evidence block still missing, ready to paste into the PR
|
|
90
|
+
mergeproof check # the gate: exit 0 pass, 1 fail, 2 pending
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`explain` on a branch that changed a tool module and nothing else:
|
|
94
|
+
|
|
95
|
+
````text
|
|
96
|
+
# What this change must prove (fail)
|
|
97
|
+
|
|
98
|
+
## tool-change-needs-unit-tests [block]
|
|
99
|
+
Every changed tool module ships with changed unit tests for that module.
|
|
100
|
+
Triggered by `server/tools/search.py`.
|
|
101
|
+
|
|
102
|
+
- ❌ **unit tests for touched tools**: Each changed source file needs a changed test file:
|
|
103
|
+
`server/tools/{name}.py` needs `tests/unit/**/test_{name}*.py`.
|
|
104
|
+
- now: 1 changed source file(s) without test changes
|
|
105
|
+
- server/tools/search.py: expected a changed test matching tests/unit/**/test_search*.py
|
|
106
|
+
- fix: Add a regression test for each listed file: it should fail before the change and pass after.
|
|
107
|
+
- ⏳ **unit tests green**: CI check run matching `^unit` is green on the head commit.
|
|
108
|
+
- now: CI status for `^unit` is only visible in GitHub mode
|
|
109
|
+
|
|
110
|
+
## fix-needs-live-evidence [block]
|
|
111
|
+
...
|
|
112
|
+
## Evidence block to add to the PR description
|
|
113
|
+
|
|
114
|
+
```evidence
|
|
115
|
+
environment: staging
|
|
116
|
+
image: <registry>/mcp-server:pr-<n>-<sha7>
|
|
117
|
+
traces:
|
|
118
|
+
- what: <what was exercised>
|
|
119
|
+
before: https://langfuse.example.com/project/<project-id>/traces/<trace-id>
|
|
120
|
+
after: https://langfuse.example.com/project/<project-id>/traces/<trace-id>
|
|
121
|
+
```
|
|
122
|
+
````
|
|
123
|
+
|
|
124
|
+
## The policy file
|
|
125
|
+
|
|
126
|
+
```yaml
|
|
127
|
+
version: 1
|
|
128
|
+
project: my-service
|
|
129
|
+
|
|
130
|
+
rules:
|
|
131
|
+
- id: source-needs-tests
|
|
132
|
+
description: Source changes ship with tests for the touched module.
|
|
133
|
+
when:
|
|
134
|
+
paths: ["src/**/*.py"]
|
|
135
|
+
exclude_paths: ["src/**/__init__.py"]
|
|
136
|
+
require:
|
|
137
|
+
- check: tests.changed
|
|
138
|
+
with:
|
|
139
|
+
map: { "src/{pkg}/{name}.py": "tests/**/test_{name}*.py" }
|
|
140
|
+
- check: ci.job_passed
|
|
141
|
+
with: { name: "^unit", regex: true }
|
|
142
|
+
|
|
143
|
+
- id: fix-needs-live-evidence
|
|
144
|
+
description: Bug fixes show the behaviour before and after on staging.
|
|
145
|
+
when:
|
|
146
|
+
title: "^fix"
|
|
147
|
+
instructions: Reproduce on staging, keep the link, deploy, repeat, keep that link too.
|
|
148
|
+
require:
|
|
149
|
+
- check: evidence.field
|
|
150
|
+
with: { key: environment, equals: staging }
|
|
151
|
+
- check: evidence.links
|
|
152
|
+
with: { min_pairs: 1 }
|
|
153
|
+
- check: review.human_verified
|
|
154
|
+
with: { phrase: "/verified", bind_to_head: true }
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
A rule has a `when` (which changes it applies to) and a list of `require`ments, each a check
|
|
158
|
+
with parameters. `severity: warn` on a rule or a requirement reports without blocking.
|
|
159
|
+
|
|
160
|
+
`when` matchers: `paths`, `exclude_paths`, `labels`, `title` (regex), `base_branches`,
|
|
161
|
+
`authors`. That last one lets you hold `copilot[bot]` or `claude[bot]` to a stricter rule set
|
|
162
|
+
than people.
|
|
163
|
+
|
|
164
|
+
## Evidence lives in the PR description
|
|
165
|
+
|
|
166
|
+
A fenced YAML block. Agents can write it, machines can read it, humans can still skim it.
|
|
167
|
+
|
|
168
|
+
````markdown
|
|
169
|
+
```evidence
|
|
170
|
+
environment: staging
|
|
171
|
+
image: registry.example.com/mcp-server:pr-77-4444444
|
|
172
|
+
traces:
|
|
173
|
+
- what: search with empty query
|
|
174
|
+
before: https://langfuse.example.com/project/p1/traces/trace-before-1
|
|
175
|
+
after: https://langfuse.example.com/project/p1/traces/trace-after-1
|
|
176
|
+
```
|
|
177
|
+
````
|
|
178
|
+
|
|
179
|
+
`mergeproof template` prints exactly the block a change still needs. Multiple blocks merge.
|
|
180
|
+
|
|
181
|
+
## What the PR sees
|
|
182
|
+
|
|
183
|
+
One comment, updated in place on every push, label change, CI completion and review comment:
|
|
184
|
+
|
|
185
|
+

|
|
186
|
+
|
|
187
|
+
Wire it up with the action (copy [`examples/github-workflow.yml`](examples/github-workflow.yml)):
|
|
188
|
+
|
|
189
|
+
```yaml
|
|
190
|
+
on:
|
|
191
|
+
pull_request:
|
|
192
|
+
types: [opened, synchronize, reopened, edited, labeled, unlabeled]
|
|
193
|
+
issue_comment: { types: [created, edited] } # /verified and agent verdicts land here
|
|
194
|
+
check_suite: { types: [completed] } # re-check when CI finishes
|
|
195
|
+
jobs:
|
|
196
|
+
gate:
|
|
197
|
+
if: github.event_name != 'issue_comment' || github.event.issue.pull_request
|
|
198
|
+
runs-on: ubuntu-latest
|
|
199
|
+
permissions: { contents: read, pull-requests: write, statuses: write, checks: write }
|
|
200
|
+
steps:
|
|
201
|
+
- uses: actions/checkout@v4
|
|
202
|
+
with: { ref: ${{ github.event.repository.default_branch }} } # policy from the base branch
|
|
203
|
+
- uses: Aryamanz29/mergeproof@v0.2.0
|
|
204
|
+
env:
|
|
205
|
+
MERGEPROOF_PR_NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
The action reports in three places, each switchable: the sticky comment above, a **commit status**
|
|
209
|
+
named `mergeproof` in the merge box with the headline (`3 of 5 requirements satisfied, 2 pending`),
|
|
210
|
+
and a **Check Run** in the Checks tab whose annotations land on the files concerned, for example
|
|
211
|
+
"expected a changed test matching tests/**/test_search*.py" on `server/tools/search.py`. Require
|
|
212
|
+
the `mergeproof` status in branch protection and the gate is enforced.
|
|
213
|
+
|
|
214
|
+
None of this blocks a merge until the repository says so. GitHub merges anything unless a
|
|
215
|
+
ruleset requires specific checks (rulesets need a public repository or a paid plan), so create
|
|
216
|
+
one that requires the `mergeproof` status next to the CI jobs you already trust:
|
|
217
|
+
|
|
218
|
+
```sh
|
|
219
|
+
gh api -X POST repos/OWNER/REPO/rulesets --input ruleset.json
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
```json
|
|
223
|
+
{ "name": "main: pull requests with evidence", "target": "branch", "enforcement": "active",
|
|
224
|
+
"conditions": { "ref_name": { "include": ["~DEFAULT_BRANCH"], "exclude": [] } },
|
|
225
|
+
"rules": [
|
|
226
|
+
{ "type": "pull_request", "parameters": { "required_approving_review_count": 0,
|
|
227
|
+
"dismiss_stale_reviews_on_push": true, "require_code_owner_review": false,
|
|
228
|
+
"require_last_push_approval": false, "required_review_thread_resolution": false } },
|
|
229
|
+
{ "type": "required_status_checks", "parameters": { "strict_required_status_checks_policy": false,
|
|
230
|
+
"required_status_checks": [ { "context": "mergeproof" }, { "context": "unit" } ] } } ] }
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
With `pending-ok: "true"` the workflow job stays green while evidence is outstanding, but the
|
|
234
|
+
`mergeproof` status stays `pending`, and a pending required status blocks the merge button. That
|
|
235
|
+
split is deliberate: the job says the tool ran, the status says whether the evidence is there.
|
|
236
|
+
|
|
237
|
+
### Rendering with tools you already use
|
|
238
|
+
|
|
239
|
+
The action also writes the report as **JUnit XML** (`mergeproof-junit.xml`, one suite per rule,
|
|
240
|
+
one case per requirement) and in **reviewdog's format** (`mergeproof.rdjson`, one diagnostic per
|
|
241
|
+
file annotation). Any renderer for those formats then does the presentation:
|
|
242
|
+
|
|
243
|
+
```yaml
|
|
244
|
+
- uses: Aryamanz29/mergeproof@v0.2.0
|
|
245
|
+
id: gate
|
|
246
|
+
- uses: EnricoMi/publish-unit-test-result-action@v2 # rich check run: counts, per-requirement detail, trends
|
|
247
|
+
if: always()
|
|
248
|
+
with: { files: mergeproof-junit.xml, check_name: mergeproof requirements, comment_mode: off }
|
|
249
|
+
- uses: reviewdog/action-setup@v1 # inline review comments on the files concerned
|
|
250
|
+
- run: reviewdog -f=rdjson -reporter=github-pr-review -level=error < mergeproof.rdjson
|
|
251
|
+
env: { REVIEWDOG_GITHUB_API_TOKEN: ${{ github.token }} }
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
`dorny/test-reporter` reads the same JUnit file. Locally, `mergeproof check -f junit` and
|
|
255
|
+
`-f rdjson` print the same documents.
|
|
256
|
+
|
|
257
|
+
By default all three appear under `github-actions[bot]` with GitHub's avatar. To have them show
|
|
258
|
+
as **mergeproof** with the shield, create a GitHub App named mergeproof (avatar `docs/logo.svg`,
|
|
259
|
+
permissions: pull requests write, checks write, commit statuses write), install it on the repo,
|
|
260
|
+
and mint its token in the workflow:
|
|
261
|
+
|
|
262
|
+
```yaml
|
|
263
|
+
- uses: actions/create-github-app-token@v1
|
|
264
|
+
id: app
|
|
265
|
+
with:
|
|
266
|
+
app-id: ${{ vars.MERGEPROOF_APP_ID }}
|
|
267
|
+
private-key: ${{ secrets.MERGEPROOF_APP_KEY }}
|
|
268
|
+
- uses: Aryamanz29/mergeproof@v0.2.0
|
|
269
|
+
with:
|
|
270
|
+
github-token: ${{ steps.app.outputs.token }}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
The comment, the status and the Check Run are then attributed to the app, and a later push from
|
|
274
|
+
the app's token does not trigger workflows recursively, which the default token would not either.
|
|
275
|
+
|
|
276
|
+
## Built-in checks
|
|
277
|
+
|
|
278
|
+
| check | proves |
|
|
279
|
+
|---|---|
|
|
280
|
+
| `tests.changed` | each changed source file (via `{capture}` globs) or any file has a changed test |
|
|
281
|
+
| `files.changed` | the change touches / avoids certain paths (a down-migration, a changelog entry) |
|
|
282
|
+
| `evidence.field` | a key in the evidence block exists, equals, matches, or has N items |
|
|
283
|
+
| `evidence.links` | before/after link pairs matching a pattern; optional verification through a verifier |
|
|
284
|
+
| `ci.job_passed` | a named check run on the head commit succeeded (pending while it runs) |
|
|
285
|
+
| `review.human_verified` | a non-author, non-bot comment `/verified <sha7>`; a new push invalidates it |
|
|
286
|
+
| `agent.verdict` | an allowed bot posted a head-bound ```verdict block with `verdict: pass` |
|
|
287
|
+
| `pr.labels`, `pr.body` | labels present or absent; required sections, regex, minimum length |
|
|
288
|
+
| `shell` | a command from the policy exits 0 (changed files in `$MERGEPROOF_FILES`) |
|
|
289
|
+
|
|
290
|
+
`mergeproof checks` prints every parameter with its default.
|
|
291
|
+
|
|
292
|
+
## Verifying links against their source
|
|
293
|
+
|
|
294
|
+
A pasted link should have to be real. `evidence.links` accepts a `pattern` with named groups and
|
|
295
|
+
a `verify` name:
|
|
296
|
+
|
|
297
|
+
```yaml
|
|
298
|
+
- check: evidence.links
|
|
299
|
+
with:
|
|
300
|
+
key: traces
|
|
301
|
+
pattern: "^(?P<host>https?://[^/]+)/project/(?P<project>[^/]+)/traces/(?P<trace_id>[\\w-]+)"
|
|
302
|
+
verify: langfuse
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
The core ships `http` (the URL answers 2xx; `verify_options: {auth_header_env: DASH_TOKEN}` for
|
|
306
|
+
private dashboards). Anything vendor-specific is a plugin: a class with `verify(url, match)`,
|
|
307
|
+
registered under the `mergeproof.verifiers` entry-point group.
|
|
308
|
+
[`examples/plugins/mergeproof-langfuse`](examples/plugins/mergeproof-langfuse) is a complete one in
|
|
309
|
+
forty lines, for [Langfuse](https://langfuse.com), an MIT-licensed tracer you can self-host and
|
|
310
|
+
feed from OpenTelemetry. Jaeger and Arize Phoenix work the same way.
|
|
311
|
+
|
|
312
|
+
## Non-deterministic reviewers
|
|
313
|
+
|
|
314
|
+
LLM review agents are witnesses, not the gate. Have yours post a comment:
|
|
315
|
+
|
|
316
|
+
````markdown
|
|
317
|
+
```verdict
|
|
318
|
+
check: trace-review
|
|
319
|
+
verdict: pass
|
|
320
|
+
head: 4444444
|
|
321
|
+
confidence: 0.85
|
|
322
|
+
summary: the after-trace returns the empty page; the before-trace raises.
|
|
323
|
+
```
|
|
324
|
+
````
|
|
325
|
+
|
|
326
|
+
and require it:
|
|
327
|
+
|
|
328
|
+
```yaml
|
|
329
|
+
- check: agent.verdict
|
|
330
|
+
with: { name: trace-review, authors: ["github-actions[bot]"], min_confidence: 0.8 }
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
`authors` pins the verdict to the bot that runs the agent, `bind_to_head` (default) discards it on
|
|
334
|
+
the next push, and pairing it with `review.human_verified` keeps a person in the loop. If your
|
|
335
|
+
agent already speaks in labels (`reviewed` / `review-failed`), `pr.labels` consumes those.
|
|
336
|
+
|
|
337
|
+
## For coding agents
|
|
338
|
+
|
|
339
|
+
Agents get the rules two ways. Both are generated from the policy, so what an agent reads is
|
|
340
|
+
what CI enforces.
|
|
341
|
+
|
|
342
|
+
**A Markdown section** for `AGENTS.md` or `CLAUDE.md`:
|
|
343
|
+
|
|
344
|
+
```sh
|
|
345
|
+
mergeproof agent-prompt >> AGENTS.md
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
It lists every rule, when it applies, what satisfies each requirement, and the evidence block
|
|
349
|
+
to fill in, followed by three rules for agents: never fabricate evidence, never post the human
|
|
350
|
+
verification phrase, keep the evidence block plain YAML.
|
|
351
|
+
|
|
352
|
+
**An MCP server** over stdio, for agents that can call tools:
|
|
353
|
+
|
|
354
|
+
```sh
|
|
355
|
+
pip install 'mergeproof[mcp]'
|
|
356
|
+
mergeproof mcp --policy mergeproof.yaml # what the client launches
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Register it once per repository. Claude Code reads `.mcp.json` at the repo root; Cursor and
|
|
360
|
+
other clients take the same command and arguments in their own settings file:
|
|
361
|
+
|
|
362
|
+
```json
|
|
363
|
+
{
|
|
364
|
+
"mcpServers": {
|
|
365
|
+
"mergeproof": { "command": "mergeproof", "args": ["mcp", "--policy", "mergeproof.yaml"] }
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
The server exposes six tools and one resource:
|
|
371
|
+
|
|
372
|
+
| tool | arguments | returns | an agent calls it to |
|
|
373
|
+
|---|---|---|---|
|
|
374
|
+
| `explain` | `pr_title?`, `pr_body?` | `verdict`, `markdown`, `evidence_template` | learn what the current diff must prove, and what is already covered, before writing more code |
|
|
375
|
+
| `check` | `pr_title?`, `pr_body?` | `verdict`, `exit_code`, the full JSON report | dry-run the gate against the working tree with the PR description it is about to submit |
|
|
376
|
+
| `evidence_block` | `data` (mapping) | the fenced block as text | format the evidence for the PR description instead of hand-writing YAML |
|
|
377
|
+
| `validate_policy` | `text?` | `ok`, `problems`, `rules` | edit `mergeproof.yaml` safely; without `text` it validates the repo's file |
|
|
378
|
+
| `list_checks` | none | id, description and parameter schema for every check, plugins included | write or change rules with the right check ids and parameter names |
|
|
379
|
+
| `agent_instructions` | none | the same Markdown `agent-prompt` prints | refresh the rules mid-session |
|
|
380
|
+
|
|
381
|
+
Resource `mergeproof://policy` is the policy file itself.
|
|
382
|
+
|
|
383
|
+
Everything is read-only against the working tree. The server cannot post comments, add labels,
|
|
384
|
+
or produce the human sign-off; an agent can find out what to prove and check its own work, and
|
|
385
|
+
that is all. A typical session:
|
|
386
|
+
|
|
387
|
+
```mermaid
|
|
388
|
+
sequenceDiagram
|
|
389
|
+
participant A as Coding agent
|
|
390
|
+
participant M as mergeproof mcp
|
|
391
|
+
participant G as GitHub
|
|
392
|
+
A->>M: explain()
|
|
393
|
+
M-->>A: fail: tests missing for tools/search.py, evidence block missing
|
|
394
|
+
A->>A: write the regression test, reproduce on staging, keep both trace links
|
|
395
|
+
A->>M: evidence_block({environment, image, traces})
|
|
396
|
+
M-->>A: fenced evidence block
|
|
397
|
+
A->>M: check(pr_body)
|
|
398
|
+
M-->>A: pending: only CI status and reviewer sign-off remain
|
|
399
|
+
A->>G: open the PR with that description
|
|
400
|
+
G->>G: CI runs mergeproof check --github --comment
|
|
401
|
+
Note over G: a human opens the traces and posts /verified sha7
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
The CLI and the server answer the same questions:
|
|
405
|
+
|
|
406
|
+
| CLI | MCP tool |
|
|
407
|
+
|---|---|
|
|
408
|
+
| `mergeproof explain` | `explain` |
|
|
409
|
+
| `mergeproof check --body-file FILE` | `check` |
|
|
410
|
+
| `mergeproof template` | `explain`, field `evidence_template` |
|
|
411
|
+
| `mergeproof validate` | `validate_policy` |
|
|
412
|
+
| `mergeproof checks` | `list_checks` |
|
|
413
|
+
| `mergeproof agent-prompt` | `agent_instructions` |
|
|
414
|
+
|
|
415
|
+
### Tracing the server itself
|
|
416
|
+
|
|
417
|
+
The MCP SDK wraps every tool call in an OpenTelemetry span. Install the `otel` extra and point the
|
|
418
|
+
standard variables at any OTLP/HTTP receiver and those spans are exported; nothing is sent
|
|
419
|
+
otherwise.
|
|
420
|
+
|
|
421
|
+
```sh
|
|
422
|
+
pip install 'mergeproof[mcp,otel]'
|
|
423
|
+
|
|
424
|
+
# Langfuse (self-hosted or cloud): basic auth with the project keys
|
|
425
|
+
export OTEL_EXPORTER_OTLP_ENDPOINT=https://langfuse.example.com/api/public/otel
|
|
426
|
+
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic $(printf '%s:%s' "$LANGFUSE_PUBLIC_KEY" "$LANGFUSE_SECRET_KEY" | base64)"
|
|
427
|
+
|
|
428
|
+
# Jaeger or an OpenTelemetry Collector on the default OTLP/HTTP port
|
|
429
|
+
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
Each span is named after the MCP method and tool (`tools/call explain`) and carries the
|
|
433
|
+
`gen_ai.tool.name` attribute, so a dashboard can answer "which agent asked what, and how often did
|
|
434
|
+
`check` come back failing" without any code in this project knowing which backend it talks to.
|
|
435
|
+
|
|
436
|
+
### Tracing the server itself
|
|
437
|
+
|
|
438
|
+
The MCP SDK wraps every tool call in an OpenTelemetry span. Install the `otel` extra and point the
|
|
439
|
+
standard variables at any OTLP/HTTP receiver and those spans are exported; nothing is sent
|
|
440
|
+
otherwise.
|
|
441
|
+
|
|
442
|
+
```sh
|
|
443
|
+
pip install 'mergeproof[mcp,otel]'
|
|
444
|
+
|
|
445
|
+
# Langfuse (self-hosted or cloud): basic auth with the project keys
|
|
446
|
+
export OTEL_EXPORTER_OTLP_ENDPOINT=https://langfuse.example.com/api/public/otel
|
|
447
|
+
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic $(printf '%s:%s' "$LANGFUSE_PUBLIC_KEY" "$LANGFUSE_SECRET_KEY" | base64)"
|
|
448
|
+
|
|
449
|
+
# Jaeger or an OpenTelemetry Collector on the default OTLP/HTTP port
|
|
450
|
+
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
Each span is named after the MCP method and tool (`tools/call explain`) and carries the
|
|
454
|
+
`gen_ai.tool.name` attribute, so a dashboard can answer "which agent asked what, and how often did
|
|
455
|
+
`check` come back failing" without any code in this project knowing which backend it talks to.
|
|
456
|
+
|
|
457
|
+
## Plumbing
|
|
458
|
+
|
|
459
|
+
The porcelain commands are compositions of filters that read and write JSON:
|
|
460
|
+
|
|
461
|
+

|
|
462
|
+
|
|
463
|
+
```sh
|
|
464
|
+
mergeproof context --github > pr.json # snapshot a PR (files, body, labels, comments, check runs)
|
|
465
|
+
mergeproof check --context pr.json -f json > report.json
|
|
466
|
+
mergeproof report report.json -f md # render later, elsewhere
|
|
467
|
+
mergeproof comment report.json # post it
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
Snapshots make policies testable: the [examples](examples/) ship scenario files that the
|
|
471
|
+
integration suite runs through the CLI, asserting the verdict each one claims.
|
|
472
|
+
|
|
473
|
+
## Writing a check
|
|
474
|
+
|
|
475
|
+
```python
|
|
476
|
+
from pydantic import BaseModel
|
|
477
|
+
from mergeproof import Check, Context, Outcome, Status
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
class ImageSmokeTested(Check):
|
|
481
|
+
id = "image.smoke_tested"
|
|
482
|
+
description = "The image named in the evidence block was exercised against the environment."
|
|
483
|
+
|
|
484
|
+
class Params(BaseModel):
|
|
485
|
+
registry: str
|
|
486
|
+
|
|
487
|
+
def run(self, ctx: Context, params: Params, files: list[str]) -> Outcome:
|
|
488
|
+
tag = ctx.evidence().get("image")
|
|
489
|
+
if not tag or not tag.startswith(params.registry):
|
|
490
|
+
return Outcome(
|
|
491
|
+
status=Status.FAIL,
|
|
492
|
+
summary="no image under the expected registry",
|
|
493
|
+
fix="Build the branch image and record its tag under `image`.",
|
|
494
|
+
)
|
|
495
|
+
# query your deployment API here
|
|
496
|
+
return Outcome(status=Status.PASS, summary=f"{tag} smoke-tested")
|
|
497
|
+
|
|
498
|
+
def explain(self, params: Params) -> str:
|
|
499
|
+
return f"An image under `{params.registry}` recorded as `image` and smoke-tested."
|
|
500
|
+
|
|
501
|
+
def evidence_template(self, params: Params) -> dict:
|
|
502
|
+
return {"image": f"{params.registry}/<name>:pr-<n>-<sha7>"}
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
```toml
|
|
506
|
+
[project.entry-points."mergeproof.checks"]
|
|
507
|
+
"image.smoke_tested" = "mypkg.checks:ImageSmokeTested"
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
## Prior art
|
|
511
|
+
|
|
512
|
+
[Danger](https://danger.systems/js/) runs rules written in JavaScript and comments on the PR;
|
|
513
|
+
`mergeproof` is declarative, has a notion of evidence, and explains itself to agents.
|
|
514
|
+
[policy-bot](https://github.com/palantir/policy-bot) enforces rich approval policies but only sees
|
|
515
|
+
GitHub-native data. Checklist actions fail on unticked boxes, which is the thing agents will tick.
|
|
516
|
+
Hosted "evidence gate" products evaluate your artifacts on their servers. `mergeproof` borrows
|
|
517
|
+
policy-bot's matchers and Danger's sticky comment and runs entirely in your CI.
|
|
518
|
+
|
|
519
|
+
## Exit codes
|
|
520
|
+
|
|
521
|
+
| code | meaning |
|
|
522
|
+
|---|---|
|
|
523
|
+
| 0 | pass, or only warnings |
|
|
524
|
+
| 1 | a blocking requirement failed or errored |
|
|
525
|
+
| 2 | a blocking requirement is pending (CI running, reviewer not yet verified) |
|
|
526
|
+
| 3 | usage or policy error |
|
|
527
|
+
|
|
528
|
+
## Development
|
|
529
|
+
|
|
530
|
+
```sh
|
|
531
|
+
make setup # uv sync, example plugin, pre-commit hooks
|
|
532
|
+
make lint typecheck test integration
|
|
533
|
+
make check # this repository's own gate against your working tree
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
MIT. See [CONTRIBUTING.md](CONTRIBUTING.md).
|