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.
- pr_merge_audit-1.2.0/LICENSE +202 -0
- pr_merge_audit-1.2.0/PKG-INFO +321 -0
- pr_merge_audit-1.2.0/README.md +308 -0
- pr_merge_audit-1.2.0/pr_merge_audit.egg-info/PKG-INFO +321 -0
- pr_merge_audit-1.2.0/pr_merge_audit.egg-info/SOURCES.txt +10 -0
- pr_merge_audit-1.2.0/pr_merge_audit.egg-info/dependency_links.txt +1 -0
- pr_merge_audit-1.2.0/pr_merge_audit.egg-info/entry_points.txt +2 -0
- pr_merge_audit-1.2.0/pr_merge_audit.egg-info/top_level.txt +1 -0
- pr_merge_audit-1.2.0/pr_merge_audit.py +1654 -0
- pr_merge_audit-1.2.0/pyproject.toml +23 -0
- pr_merge_audit-1.2.0/setup.cfg +4 -0
- pr_merge_audit-1.2.0/tests/test_pr_merge_audit.py +1402 -0
|
@@ -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`.
|