pr-merge-audit 1.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.
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
@@ -0,0 +1,321 @@
1
+ Metadata-Version: 2.4
2
+ Name: pr-merge-audit
3
+ Version: 1.2.0
4
+ Summary: Merge-test a repository's open pull requests together and report which combine, conflict or break each other's tests.
5
+ License: Apache-2.0
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: Environment :: Console
8
+ Classifier: Topic :: Software Development :: Testing
9
+ Requires-Python: >=3.8
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Dynamic: license-file
13
+
14
+ # pr-merge-audit
15
+
16
+ Merge-tests the open pull requests of a repository against its base branch and writes a
17
+ Markdown report of which PRs combine cleanly, which conflict, and which break each other's tests.
18
+
19
+ No AI or network is involved after the snapshot step: the same snapshot, configuration and
20
+ machine always produce the same report.
21
+
22
+ The tool is not tied to any project, language or test runner. Everything about the repository
23
+ under test (where it lives, how its tests run, how their output reads) is in a configuration
24
+ file kept with that project, not here. `config.example.json` is a starting point.
25
+
26
+ ## Requirements
27
+
28
+ - Python 3.8 or later, standard library only
29
+ - git 2.38 or later (`git merge-tree --write-tree`)
30
+ - Linux, macOS or Windows. Each test run gets a private home and temp directory, and the tool
31
+ stops every process the run started when it ends (a process group on Linux and macOS, a job
32
+ object on Windows). The target project's own test command must of course run on that platform.
33
+ - For `snapshot` only: GitHub (github.com, or GitHub Enterprise through `api`). Optional:
34
+ `GITHUB_TOKEN` in the environment (unauthenticated API access is rate-limited).
35
+
36
+ `snapshot` is the only GitHub-specific step. For another host (GitLab, Gitea, Bitbucket), write
37
+ the two snapshot files yourself and run `build` on them:
38
+
39
+ - `snapshots/DATE.json`: `{"date", "repo", "base_ref", "base_sha", "prs": [{"number", "title",
40
+ "head", "draft", "status", "files"}]}`, where `status` is `SUCCESS` when the PR passes your
41
+ required check and `files` lists the paths it changes against the base.
42
+ - `snapshots/DATE.bundle`: a `git bundle` holding `refs/audit/base` and `refs/audit/pr/N` for
43
+ each PR.
44
+
45
+ `build` refuses a snapshot whose `base_sha` or a PR's `head` is not a full commit id, that lists a
46
+ PR number twice, or whose ids differ from the bundle's refs, before any test runs.
47
+
48
+ ## Install
49
+
50
+ ```
51
+ pip install pr-merge-audit
52
+ pr-merge-audit --help
53
+ ```
54
+
55
+ or run `python3 pr_merge_audit.py` from a clone; it has no dependencies. The commands below use
56
+ `python3 pr_merge_audit.py`; `pr-merge-audit` takes the same arguments.
57
+
58
+ ## Usage
59
+
60
+ ```
61
+ python3 pr_merge_audit.py --config PROJECT/config.json snapshot # freeze PR heads and metadata
62
+ python3 pr_merge_audit.py --config PROJECT/config.json build --snapshot snapshots/DATE.json # offline
63
+ python3 pr_merge_audit.py --config PROJECT/config.json build --snapshot snapshots/DATE.json \
64
+ --emit-branch refs/heads/integration --emit-repo PATH/TO/CLONE # also keep the merged build as a branch
65
+ python3 pr_merge_audit.py report --results results/DATE.json --out reports/DATE.md
66
+ python3 pr_merge_audit.py verify --results results/DATE.json --golden reports/DATE.md
67
+ python3 pr_merge_audit.py --config PROJECT/config.json estimate --snapshot snapshots/DATE.json \
68
+ --reference results/EARLIER.json # rough running time on this machine
69
+ ```
70
+
71
+ Paths are relative to the current directory, so run it from wherever you keep that project's
72
+ snapshots and results. `report` and `verify` need no configuration.
73
+
74
+ `snapshot` writes `snapshots/DATE.json` (PR numbers, titles, head commits, changed files,
75
+ draft flag, required-status state) and `snapshots/DATE.bundle` (every commit involved), so a
76
+ build can be re-run later even after PR branches move or are deleted.
77
+
78
+ ## Procedure
79
+
80
+ 1. **Classify.** Each open PR is either tried or skipped with a reason, in this order: excluded
81
+ in the config, required status check not successful, draft, listed as a duplicate, listed as
82
+ a feature, or changes files without touching the test directory (unless listed in
83
+ `no_tests_ok`).
84
+ 2. **Sequential build.** Tried PRs are merged with `git merge --no-ff` onto the base in the
85
+ configured order (unlisted PRs follow by number). The test command runs after each merge.
86
+ A conflict is aborted; a test failure is re-run once and the merge is reset. A failure that
87
+ passes on re-run is recorded as flaky.
88
+ 3. **Swap test.** For each conflicting PR, the merged PRs it conflicts with pairwise
89
+ (`git merge-tree`) are reverted, the PR is merged in their place, and the tests run.
90
+ 4. **Culprit search.** For each PR that fails tests without a text conflict, or still fails after
91
+ its swap, the merge sequence is bisected for a PR that makes it fail, and that pair is tested
92
+ alone on the base. Bisection assumes a failure, once it appears, stays as more PRs are merged;
93
+ a later PR that repairs an earlier interaction can mislead it. The report names a pair as
94
+ failing only when the pair fails alone; otherwise it lists the PR under Other with the
95
+ candidate bisection pointed at.
96
+ 5. **Probes.** Each file in `probes` is copied into each listed target (the base, or a PR head)
97
+ and run with the probe's own command.
98
+
99
+ Merge commits use a fixed identity and timestamp, global and system git config are ignored,
100
+ and tests run with a fixed locale, time zone and hash seed, a private `HOME` and `TMPDIR`, and
101
+ none of the caller's environment variables other than `PATH` (plus proxy and certificate settings)
102
+ and those listed in `pass_env`.
103
+
104
+ ## Keeping the merged build as a branch
105
+
106
+ `build --emit-branch REF --emit-repo PATH` keeps the result of the sequential build: a new branch
107
+ `REF` at its last commit, which is the base with every PR that merged and passed, merged in the
108
+ configured order. Land it as one pull request instead of landing the PRs one by one.
109
+
110
+ - **Which repository.** The build runs in a private repository loaded from the snapshot bundle and
111
+ deleted at the end, so the branch goes to the local git repository at `PATH` (found as
112
+ `git -C PATH` would), usually your clone of the project. Nothing else receives it.
113
+ - **What is written there.** The commits the branch needs, the one ref `REF`, and that ref's
114
+ reflog entry, which carries your own git identity and the current time (the commits keep the
115
+ fixed merge identity). No remote, tag, `FETCH_HEAD` or other ref changes, and nothing is
116
+ pushed: push the branch yourself when you are ready.
117
+ - **Checked before the build.** The two flags go together. `REF` must be a full branch name
118
+ (`refs/heads/...`) that is not a symbolic ref, and `PATH` must use the snapshot's object format
119
+ (SHA-1 or SHA-256). A dry run of the final write, which locks `REF` and lets it go, finds an
120
+ existing branch of that name and clashes such as `x` beside `x/y`. Any of these stops the build
121
+ before any test runs, with git's own message where git gave one.
122
+ - **Never overwrites.** The write itself requires that `REF` still does not exist and is not a
123
+ symbolic ref. If either has changed during the build, the results are written without the
124
+ branch and the build exits with an error. The write never follows a symbolic ref, so no other
125
+ branch is created: a symbolic ref to an existing branch makes it fail, and so does one that
126
+ points nowhere, created in the moment between the last check and the write, with recent git
127
+ (2.56 refuses it; 2.50 replaces it with `REF` itself).
128
+ - **Manifest.** The results file gains an `emitted` key: the repository's git directory, the
129
+ ref, the commit, the PRs included (in merge order) and every PR excluded with its reason
130
+ (`conflict`, `fail` or `flaky` for PRs that were tried, `not tried: ...` for the rest). The
131
+ report does not read it, so it is byte-identical to that of a build without the flag.
132
+ - **Reproducible.** Merge commits use a fixed identity and timestamp, so the same snapshot,
133
+ configuration and test outcomes give the same commit id.
134
+
135
+ Swap tests, culprit searches and probes do not change the branch. A PR that conflicts and
136
+ passes once swapped in is excluded: it conflicts with a PR on the branch, so it cannot join as it is.
137
+ If no PR merged, the branch is the base itself.
138
+
139
+ ## Security
140
+
141
+ `build` runs the test code of every PR it tries, and also runs each probe on its listed targets
142
+ whatever the filters decided about them; `estimate` runs the base's tests. That code runs as you,
143
+ with access to everything your user can reach. **Run builds on a disposable machine or
144
+ container that holds no credentials, keys or tokens.** Nothing in this tool is a sandbox.
145
+
146
+ Because a PR's code runs inside the test runner, a PR written to fake a pass can do so. A pass
147
+ needs exit status 0 and, when `count_pattern` is set, at least one test counted. That catches an
148
+ early exit or an empty run, but not output crafted to look like a real one.
149
+
150
+ What the tool does to limit damage short of that:
151
+
152
+ - Tests run in a fresh copy of the commit's files, made with `git archive`, with no `.git`
153
+ directory, in a new temporary directory with its own `HOME` and `TMPDIR`. Test code cannot
154
+ change the shared repository's config, hooks or refs through its working tree. The commit's
155
+ own `.gitattributes` cannot drop files from the copy (`export-ignore`) or rewrite them
156
+ (`export-subst`).
157
+ - No archive entry is written through a link or over an entry already there (on a
158
+ case-insensitive disk, `t` after a link `T`), and links that resolve outside the copy are
159
+ removed before the tests run. Probe files are written only if no directory on the way is a
160
+ symlink and no file of that name exists.
161
+ - Each test run gets its own process group, which is killed when the run ends or times out, so
162
+ processes the tests leave in the background stop with it (unless they start their own session).
163
+ On Windows a job object does the same; if Windows refuses to put the run in one, the tool prints
164
+ a warning and stops only the test process itself, so its children may survive the run.
165
+ - Probe names and the reasons from `exclude`, `duplicates` and status checks are escaped in the
166
+ report, so they cannot add table cells, list items, links, mentions or HTML.
167
+ - The config is checked when it is loaded: commands must be non-empty lists of strings, `workers`
168
+ and `test_timeout` positive numbers, and each output pattern a valid regular expression with a
169
+ capturing group.
170
+ - Git runs with system and global config ignored, hooks pointed at the null device,
171
+ `core.fsmonitor` off and only `PATH` (plus proxy and certificate settings) taken from your
172
+ environment. Subprocesses never go through a shell. Test runs also get the variables you list in
173
+ `pass_env` and `env`; anything listed there is visible to every PR's code.
174
+ - Test output is written to disk and at most 4 MB of it is read back.
175
+ - Text taken from test output is put in the report only inside code spans, with backticks
176
+ replaced, `|` escaped in tables, control characters removed and a length cap, so it cannot add
177
+ mentions, links, images or table rows to a comment.
178
+ - The work directory is only ever deleted if it holds the `.audit-workspace` marker the tool
179
+ wrote there.
180
+ - `GITHUB_TOKEN` is sent only to the configured API host and is not passed on by redirects.
181
+
182
+ ## How long it takes
183
+
184
+ Almost all the time goes into running the test suite, so the cost of a build is counted in
185
+ test-suite runs rather than seconds. Each results file records how many runs each phase used,
186
+ and the report states them:
187
+
188
+ - **Sequential build:** one run on the base, one per PR that merges, two per PR that fails
189
+ (the re-run that checks for flakiness), none for a conflict. These run one at a time.
190
+ - **Swap tests:** one run per conflicting PR, two if it fails.
191
+ - **Culprit searches:** about log2(number of merged PRs) + 3 bisection steps per failing PR,
192
+ each of which also rebuilds part of the merge sequence.
193
+ - **Probes:** one run per target.
194
+
195
+ Swap tests, culprit searches and probes run on `workers` parallel jobs. To estimate the
196
+ running time on your machine, time one test-suite run here and scale it by the counts of a
197
+ reference build of the same project:
198
+
199
+ ```
200
+ python3 pr_merge_audit.py --config PROJECT/config.json estimate --snapshot snapshots/DATE.json --reference results/EARLIER.json
201
+ ```
202
+
203
+ The estimate uses the counts in the reference results, scaled by how many PRs the new snapshot
204
+ would try. It is a guide, not a promise: the number of conflicts and failures, and so
205
+ the number of follow-up runs, depends on the PRs.
206
+
207
+ The results file also records the measured time of every run under `timing`, by phase:
208
+ the count, total, median and maximum seconds, and the wall-clock time of the sequential build
209
+ and of the follow-ups.
210
+
211
+ ### Serial landing, for comparison
212
+
213
+ The report also counts what landing the same PRs one at a time would take. N is the number of
214
+ PRs tried (merged or not). Landing them serially, every PR still open is re-tested after each
215
+ landing, so the runs are N + (N - 1) + ... + 1 = N(N+1)/2. The report states N, that count and
216
+ the runs this build counted, with their ratio when serial landing needs more. It gives no times,
217
+ so it stays the same from build to build.
218
+
219
+ `estimate` gives the comparison in hours for the snapshot it estimates: N is the number of
220
+ PRs that snapshot would try, and the suite time is the one test-suite run it just timed on the
221
+ base on this machine. It prints the N(N+1)/2 runs times that time in machine-hours, and N times
222
+ it as the hours to land them, assuming one suite run at a time, since each landing waits for its
223
+ run. The suite usually grows as PRs merge, so treat both as a minimum. Both the report and
224
+ `estimate` assume every PR stays until it lands.
225
+
226
+ ### A measured build
227
+
228
+ One full build of a Python project with 111 open PRs, on a 14-core laptop
229
+ with `workers: 4`. The tool tried 76 PRs. 53 merged, 18 conflicted and 5 failed.
230
+ The suite grew from 70 tests on the base to 248 after the last merge.
231
+
232
+ | Phase | Runs | Median run | Longest run |
233
+ | :--- | ---: | ---: | ---: |
234
+ | Sequential build | 64 | 76 s | 139 s |
235
+ | Swap tests | 14 | 163 s | 189 s |
236
+ | Culprit searches (5 PRs) | 57 | 62 s | 148 s |
237
+ | Probes | 3 | 0.5 s | 0.8 s |
238
+
239
+ Wall clock: 72 min for the sequential build, 38 min for the follow-ups (swaps, culprit
240
+ searches and probes on 4 workers), 110 min in total.
241
+
242
+ Use these numbers to estimate a build of your own repository:
243
+
244
+ ```
245
+ sequential ≈ (1 + merged + 2 × failed) × suite time
246
+ follow-ups ≈ (conflicts + 11 × failed) × suite time ÷ workers (at least one culprit search, ~11 runs, end to end)
247
+ total ≈ sequential + follow-ups
248
+ ```
249
+
250
+ Here "suite time" is the average time of one test-suite run across the build. Since every
251
+ merge can add tests, it grows as the build goes on, so time the suite on a branch near the
252
+ end state rather than on the base. In the example the suite ran in 68 s on average, so the
253
+ formula gives (1 + 53 + 10) × 68 s ≈ 73 min, and (18 + 55) × 68 s ÷ 4 ≈ 21 min, which is
254
+ lower than the 38 min measured. The difference is the longest culprit search, which bisects
255
+ one step at a time, and the swap tests, which run the full suite after the whole merge
256
+ sequence. Treat the follow-up figure as a minimum.
257
+
258
+ ## Configuration
259
+
260
+ | Key | Meaning |
261
+ |---|---|
262
+ | `repo`, `remote`, `api`, `base` | Repository, fetch URL, API root, base branch |
263
+ | `required_status` | Status or check name a PR must pass to be tried (empty: none) |
264
+ | `test_cmd` | Required. Test command as a list of arguments; `{python}` is replaced |
265
+ | `test_prefix`, `test_timeout` | Test directory (default `tests/`), seconds per run (default 1800) |
266
+ | `failure_pattern` | Regex whose group 1 is a failing test's name in the output (unset: none named) |
267
+ | `count_pattern` | Regex whose group 1 is the number of tests run (unset: exit status alone decides) |
268
+ | `skipped_pattern`, `message_pattern` | Regexes for the skipped count and the first error message |
269
+ | `test_files` | Regex for test files, used to decide whether a PR changes tests (default: anything under `test_prefix`) |
270
+ | `python`, `workers` | Interpreter for tests (default: the one running the tool), parallel jobs |
271
+ | `pass_env` | Names of variables copied from your environment into test runs (e.g. `RUSTUP_HOME`, `CARGO_HOME`, `GOPATH`, `VIRTUAL_ENV`) |
272
+ | `env` | Variables set in test runs, applied last (can override `HOME`) |
273
+ | `allow_incomplete_checkout` | Run even though the base uses submodules or Git LFS, which test runs lack (shown in the report) |
274
+ | `setup_cmd` | Command run before `test_cmd` in the same copy (e.g. installing dependencies); if it fails, the run fails as `(setup failed)` |
275
+ | `order` | Merge order; it decides which side of each conflict merged first |
276
+ | `exclude`, `duplicates`, `features` | PR number to reason; these PRs are not tried |
277
+ | `no_tests_ok` | PRs tried even though they change no tests |
278
+ | `probes` | `{name, file, cmd, targets}`, optional `dest`; see below |
279
+
280
+ A probe's `file` (relative to the configuration file) is written to `dest` in the target, by
281
+ default `test_prefix` + `audit_probe_` + its file name, and `cmd` is run there. In `cmd`,
282
+ `{python}`, `{dest}` and `{module}` (`dest` without its suffix, with `.` for `/`) are replaced.
283
+ Targets are `"base"` or PR numbers. Probe output is read with the same patterns as the tests. `dest`
284
+ must be a relative path inside the target: an absolute path, a drive or a `..` part stops the
285
+ build before any test runs.
286
+
287
+ ## Tests
288
+
289
+ ```
290
+ python3 -m unittest discover -s tests
291
+ ```
292
+
293
+ `tests/test_pr_merge_audit.py` builds small git repositories in temporary directories and runs the real
294
+ tool on them (no mocks): merge, conflict and swap, a cross-PR test failure found by bisection, a
295
+ PR that fails on the base alone, the not-tried reasons, probes, run counts, determinism across
296
+ two builds, test isolation from `.git`, archive and probe containment, hidden and early-exiting
297
+ tests, the emitted branch and its manifest, background processes, report escaping, output capping, the work-directory marker and
298
+ token handling on redirects. It also checks the golden report. The suite takes a few seconds.
299
+
300
+ ## Self-check
301
+
302
+ `tests/golden/` holds the results of a build of the test fixture in `tests/test_pr_merge_audit.py` and
303
+ the report they render to. Rendering is checked with:
304
+
305
+ ```
306
+ python3 pr_merge_audit.py verify --results tests/golden/results.json --golden tests/golden/report.md
307
+ ```
308
+
309
+ which renders the report twice and compares both renders with the golden copy byte for byte.
310
+ Version 1.1 adds the serial-landing line to the report, so a report written by 1.0.x no longer
311
+ matches its results under `verify`; render it again with `report` first.
312
+ After a deliberate change to the report, rebuild the fixture and replace both files.
313
+
314
+ ## Contributing and security
315
+
316
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for how the project is developed and what is enforced,
317
+ and [SECURITY.md](SECURITY.md) for the threat model and how to report a vulnerability.
318
+
319
+ ## License
320
+
321
+ Apache License 2.0; see `LICENSE`.