gh-ci-truth 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.
- gh_ci_truth-0.1.0/LICENSE +21 -0
- gh_ci_truth-0.1.0/PKG-INFO +349 -0
- gh_ci_truth-0.1.0/README.md +311 -0
- gh_ci_truth-0.1.0/pyproject.toml +74 -0
- gh_ci_truth-0.1.0/setup.cfg +4 -0
- gh_ci_truth-0.1.0/src/gh_ci_truth/__init__.py +95 -0
- gh_ci_truth-0.1.0/src/gh_ci_truth/__main__.py +8 -0
- gh_ci_truth-0.1.0/src/gh_ci_truth/_version.py +3 -0
- gh_ci_truth-0.1.0/src/gh_ci_truth/action.py +191 -0
- gh_ci_truth-0.1.0/src/gh_ci_truth/analyze.py +297 -0
- gh_ci_truth-0.1.0/src/gh_ci_truth/classify.py +373 -0
- gh_ci_truth-0.1.0/src/gh_ci_truth/cli.py +255 -0
- gh_ci_truth-0.1.0/src/gh_ci_truth/client.py +276 -0
- gh_ci_truth-0.1.0/src/gh_ci_truth/model.py +361 -0
- gh_ci_truth-0.1.0/src/gh_ci_truth/py.typed +0 -0
- gh_ci_truth-0.1.0/src/gh_ci_truth/report.py +197 -0
- gh_ci_truth-0.1.0/src/gh_ci_truth.egg-info/PKG-INFO +349 -0
- gh_ci_truth-0.1.0/src/gh_ci_truth.egg-info/SOURCES.txt +24 -0
- gh_ci_truth-0.1.0/src/gh_ci_truth.egg-info/dependency_links.txt +1 -0
- gh_ci_truth-0.1.0/src/gh_ci_truth.egg-info/entry_points.txt +2 -0
- gh_ci_truth-0.1.0/src/gh_ci_truth.egg-info/requires.txt +8 -0
- gh_ci_truth-0.1.0/src/gh_ci_truth.egg-info/top_level.txt +1 -0
- gh_ci_truth-0.1.0/tests/test_action.py +585 -0
- gh_ci_truth-0.1.0/tests/test_classify.py +500 -0
- gh_ci_truth-0.1.0/tests/test_cli.py +443 -0
- gh_ci_truth-0.1.0/tests/test_recorded.py +282 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Manu Nicholas Jacob
|
|
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,349 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gh-ci-truth
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Tell whether a pull request's CI actually ran, instead of whether its checks panel looks clean.
|
|
5
|
+
Author-email: Manu Nicholas Jacob <manunicholasjacob@gmail.com>
|
|
6
|
+
Maintainer-email: Manu Nicholas Jacob <manunicholasjacob@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/manunicholasjacob/gh-ci-truth
|
|
9
|
+
Project-URL: Source, https://github.com/manunicholasjacob/gh-ci-truth
|
|
10
|
+
Project-URL: Issues, https://github.com/manunicholasjacob/gh-ci-truth/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/manunicholasjacob/gh-ci-truth/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: github,github-actions,continuous-integration,pull-request,ci,gh-extension,open-source-maintenance
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
24
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
25
|
+
Classifier: Topic :: Software Development :: Testing
|
|
26
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
27
|
+
Classifier: Typing :: Typed
|
|
28
|
+
Requires-Python: >=3.9
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Provides-Extra: test
|
|
32
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
35
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
36
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# gh-ci-truth
|
|
40
|
+
|
|
41
|
+
Tell whether a pull request's CI actually ran, instead of whether its checks panel looks clean.
|
|
42
|
+
|
|
43
|
+
A `gh` CLI extension, an importable Python library, and a GitHub Action, all driven by one classifier.
|
|
44
|
+
|
|
45
|
+
## The problem
|
|
46
|
+
|
|
47
|
+
`gh pr checks` and `gh pr view --json statusCheckRollup` report check runs attached to the head commit. That is all they report. GitHub's own documentation is explicit that the rollup is commit scoped: `StatusCheckRollup.contexts` is "A list of status contexts and check runs for this commit." ([GraphQL reference: commits](https://docs.github.com/en/graphql/reference/commits)).
|
|
48
|
+
|
|
49
|
+
For an outside contributor, GitHub holds workflow runs for maintainer approval. A held run has executed no job, so it attaches no check run. If nobody approves it within 30 days, GitHub closes the window: "awaiting approval for more than 30 days expire" and "and are marked as failed" ([Approving workflow runs from forks](https://docs.github.com/en/actions/how-tos/manage-workflow-runs/approve-runs-from-forks)). The run reaches a terminal state having still executed no job, so it still attaches no check run.
|
|
50
|
+
|
|
51
|
+
The result is that a pull request whose CI has never produced a single test result renders identically to one with no CI required. A blank checks panel reads as a clean one, to maintainers and to contributors alike. The distinction between "no checks exist" and "checks exist and are green" is the distinction the existing tooling destroys, and it is the one this tool restores.
|
|
52
|
+
|
|
53
|
+
## The verified evidence
|
|
54
|
+
|
|
55
|
+
Two pull requests in the public repository [NVIDIA/garak](https://github.com/NVIDIA/garak), numbers 2031 and 2032. They are the author's own pull requests, used here as a worked example because the author can speak to their full history. Nothing below characterizes the repository's maintainers; approval queues go unattended for ordinary reasons.
|
|
56
|
+
|
|
57
|
+
Recorded from the live REST API on 2026-11-01:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
$ gh pr view 2031 --repo NVIDIA/garak --json headRefOid -q .headRefOid
|
|
61
|
+
4b3e3374cf8355388460e99f055314209d73089e
|
|
62
|
+
|
|
63
|
+
$ gh api "repos/NVIDIA/garak/actions/runs?head_sha=4b3e3374cf8355388460e99f055314209d73089e" \
|
|
64
|
+
-q '.workflow_runs[]|"\(.name)|\(.created_at)|\(.status)|\(.conclusion)"'
|
|
65
|
+
Garak pytest - MacOS|2026-08-28T01:13:15Z|completed|failure
|
|
66
|
+
Garak pip - install from repo|2026-08-28T01:13:15Z|completed|failure
|
|
67
|
+
Garak pytest - Linux|2026-08-28T01:13:15Z|completed|failure
|
|
68
|
+
Garak pytest - Windows|2026-08-28T01:13:15Z|completed|failure
|
|
69
|
+
Documentation build|2026-08-28T01:13:15Z|completed|failure
|
|
70
|
+
|
|
71
|
+
$ gh api repos/NVIDIA/garak/actions/runs/33132180993/jobs -q .total_count
|
|
72
|
+
0
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Every run was created `2026-08-28T01:13:15Z` and reached `2026-09-27T01:17:20Z`. That is 30 days and about four minutes. Every one reports `status: completed`, `conclusion: failure`, and zero jobs.
|
|
76
|
+
|
|
77
|
+
Now the part that matters. What does the checks panel show for that same commit?
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
$ gh pr view 2031 --repo NVIDIA/garak --json statusCheckRollup
|
|
81
|
+
{"statusCheckRollup":[{"__typename":"CheckRun","conclusion":"SUCCESS","name":"DCO",...}]}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
One check run. It is green. It is a signature check, not a test. Five test workflows produced nothing at all and are absent from the panel entirely. The pull request reads as mergeable with a passing check, and has never been tested.
|
|
85
|
+
|
|
86
|
+
`gh-ci-truth` says so instead:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
$ gh ci-truth 2031 --repo NVIDIA/garak
|
|
90
|
+
NVIDIA/garak#2031 NEVER_RAN_EXPIRED
|
|
91
|
+
title fix(analyze): count prompt characters, not dict length, in count_tokens
|
|
92
|
+
author manunicholasjacob
|
|
93
|
+
verdict CI has never run: the approval request expired after 30 days
|
|
94
|
+
because 5 workflow runs on the head commit sat unapproved for 30.0 days and were
|
|
95
|
+
expired by GitHub with zero jobs: Documentation build, Garak pip - install
|
|
96
|
+
from repo, Garak pytest - Linux, Garak pytest - MacOS, Garak pytest - Windows
|
|
97
|
+
confidence high
|
|
98
|
+
head 4b3e3374cf8355388460e99f055314209d73089e
|
|
99
|
+
rollup says 1 check run(s) attached: 1 success
|
|
100
|
+
jobs run 0
|
|
101
|
+
WARNING the checks panel does not show a failure for this commit, so this pull
|
|
102
|
+
request reads as clean while having no test result
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Run across the repository's open pull requests on 2026-11-01, the first eight gave four distinct categories, five of the eight never actually tested while all five looked clean:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
$ gh ci-truth --repo NVIDIA/garak --all-open --limit 8
|
|
109
|
+
NVIDIA/garak#2385 NEVER_RAN_AWAITING_APPROVAL
|
|
110
|
+
NVIDIA/garak#2243 TESTED_PASSING
|
|
111
|
+
NVIDIA/garak#2383 NEVER_RAN_AWAITING_APPROVAL
|
|
112
|
+
NVIDIA/garak#2378 TESTED_PASSING
|
|
113
|
+
NVIDIA/garak#2257 TESTED_PASSING
|
|
114
|
+
NVIDIA/garak#2032 NEVER_RAN_EXPIRED
|
|
115
|
+
NVIDIA/garak#2031 NEVER_RAN_EXPIRED
|
|
116
|
+
NVIDIA/garak#2030 NEVER_RAN_AWAITING_APPROVAL
|
|
117
|
+
------------------------------------------------------------------------
|
|
118
|
+
8 pull request(s) examined, 5 never actually tested, 5 of those look clean, 0 undetermined
|
|
119
|
+
look clean but were never tested: #2385, #2383, #2032, #2031, #2030
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### The second trap: a fix can make it worse
|
|
123
|
+
|
|
124
|
+
Pushing a new commit to an unapproved pull request re-queues its CI. A merge that resolves a conflict therefore moves the pull request from "has stale results on an older commit" to "has no results at all", and the checks panel gets emptier, not fuller. That is the `STALE_RESULTS_FOR_OLDER_COMMIT` category.
|
|
125
|
+
|
|
126
|
+
## Categories
|
|
127
|
+
|
|
128
|
+
| Category | Meaning | Does the panel mislead? |
|
|
129
|
+
| --- | --- | --- |
|
|
130
|
+
| `TESTED_PASSING` | Runs on the head commit executed jobs and none failed. | No |
|
|
131
|
+
| `TESTED_FAILING` | Runs on the head commit executed jobs and at least one failed. Also covers a workflow that failed to start, which does surface as a red check. | No |
|
|
132
|
+
| `NEVER_RAN_AWAITING_APPROVAL` | Runs exist for the head commit but are parked for maintainer approval with zero jobs. Actionable today. | Yes |
|
|
133
|
+
| `NEVER_RAN_EXPIRED` | Runs sat unapproved for about 30 days and were expired with zero jobs. Needs a new push to re-queue. | Yes |
|
|
134
|
+
| `NO_CI_CONFIGURED` | No run for the head commit, and the repository has no workflows, no workflow that fires on pull requests, or skipped every one. | No |
|
|
135
|
+
| `STALE_RESULTS_FOR_OLDER_COMMIT` | No run for the head commit, but an earlier commit of the pull request did execute jobs. | Yes |
|
|
136
|
+
| `UNKNOWN` | The evidence does not settle it. Reported as undetermined rather than guessed. | Unclear |
|
|
137
|
+
|
|
138
|
+
Each verdict also carries `confidence` (`high`, `medium`, `low`) and a `misleading` flag, which is true only when the pull request is untested *and* nothing attached to the head commit is failing. A pull request with an expired run and a separate failing lint check is untested but not misleading: the panel is already red, so nobody is deceived. Gate labels and comments on `misleading`, not on `category`.
|
|
139
|
+
|
|
140
|
+
## The detection recipe
|
|
141
|
+
|
|
142
|
+
The signature of an expired approval, as opposed to a real test failure:
|
|
143
|
+
|
|
144
|
+
1. `status == "completed"`
|
|
145
|
+
2. `conclusion == "failure"` (also accepted: `cancelled`, `stale`, `skipped`)
|
|
146
|
+
3. the jobs endpoint reports `total_count == 0`
|
|
147
|
+
4. `created_at` to `updated_at` is about 30 days
|
|
148
|
+
|
|
149
|
+
All four must hold. A real failure has jobs. A workflow that could not start has a gap of seconds. The job count is the load bearing field and the one the rollup never shows you.
|
|
150
|
+
|
|
151
|
+
A run still awaiting approval has a different shape, and it is not the shape the REST documentation implies. Observed on 2026-11-01:
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
$ gh api "repos/NVIDIA/garak/actions/runs?status=action_required&per_page=1" \
|
|
155
|
+
-q '.workflow_runs[]|"\(.status)|\(.conclusion)|\(.created_at)|\(.updated_at)"'
|
|
156
|
+
completed|action_required|2026-11-01T10:34:26Z|2026-11-01T10:34:26Z
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`status` is `completed`; the pending marker is in `conclusion`. The documented `status` filter value `action_required` in fact matches the conclusion. Reading only `status` would classify a parked run as an ordinary completed one. This tool checks both fields.
|
|
160
|
+
|
|
161
|
+
Note also that `head_sha` matches exactly. A seven character prefix of a real head SHA returned `total_count: 0` when tested on 2026-11-01. Always pass the full 40 characters.
|
|
162
|
+
|
|
163
|
+
## Install
|
|
164
|
+
|
|
165
|
+
### As a `gh` extension
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
gh extension install manunicholasjacob/gh-ci-truth
|
|
169
|
+
gh ci-truth --help
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Needs Python 3.9 or newer on `PATH`. No third party Python packages. The extension is a shim over the bundled library, so there is one classifier, not two.
|
|
173
|
+
|
|
174
|
+
### As a library
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
pip install gh-ci-truth
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### As a GitHub Action
|
|
181
|
+
|
|
182
|
+
```yaml
|
|
183
|
+
# .github/workflows/ci-truth.yml
|
|
184
|
+
name: ci-truth
|
|
185
|
+
|
|
186
|
+
on:
|
|
187
|
+
pull_request_target:
|
|
188
|
+
types: [opened, synchronize, reopened]
|
|
189
|
+
schedule:
|
|
190
|
+
- cron: "17 6 * * *"
|
|
191
|
+
|
|
192
|
+
permissions:
|
|
193
|
+
contents: read
|
|
194
|
+
pull-requests: write
|
|
195
|
+
actions: read
|
|
196
|
+
|
|
197
|
+
jobs:
|
|
198
|
+
ci-truth:
|
|
199
|
+
runs-on: ubuntu-latest
|
|
200
|
+
steps:
|
|
201
|
+
- id: truth
|
|
202
|
+
uses: manunicholasjacob/gh-ci-truth@v0.1.0
|
|
203
|
+
|
|
204
|
+
- name: Label a pull request that only looks tested
|
|
205
|
+
if: steps.truth.outputs.misleading == 'true'
|
|
206
|
+
env:
|
|
207
|
+
GH_TOKEN: ${{ github.token }}
|
|
208
|
+
run: |
|
|
209
|
+
gh pr edit "${{ github.event.pull_request.number }}" \
|
|
210
|
+
--repo "${{ github.repository }}" \
|
|
211
|
+
--add-label "${{ steps.truth.outputs.label }}"
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The action makes no write calls of its own. It classifies and emits outputs; labelling and commenting are left to your own step, so the write permission stays visible in your workflow where you can audit it. `steps.truth.outputs.comment-body` is a ready made Markdown report if you would rather comment than label.
|
|
215
|
+
|
|
216
|
+
On `pull_request_target` the workflow runs with the base repository's permissions, which is what lets it label a fork's pull request. It checks out nothing and runs no code from the fork.
|
|
217
|
+
|
|
218
|
+
## Usage
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
gh ci-truth 2031 --repo NVIDIA/garak
|
|
222
|
+
gh ci-truth 2031 2032 --repo NVIDIA/garak --verbose
|
|
223
|
+
gh ci-truth --repo NVIDIA/garak --all-open --limit 50
|
|
224
|
+
gh ci-truth --repo owner/name --all-open --json
|
|
225
|
+
gh ci-truth --repo owner/name --all-open --strict
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
| Flag | Effect |
|
|
229
|
+
| --- | --- |
|
|
230
|
+
| `-R`, `--repo OWNER/REPO` | Target repository. Falls back to `GH_REPO`, then `GITHUB_REPOSITORY`, then the local git remote. |
|
|
231
|
+
| `--all-open` | Examine every open pull request. |
|
|
232
|
+
| `--limit N` | Cap for `--all-open`. Default 50. |
|
|
233
|
+
| `--json` | Machine readable output under the `gh-ci-truth/v1` schema. |
|
|
234
|
+
| `-v`, `--verbose` | List every workflow run and check run considered. |
|
|
235
|
+
| `--strict` | Exit non-zero when a pull request was never actually tested. |
|
|
236
|
+
| `--fail-on-tested-failing` | With `--strict`, also fail for a genuine test failure. Off by default, because `gh pr checks` already reports those correctly. |
|
|
237
|
+
| `--shallow` | Skip the older-commit and repository-workflow lookups. Three fewer requests per pull request, at the cost of collapsing `NO_CI_CONFIGURED` and `STALE_RESULTS_FOR_OLDER_COMMIT` into `UNKNOWN`. |
|
|
238
|
+
| `--expiry-gap-days D` | Gap threshold for the expiry signature. Default 29.0. |
|
|
239
|
+
| `--token` | Use `GITHUB_TOKEN` over REST instead of the `gh` CLI. |
|
|
240
|
+
| `--cassette PATH` | Replay a recorded JSON cassette instead of calling GitHub. |
|
|
241
|
+
|
|
242
|
+
Exit status: `0` nothing to report, `1` something was never actually tested, `2` something is `UNKNOWN`, `3` operational failure. Without `--strict` a successful run is always `0`.
|
|
243
|
+
|
|
244
|
+
## Library API
|
|
245
|
+
|
|
246
|
+
The classifier is a pure function from recorded evidence to a verdict. It has no clock, no network and no subprocess, which is why it is safe to depend on.
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
from gh_ci_truth import Category, Evidence, WorkflowRun, classify
|
|
250
|
+
|
|
251
|
+
evidence = Evidence(
|
|
252
|
+
head_sha="4b3e3374cf8355388460e99f055314209d73089e",
|
|
253
|
+
runs_on_head=[
|
|
254
|
+
WorkflowRun(
|
|
255
|
+
id=33132180993,
|
|
256
|
+
name="Garak pytest - MacOS",
|
|
257
|
+
status="completed",
|
|
258
|
+
conclusion="failure",
|
|
259
|
+
created_at="2026-08-28T01:13:15Z",
|
|
260
|
+
updated_at="2026-09-27T01:17:20Z",
|
|
261
|
+
job_count=0,
|
|
262
|
+
)
|
|
263
|
+
],
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
verdict = classify(evidence, repo="NVIDIA/garak", number=2031)
|
|
267
|
+
verdict.category is Category.NEVER_RAN_EXPIRED # True
|
|
268
|
+
verdict.never_actually_tested # True
|
|
269
|
+
verdict.misleading # True
|
|
270
|
+
verdict.reason # a sentence naming the workflows
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
To let the library fetch the evidence:
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
from gh_ci_truth import GhCliClient, analyze_pull_request, render_json
|
|
277
|
+
|
|
278
|
+
verdict = analyze_pull_request(GhCliClient(), "NVIDIA/garak", 2031)
|
|
279
|
+
print(render_json([verdict]))
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Public surface:
|
|
283
|
+
|
|
284
|
+
- Model: `Category`, `UNTESTED_CATEGORIES`, `WorkflowRun`, `CheckRun`, `Evidence`, `Verdict`
|
|
285
|
+
- Classifier: `classify`, `is_expiry_signature`, `DEFAULT_EXPIRY_GAP_DAYS`, `APPROVAL_WINDOW_DAYS`
|
|
286
|
+
- Access: `GitHubClient` (a `Protocol`), `GhCliClient`, `TokenClient`, `RecordedClient`, `default_client`, `GitHubError`
|
|
287
|
+
- Orchestration: `analyze_pull_request`, `analyze_pull_requests`, `collect_evidence`, `list_open_pull_requests`
|
|
288
|
+
- Rendering: `render_text`, `render_json`, `render_markdown`, `CATEGORY_HEADLINE`, `exit_code`
|
|
289
|
+
|
|
290
|
+
`GitHubClient` is a two method `Protocol` (`get`, `paginate`). Implement it to drive the classifier from a cache, a GraphQL layer, or a different forge.
|
|
291
|
+
|
|
292
|
+
## Limitations
|
|
293
|
+
|
|
294
|
+
Stated plainly, because a tool about misleading signals should not produce any.
|
|
295
|
+
|
|
296
|
+
- **The 30 day gap is a heuristic, not a flag.** GitHub does not expose "this run expired". The documentation says an unapproved run is "marked as failed" in prose, but the REST reference never documents which `conclusion` an expired run receives, and no `expired` value exists in any documented enumeration. The four part signature is an inference from observed data. A workflow that genuinely took 30 days to fail would be misread, and `--expiry-gap-days` exists for that case.
|
|
297
|
+
- **`UNKNOWN` is a real answer.** A head commit with no runs in a repository that does have pull request workflows is reported as `UNKNOWN`, not guessed at. A path filter, a branch filter, a `paths-ignore`, or a deleted run all produce that state and the API does not distinguish them.
|
|
298
|
+
- **`NO_CI_CONFIGURED` partly rests on a proxy.** "Has any workflow in this repository ever run for a `pull_request` event" stands in for "a `pull_request` trigger exists", because reading every workflow file costs a request each and still misses reusable workflows. Verdicts resting on the proxy are marked `medium` confidence.
|
|
299
|
+
- **Cost.** One request for the pull request, one for its runs, one per run for the job count, one for the check runs. A pull request with five workflows is about eight requests. `--all-open` across a busy repository will feel it, and `--shallow` only helps when the head commit has no runs.
|
|
300
|
+
- **Job counts, not job results.** The tool asks whether jobs ran, then trusts the run level `conclusion` for pass or fail. It does not inspect individual job conclusions, annotations, or logs.
|
|
301
|
+
- **Required checks are not consulted.** It does not read branch protection, so it cannot say whether the workflows that never ran were ones the repository requires.
|
|
302
|
+
- **Only `action_required` expiry is modelled.** The 35 day total run limit and the 24 hour self-hosted queue limit are separate mechanisms and are not detected.
|
|
303
|
+
- **GitHub Enterprise Server differs.** The GHES 3.19 documentation says runs awaiting approval over 30 days "are automatically deleted" ([GHES 3.19](https://docs.github.com/en/enterprise-server@3.19/actions/how-tos/manage-workflow-runs/approve-runs-from-forks)), which contradicts the GitHub.com page's "marked as failed". A deleted run has no conclusion to read, so on GHES the expected verdict is `UNKNOWN` or `NO_CI_CONFIGURED`, not `NEVER_RAN_EXPIRED`. This tool has been tested against GitHub.com only.
|
|
304
|
+
- **A commit with an unusual number of workflows is refused, not guessed.** The tool inspects at most 60 runs per commit. Beyond that it raises rather than returning a partial view, because a partial view could omit the one expired run that matters.
|
|
305
|
+
- **Any evidence it could not gather becomes `UNKNOWN`, not a pass.** If the jobs endpoint fails for one run, if the workflow runs endpoint returns something unexpected, or if the commit list cannot be read, the verdict degrades and says why. The tool is built to prefer "I do not know" over a clean answer it has not earned, and five of its tests exist only to hold that line.
|
|
306
|
+
- **Not adopted by anyone.** This is a new tool with no users, no dependents and no endorsement. Version 0.1.0.
|
|
307
|
+
|
|
308
|
+
## What is documented and what is observed
|
|
309
|
+
|
|
310
|
+
Keeping these apart is the point of the tool, so the tool's own claims are kept apart too.
|
|
311
|
+
|
|
312
|
+
| Claim | Status | Source |
|
|
313
|
+
| --- | --- | --- |
|
|
314
|
+
| Unapproved fork runs expire after 30 days and are marked as failed | **Documented** | ["awaiting approval for more than 30 days expire"](https://docs.github.com/en/actions/how-tos/manage-workflow-runs/approve-runs-from-forks) |
|
|
315
|
+
| First time and external contributors need approval; the options are "Require approval for first-time contributors who are new to GitHub", "Require approval for first-time contributors", "Require approval for all external contributors" | **Documented** | [Managing GitHub Actions settings for a repository](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository) |
|
|
316
|
+
| First time contributors require approval by default | **Documented** | [Disabling or limiting GitHub Actions for your organization](https://docs.github.com/en/organizations/managing-organization-settings/disabling-or-limiting-github-actions-for-your-organization) |
|
|
317
|
+
| A contributor with any prior merged commit does not need approval under the first two settings | **Documented** | same page |
|
|
318
|
+
| `action_required` is a valid `status` filter value | **Documented** | [REST: workflow runs](https://docs.github.com/en/rest/actions/workflow-runs) |
|
|
319
|
+
| The rollup is scoped to one commit | **Documented** | [GraphQL reference: commits](https://docs.github.com/en/graphql/reference/commits) |
|
|
320
|
+
| An expired run receives `status: completed`, `conclusion: failure` | **Observed, not documented.** The REST reference gives no description or enumeration for run level `conclusion`, and no `expired` value exists anywhere. | garak runs 33132180993 and four others, read 2026-11-01 |
|
|
321
|
+
| An expired run reports zero jobs, so it attaches no check run | **Observed, not documented** | jobs endpoint `total_count: 0`, read 2026-11-01 |
|
|
322
|
+
| A pending run reports `status: completed` with `conclusion: action_required` | **Observed, and contrary to what the documented `status` enumeration implies** | `?status=action_required`, read 2026-11-01 |
|
|
323
|
+
| `head_sha` requires the full 40 characters | **Observed. Not documented either way**; the parameter says only "Only returns workflow runs that are associated with the specified head_sha." | 7 character prefix returned `total_count: 0`, 2026-11-01 |
|
|
324
|
+
| GHES deletes expired runs rather than failing them | **Documented, and contradicts the GitHub.com page** | [GHES 3.19](https://docs.github.com/en/enterprise-server@3.19/actions/how-tos/manage-workflow-runs/approve-runs-from-forks) |
|
|
325
|
+
|
|
326
|
+
## Tests
|
|
327
|
+
|
|
328
|
+
167 tests, all offline. Every test runs against a cassette recorded from the live API or a clearly labelled synthetic one, and a fixture fails the suite if any test opens a socket.
|
|
329
|
+
|
|
330
|
+
```
|
|
331
|
+
pip install -e ".[test]"
|
|
332
|
+
python -m pytest
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
The four `tests/fixtures/garak_*.json` cassettes hold payloads recorded from NVIDIA/garak on 2026-11-01, trimmed to the fields the classifier reads and transcoded to ASCII, with no values altered. The five `synth_*.json` cassettes are invented and say so in their `provenance` field; a test asserts that they do.
|
|
336
|
+
|
|
337
|
+
## Contributing
|
|
338
|
+
|
|
339
|
+
Issues and pull requests welcome. A bug report is most useful with the `gh api` output that produced the wrong verdict, so it can become a cassette.
|
|
340
|
+
|
|
341
|
+
## Author
|
|
342
|
+
|
|
343
|
+
Manu Nicholas Jacob, Independent Researcher.
|
|
344
|
+
ORCID [0009-0007-6589-6572](https://orcid.org/0009-0007-6589-6572).
|
|
345
|
+
GitHub [@manunicholasjacob](https://github.com/manunicholasjacob).
|
|
346
|
+
|
|
347
|
+
## License
|
|
348
|
+
|
|
349
|
+
MIT. See [LICENSE](LICENSE).
|