release-state-reconcile 0.1.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.
- release_state_reconcile-0.1.0/LICENSE +21 -0
- release_state_reconcile-0.1.0/PKG-INFO +177 -0
- release_state_reconcile-0.1.0/README.md +133 -0
- release_state_reconcile-0.1.0/pyproject.toml +40 -0
- release_state_reconcile-0.1.0/setup.cfg +4 -0
- release_state_reconcile-0.1.0/src/release_state_reconcile/__init__.py +5 -0
- release_state_reconcile-0.1.0/src/release_state_reconcile/__main__.py +4 -0
- release_state_reconcile-0.1.0/src/release_state_reconcile/cli.py +42 -0
- release_state_reconcile-0.1.0/src/release_state_reconcile/github.py +107 -0
- release_state_reconcile-0.1.0/src/release_state_reconcile/models.py +70 -0
- release_state_reconcile-0.1.0/src/release_state_reconcile/reconcile.py +776 -0
- release_state_reconcile-0.1.0/src/release_state_reconcile/report.py +105 -0
- release_state_reconcile-0.1.0/src/release_state_reconcile.egg-info/PKG-INFO +177 -0
- release_state_reconcile-0.1.0/src/release_state_reconcile.egg-info/SOURCES.txt +16 -0
- release_state_reconcile-0.1.0/src/release_state_reconcile.egg-info/dependency_links.txt +1 -0
- release_state_reconcile-0.1.0/src/release_state_reconcile.egg-info/entry_points.txt +2 -0
- release_state_reconcile-0.1.0/src/release_state_reconcile.egg-info/top_level.txt +1 -0
- release_state_reconcile-0.1.0/tests/test_reconcile.py +416 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 LaimaWu
|
|
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,177 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: release-state-reconcile
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Read-only CLI for reconciling release candidates across fixes, backports, checks, release notes, and published releases.
|
|
5
|
+
Author: LaimaWu
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 LaimaWu
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Repository, https://github.com/LaimaWu/release-state-reconcile
|
|
29
|
+
Project-URL: Issues, https://github.com/LaimaWu/release-state-reconcile/issues
|
|
30
|
+
Classifier: Development Status :: 3 - Alpha
|
|
31
|
+
Classifier: Environment :: Console
|
|
32
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
33
|
+
Classifier: Operating System :: OS Independent
|
|
34
|
+
Classifier: Programming Language :: Python :: 3
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
39
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
40
|
+
Requires-Python: >=3.10
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
License-File: LICENSE
|
|
43
|
+
Dynamic: license-file
|
|
44
|
+
|
|
45
|
+
# Release State Reconcile
|
|
46
|
+
|
|
47
|
+
Read-only CLI for reconciling release candidates across fixes, backports, checks, release notes, and published releases.
|
|
48
|
+
|
|
49
|
+
Given one public GitHub repository, one selected release candidate, and one release line, Release State Reconcile reconstructs the public evidence chain across implementation, backports, checks, release-note evidence, and published releases, surfacing missing links, branch mismatches, contradictions, and `UNKNOWN` states without mutating GitHub.
|
|
50
|
+
|
|
51
|
+
## Why this exists
|
|
52
|
+
|
|
53
|
+
Release state is often spread across an issue, one or more pull requests, CI checks, changed files, maintenance branches, and release tags. This tool assembles those public facts into one deterministic report and keeps uncertainty explicit. It supports investigation and review; it does not make release decisions.
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
Python 3.10 or newer is required.
|
|
58
|
+
|
|
59
|
+
### Stable GitHub release
|
|
60
|
+
|
|
61
|
+
Install the verified wheel attached to the `v0.1.0` GitHub Release:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
python -m pip install https://github.com/LaimaWu/release-state-reconcile/releases/download/v0.1.0/release_state_reconcile-0.1.0-py3-none-any.whl
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Tagged Git source
|
|
68
|
+
|
|
69
|
+
Alternatively, install from the exact `v0.1.0` tag:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
python -m pip install "git+https://github.com/LaimaWu/release-state-reconcile.git@v0.1.0"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
If you use `pipx`, the same tagged source can be installed as an isolated CLI application:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pipx install "git+https://github.com/LaimaWu/release-state-reconcile.git@v0.1.0"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### PyPI status
|
|
82
|
+
|
|
83
|
+
PyPI distribution is planned but not yet available. Do not use `pip install release-state-reconcile` until the package is published on PyPI.
|
|
84
|
+
|
|
85
|
+
### Contributor and development install
|
|
86
|
+
|
|
87
|
+
From a source checkout, contributors can retain an editable installation:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
python -m pip install -e .
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## CLI
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
release-state-reconcile \
|
|
97
|
+
--candidate https://github.com/OWNER/REPOSITORY/issues/123 \
|
|
98
|
+
--release-branch release/1.2 \
|
|
99
|
+
--config repository.json \
|
|
100
|
+
--output report.md
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Use `--format json` for structured output. If `--output` is omitted, the report is written to standard output.
|
|
104
|
+
|
|
105
|
+
## Output shape
|
|
106
|
+
|
|
107
|
+
The Markdown report includes an evidence-linked state table, related pull requests, explicit same- and cross-repository relationships, contradictions, and unknown or missing links.
|
|
108
|
+
|
|
109
|
+
```text
|
|
110
|
+
Fact State
|
|
111
|
+
Original issue/PR CLOSED
|
|
112
|
+
Mainline merge MERGED | OPEN | UNKNOWN
|
|
113
|
+
Required backport PRs MERGED | MISSING | TARGET_BRANCH_MISMATCH | UNKNOWN
|
|
114
|
+
Backport/check state PASS | FAIL | PENDING | UNKNOWN
|
|
115
|
+
Release-note evidence PRESENT | ABSENT | UNKNOWN
|
|
116
|
+
Final tag/release containment CONTAINED | NOT_CONTAINED | UNKNOWN
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Each observation carries a public source URL, object type, object ID, observed state, and confidence.
|
|
120
|
+
|
|
121
|
+
## Configuration
|
|
122
|
+
|
|
123
|
+
Repository conventions live in JSON rather than source code. A minimal configuration looks like this:
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"mainline_branch": "main",
|
|
128
|
+
"backport": {"required": true, "search": true},
|
|
129
|
+
"completion": {"closed_is_complete": true, "labels": []},
|
|
130
|
+
"verification_labels": ["verified"],
|
|
131
|
+
"release_note": {"path_globs": ["changes/**"]},
|
|
132
|
+
"release": {"tag_regex": "^v1\\.2\\.[0-9]+$", "max_releases": 10}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The selected release branch is always supplied explicitly on the command line. Configuration may describe the default branch, whether a backport is expected, repository-specific verification labels, release-note paths, and the release-tag convention.
|
|
137
|
+
|
|
138
|
+
## Scope and non-goals
|
|
139
|
+
|
|
140
|
+
The tool observes and reconciles public GitHub state. It does not:
|
|
141
|
+
|
|
142
|
+
- decide whether a candidate belongs in a release;
|
|
143
|
+
- assess change risk or release-note quality;
|
|
144
|
+
- approve, merge, label, comment on, or otherwise mutate GitHub objects;
|
|
145
|
+
- orchestrate releases across multiple release lines;
|
|
146
|
+
- provide end-to-end release automation; or
|
|
147
|
+
- use an LLM or make AI-generated release decisions.
|
|
148
|
+
|
|
149
|
+
## Read-only and security model
|
|
150
|
+
|
|
151
|
+
The GitHub client exposes only HTTP `GET` operations. Authentication is optional: `GITHUB_TOKEN` or `GH_TOKEN` may be supplied to increase public REST API limits, but the tool has no GitHub write methods. Reports may be written to a local path selected by the caller.
|
|
152
|
+
|
|
153
|
+
The runtime uses the Python standard library and the public GitHub REST API. Do not place tokens in configuration files or commit them to source control. See [SECURITY.md](SECURITY.md) for vulnerability reporting guidance.
|
|
154
|
+
|
|
155
|
+
## Validation
|
|
156
|
+
|
|
157
|
+
The repository contains 21 deterministic unit tests covering closure semantics, relationship discovery, cross-repository preservation, target-branch mismatch handling, external-provider uncertainty, check aggregation, release-note path evidence, and the GET-only client surface. CI runs the full suite on Python 3.10, 3.11, 3.12, and 3.13.
|
|
158
|
+
|
|
159
|
+
Run locally with:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
python -m unittest discover -s tests -v
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Known limitations
|
|
166
|
+
|
|
167
|
+
- One repository and one selected release line are evaluated per run.
|
|
168
|
+
- Repository conventions may require configuration.
|
|
169
|
+
- Unsupported external change providers remain `UNKNOWN`.
|
|
170
|
+
- Private CI and off-GitHub approvals are outside scope.
|
|
171
|
+
- Candidate eligibility and release-note quality remain human judgment.
|
|
172
|
+
- Some public issues do not expose enough relationships to reconstruct a useful chain.
|
|
173
|
+
- A complete GitHub search establishes only that no qualifying result was returned for that query; it is not proof that an unreferenced change does not exist.
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Release State Reconcile
|
|
2
|
+
|
|
3
|
+
Read-only CLI for reconciling release candidates across fixes, backports, checks, release notes, and published releases.
|
|
4
|
+
|
|
5
|
+
Given one public GitHub repository, one selected release candidate, and one release line, Release State Reconcile reconstructs the public evidence chain across implementation, backports, checks, release-note evidence, and published releases, surfacing missing links, branch mismatches, contradictions, and `UNKNOWN` states without mutating GitHub.
|
|
6
|
+
|
|
7
|
+
## Why this exists
|
|
8
|
+
|
|
9
|
+
Release state is often spread across an issue, one or more pull requests, CI checks, changed files, maintenance branches, and release tags. This tool assembles those public facts into one deterministic report and keeps uncertainty explicit. It supports investigation and review; it does not make release decisions.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
Python 3.10 or newer is required.
|
|
14
|
+
|
|
15
|
+
### Stable GitHub release
|
|
16
|
+
|
|
17
|
+
Install the verified wheel attached to the `v0.1.0` GitHub Release:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
python -m pip install https://github.com/LaimaWu/release-state-reconcile/releases/download/v0.1.0/release_state_reconcile-0.1.0-py3-none-any.whl
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Tagged Git source
|
|
24
|
+
|
|
25
|
+
Alternatively, install from the exact `v0.1.0` tag:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
python -m pip install "git+https://github.com/LaimaWu/release-state-reconcile.git@v0.1.0"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
If you use `pipx`, the same tagged source can be installed as an isolated CLI application:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pipx install "git+https://github.com/LaimaWu/release-state-reconcile.git@v0.1.0"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### PyPI status
|
|
38
|
+
|
|
39
|
+
PyPI distribution is planned but not yet available. Do not use `pip install release-state-reconcile` until the package is published on PyPI.
|
|
40
|
+
|
|
41
|
+
### Contributor and development install
|
|
42
|
+
|
|
43
|
+
From a source checkout, contributors can retain an editable installation:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
python -m pip install -e .
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## CLI
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
release-state-reconcile \
|
|
53
|
+
--candidate https://github.com/OWNER/REPOSITORY/issues/123 \
|
|
54
|
+
--release-branch release/1.2 \
|
|
55
|
+
--config repository.json \
|
|
56
|
+
--output report.md
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Use `--format json` for structured output. If `--output` is omitted, the report is written to standard output.
|
|
60
|
+
|
|
61
|
+
## Output shape
|
|
62
|
+
|
|
63
|
+
The Markdown report includes an evidence-linked state table, related pull requests, explicit same- and cross-repository relationships, contradictions, and unknown or missing links.
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
Fact State
|
|
67
|
+
Original issue/PR CLOSED
|
|
68
|
+
Mainline merge MERGED | OPEN | UNKNOWN
|
|
69
|
+
Required backport PRs MERGED | MISSING | TARGET_BRANCH_MISMATCH | UNKNOWN
|
|
70
|
+
Backport/check state PASS | FAIL | PENDING | UNKNOWN
|
|
71
|
+
Release-note evidence PRESENT | ABSENT | UNKNOWN
|
|
72
|
+
Final tag/release containment CONTAINED | NOT_CONTAINED | UNKNOWN
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Each observation carries a public source URL, object type, object ID, observed state, and confidence.
|
|
76
|
+
|
|
77
|
+
## Configuration
|
|
78
|
+
|
|
79
|
+
Repository conventions live in JSON rather than source code. A minimal configuration looks like this:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"mainline_branch": "main",
|
|
84
|
+
"backport": {"required": true, "search": true},
|
|
85
|
+
"completion": {"closed_is_complete": true, "labels": []},
|
|
86
|
+
"verification_labels": ["verified"],
|
|
87
|
+
"release_note": {"path_globs": ["changes/**"]},
|
|
88
|
+
"release": {"tag_regex": "^v1\\.2\\.[0-9]+$", "max_releases": 10}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The selected release branch is always supplied explicitly on the command line. Configuration may describe the default branch, whether a backport is expected, repository-specific verification labels, release-note paths, and the release-tag convention.
|
|
93
|
+
|
|
94
|
+
## Scope and non-goals
|
|
95
|
+
|
|
96
|
+
The tool observes and reconciles public GitHub state. It does not:
|
|
97
|
+
|
|
98
|
+
- decide whether a candidate belongs in a release;
|
|
99
|
+
- assess change risk or release-note quality;
|
|
100
|
+
- approve, merge, label, comment on, or otherwise mutate GitHub objects;
|
|
101
|
+
- orchestrate releases across multiple release lines;
|
|
102
|
+
- provide end-to-end release automation; or
|
|
103
|
+
- use an LLM or make AI-generated release decisions.
|
|
104
|
+
|
|
105
|
+
## Read-only and security model
|
|
106
|
+
|
|
107
|
+
The GitHub client exposes only HTTP `GET` operations. Authentication is optional: `GITHUB_TOKEN` or `GH_TOKEN` may be supplied to increase public REST API limits, but the tool has no GitHub write methods. Reports may be written to a local path selected by the caller.
|
|
108
|
+
|
|
109
|
+
The runtime uses the Python standard library and the public GitHub REST API. Do not place tokens in configuration files or commit them to source control. See [SECURITY.md](SECURITY.md) for vulnerability reporting guidance.
|
|
110
|
+
|
|
111
|
+
## Validation
|
|
112
|
+
|
|
113
|
+
The repository contains 21 deterministic unit tests covering closure semantics, relationship discovery, cross-repository preservation, target-branch mismatch handling, external-provider uncertainty, check aggregation, release-note path evidence, and the GET-only client surface. CI runs the full suite on Python 3.10, 3.11, 3.12, and 3.13.
|
|
114
|
+
|
|
115
|
+
Run locally with:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
python -m unittest discover -s tests -v
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Known limitations
|
|
122
|
+
|
|
123
|
+
- One repository and one selected release line are evaluated per run.
|
|
124
|
+
- Repository conventions may require configuration.
|
|
125
|
+
- Unsupported external change providers remain `UNKNOWN`.
|
|
126
|
+
- Private CI and off-GitHub approvals are outside scope.
|
|
127
|
+
- Candidate eligibility and release-note quality remain human judgment.
|
|
128
|
+
- Some public issues do not expose enough relationships to reconstruct a useful chain.
|
|
129
|
+
- A complete GitHub search establishes only that no qualifying result was returned for that query; it is not proof that an unreferenced change does not exist.
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "release-state-reconcile"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Read-only CLI for reconciling release candidates across fixes, backports, checks, release notes, and published releases."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = {file = "LICENSE"}
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "LaimaWu"}
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: Software Development :: Quality Assurance"
|
|
26
|
+
]
|
|
27
|
+
dependencies = []
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Repository = "https://github.com/LaimaWu/release-state-reconcile"
|
|
31
|
+
Issues = "https://github.com/LaimaWu/release-state-reconcile/issues"
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
release-state-reconcile = "release_state_reconcile.cli:main"
|
|
35
|
+
|
|
36
|
+
[tool.setuptools]
|
|
37
|
+
package-dir = {"" = "src"}
|
|
38
|
+
|
|
39
|
+
[tool.setuptools.packages.find]
|
|
40
|
+
where = ["src"]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .github import GitHubAPIError, GitHubClient
|
|
9
|
+
from .reconcile import reconcile_candidate
|
|
10
|
+
from .report import render_json, render_markdown
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def parser() -> argparse.ArgumentParser:
|
|
14
|
+
result = argparse.ArgumentParser(
|
|
15
|
+
description="Reconstruct release-candidate state from public GitHub data (GET-only)."
|
|
16
|
+
)
|
|
17
|
+
result.add_argument("--candidate", required=True, help="Public GitHub issue or PR URL")
|
|
18
|
+
result.add_argument("--release-branch", required=True, help="Target release branch")
|
|
19
|
+
result.add_argument("--config", required=True, type=Path, help="Repository configuration JSON")
|
|
20
|
+
result.add_argument("--format", choices=("markdown", "json"), default="markdown")
|
|
21
|
+
result.add_argument("--output", type=Path, help="Write report to this path instead of stdout")
|
|
22
|
+
return result
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def main(argv: list[str] | None = None) -> int:
|
|
26
|
+
args = parser().parse_args(argv)
|
|
27
|
+
try:
|
|
28
|
+
config = json.loads(args.config.read_text(encoding="utf-8"))
|
|
29
|
+
report = reconcile_candidate(
|
|
30
|
+
GitHubClient(), args.candidate, args.release_branch, config
|
|
31
|
+
)
|
|
32
|
+
output = render_json(report) if args.format == "json" else render_markdown(report)
|
|
33
|
+
if args.output:
|
|
34
|
+
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
args.output.write_text(output + "\n", encoding="utf-8")
|
|
36
|
+
else:
|
|
37
|
+
sys.stdout.write(output + "\n")
|
|
38
|
+
except (ValueError, OSError, json.JSONDecodeError, GitHubAPIError) as exc:
|
|
39
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
40
|
+
return 2
|
|
41
|
+
return 0
|
|
42
|
+
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import urllib.error
|
|
6
|
+
import urllib.parse
|
|
7
|
+
import urllib.request
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GitHubAPIError(RuntimeError):
|
|
12
|
+
def __init__(self, status: int, url: str, message: str):
|
|
13
|
+
super().__init__(f"GitHub API {status} for {url}: {message}")
|
|
14
|
+
self.status = status
|
|
15
|
+
self.url = url
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GitHubClient:
|
|
19
|
+
"""Small GET-only GitHub REST client.
|
|
20
|
+
|
|
21
|
+
There are deliberately no mutation methods in this class. Authentication is
|
|
22
|
+
optional and only expands the public REST rate limit.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
api_root = "https://api.github.com"
|
|
26
|
+
|
|
27
|
+
def __init__(self, token: str | None = None, timeout: int = 30):
|
|
28
|
+
self.token = token or os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
|
|
29
|
+
self.timeout = timeout
|
|
30
|
+
self.requests: list[str] = []
|
|
31
|
+
self._cache: dict[str, Any] = {}
|
|
32
|
+
|
|
33
|
+
def _get(self, path: str, params: dict[str, str | int] | None = None) -> Any:
|
|
34
|
+
url = path if path.startswith("https://") else f"{self.api_root}{path}"
|
|
35
|
+
if params:
|
|
36
|
+
url = f"{url}?{urllib.parse.urlencode(params)}"
|
|
37
|
+
if url in self._cache:
|
|
38
|
+
return self._cache[url]
|
|
39
|
+
headers = {
|
|
40
|
+
"Accept": "application/vnd.github+json",
|
|
41
|
+
"User-Agent": "release-state-reconcile/0.1",
|
|
42
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
43
|
+
}
|
|
44
|
+
if self.token:
|
|
45
|
+
headers["Authorization"] = f"Bearer {self.token}"
|
|
46
|
+
request = urllib.request.Request(url, method="GET", headers=headers)
|
|
47
|
+
self.requests.append(url)
|
|
48
|
+
try:
|
|
49
|
+
with urllib.request.urlopen(request, timeout=self.timeout) as response:
|
|
50
|
+
payload = json.loads(response.read().decode("utf-8"))
|
|
51
|
+
except urllib.error.HTTPError as exc:
|
|
52
|
+
body = exc.read().decode("utf-8", errors="replace")
|
|
53
|
+
try:
|
|
54
|
+
message = json.loads(body).get("message", body)
|
|
55
|
+
except json.JSONDecodeError:
|
|
56
|
+
message = body
|
|
57
|
+
raise GitHubAPIError(exc.code, url, message) from exc
|
|
58
|
+
self._cache[url] = payload
|
|
59
|
+
return payload
|
|
60
|
+
|
|
61
|
+
def issue(self, owner: str, repo: str, number: int) -> dict[str, Any]:
|
|
62
|
+
return self._get(f"/repos/{owner}/{repo}/issues/{number}")
|
|
63
|
+
|
|
64
|
+
def timeline(self, owner: str, repo: str, number: int) -> list[dict[str, Any]]:
|
|
65
|
+
return self._get(
|
|
66
|
+
f"/repos/{owner}/{repo}/issues/{number}/timeline", {"per_page": 100}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def comments(self, owner: str, repo: str, number: int) -> list[dict[str, Any]]:
|
|
70
|
+
return self._get(
|
|
71
|
+
f"/repos/{owner}/{repo}/issues/{number}/comments", {"per_page": 100}
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def pull(self, owner: str, repo: str, number: int) -> dict[str, Any]:
|
|
75
|
+
return self._get(f"/repos/{owner}/{repo}/pulls/{number}")
|
|
76
|
+
|
|
77
|
+
def check_runs(self, owner: str, repo: str, sha: str) -> dict[str, Any]:
|
|
78
|
+
return self._get(
|
|
79
|
+
f"/repos/{owner}/{repo}/commits/{sha}/check-runs", {"per_page": 100}
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def pull_files(self, owner: str, repo: str, number: int) -> list[dict[str, Any]]:
|
|
83
|
+
return self._get(
|
|
84
|
+
f"/repos/{owner}/{repo}/pulls/{number}/files", {"per_page": 100}
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def branch(self, owner: str, repo: str, branch: str) -> dict[str, Any]:
|
|
88
|
+
quoted = urllib.parse.quote(branch, safe="")
|
|
89
|
+
return self._get(f"/repos/{owner}/{repo}/branches/{quoted}")
|
|
90
|
+
|
|
91
|
+
def search_pulls(
|
|
92
|
+
self, owner: str, repo: str, base: str, reference: str
|
|
93
|
+
) -> dict[str, Any]:
|
|
94
|
+
query = f'repo:{owner}/{repo} is:pr base:"{base}" "{reference}"'
|
|
95
|
+
return self._get("/search/issues", {"q": query, "per_page": 100})
|
|
96
|
+
|
|
97
|
+
def releases(self, owner: str, repo: str) -> list[dict[str, Any]]:
|
|
98
|
+
return self._get(f"/repos/{owner}/{repo}/releases", {"per_page": 100})
|
|
99
|
+
|
|
100
|
+
def release_by_tag(self, owner: str, repo: str, tag: str) -> dict[str, Any]:
|
|
101
|
+
quoted = urllib.parse.quote(tag, safe="")
|
|
102
|
+
return self._get(f"/repos/{owner}/{repo}/releases/tags/{quoted}")
|
|
103
|
+
|
|
104
|
+
def compare(self, owner: str, repo: str, base: str, head: str) -> dict[str, Any]:
|
|
105
|
+
quoted_base = urllib.parse.quote(base, safe="")
|
|
106
|
+
quoted_head = urllib.parse.quote(head, safe="")
|
|
107
|
+
return self._get(f"/repos/{owner}/{repo}/compare/{quoted_base}...{quoted_head}")
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict, dataclass, field
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class Evidence:
|
|
9
|
+
source_url: str
|
|
10
|
+
object_type: str
|
|
11
|
+
object_id: str
|
|
12
|
+
observed_state: str
|
|
13
|
+
confidence: str = "factual"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class Finding:
|
|
18
|
+
name: str
|
|
19
|
+
status: str
|
|
20
|
+
summary: str
|
|
21
|
+
evidence: list[Evidence] = field(default_factory=list)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class PullRecord:
|
|
26
|
+
number: int
|
|
27
|
+
url: str
|
|
28
|
+
title: str
|
|
29
|
+
state: str
|
|
30
|
+
merged: bool
|
|
31
|
+
merged_at: str | None
|
|
32
|
+
merge_sha: str | None
|
|
33
|
+
head_sha: str | None
|
|
34
|
+
base: str
|
|
35
|
+
labels: list[str]
|
|
36
|
+
relation: str
|
|
37
|
+
relation_evidence: list[Evidence] = field(default_factory=list)
|
|
38
|
+
changed_files: int = 0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class RelationshipRecord:
|
|
43
|
+
url: str
|
|
44
|
+
provider: str
|
|
45
|
+
repository: str | None
|
|
46
|
+
object_type: str
|
|
47
|
+
object_id: str
|
|
48
|
+
scope: str
|
|
49
|
+
relation: str
|
|
50
|
+
evidence: list[Evidence] = field(default_factory=list)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class CandidateReport:
|
|
55
|
+
candidate_url: str
|
|
56
|
+
repo: str
|
|
57
|
+
number: int
|
|
58
|
+
observed_at: str
|
|
59
|
+
target_release_branch: str
|
|
60
|
+
mainline_branch: str
|
|
61
|
+
findings: list[Finding]
|
|
62
|
+
related_pulls: list[PullRecord]
|
|
63
|
+
relationships: list[RelationshipRecord]
|
|
64
|
+
contradictions: list[Finding]
|
|
65
|
+
unknowns: list[Finding]
|
|
66
|
+
materially_useful: bool
|
|
67
|
+
api_requests: list[str] = field(default_factory=list)
|
|
68
|
+
|
|
69
|
+
def to_dict(self) -> dict[str, Any]:
|
|
70
|
+
return asdict(self)
|