github-security-report 0.14.1__tar.gz → 0.15.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.
- {github_security_report-0.14.1 → github_security_report-0.15.0}/PKG-INFO +88 -39
- {github_security_report-0.14.1 → github_security_report-0.15.0}/README.md +87 -38
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/_version.py +2 -2
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/categories.py +5 -3
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/client/copilot.py +5 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/client/parsers.py +2 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/client/queries.py +28 -3
- github_security_report-0.15.0/src/github_security_report/client/reviews.py +76 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/models.py +22 -71
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/ordering.py +2 -2
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/pulls/__init__.py +4 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/pulls/columns.py +10 -3
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/pulls/counting.py +24 -4
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/pulls/presentation.py +21 -1
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/pulls/table.py +4 -0
- github_security_report-0.15.0/src/github_security_report/ranking.py +86 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/report/__init__.py +1 -1
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/report/aggregate.py +1 -1
- {github_security_report-0.14.1 → github_security_report-0.15.0}/.gitignore +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/LICENSE +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/LICENSES/Apache-2.0.txt +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/pyproject.toml +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/scripts/README.md +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/__init__.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/authors.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/classify.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/cli/__init__.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/cli/__main__.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/cli/app.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/cli/boundary.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/cli/modes.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/cli/options.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/cli/outputs.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/cli/publish.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/cli/serialise.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/client/__init__.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/client/alerts.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/client/batch_errors.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/client/endpoints.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/client/errors.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/client/org_reads.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/client/reads.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/client/transport.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/client/writes.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/collect/__init__.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/collect/context.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/collect/extras.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/collect/org.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/collect/protocols.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/collect/repo.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/config/__init__.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/config/loader.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/config/models.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/config/order.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/config/schema.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/gating.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/gitctx.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/issues.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/layout.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/posture/__init__.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/posture/enablement.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/posture/facts.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/posture/releases.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/py.typed +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/remediate.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/render/__init__.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/render/html.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/render/markdown.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/render/slack.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/render/slack_limits.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/render/terminal.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/report/display.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/report/signals.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/report/tables.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/rulesets.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/runner.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/scope.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/secret_patterns.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/severity.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/summary.py +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/templates/index.html.j2 +0 -0
- {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/templates/report.html.j2 +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: github-security-report
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.15.0
|
|
4
4
|
Summary: Security and quality reporting across GitHub organisations
|
|
5
5
|
Project-URL: Homepage, https://github.com/lfreleng-actions/github-security-report-action
|
|
6
6
|
Project-URL: Repository, https://github.com/lfreleng-actions/github-security-report-action
|
|
@@ -736,17 +736,17 @@ organisation](#inside-or-outside-the-organisation):
|
|
|
736
736
|
|
|
737
737
|
```text
|
|
738
738
|
Pull Requests
|
|
739
|
-
|
|
740
|
-
┃ Repository ┃ Human ┃ Ext ┃ Auto ┃ Conflict ┃ Fail ┃ Copilot ┃ Draft ┃ Total ┃
|
|
741
|
-
|
|
742
|
-
│ lftools-uv │ 8 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 8 │
|
|
743
|
-
│ dependamerge │ 3 │ 0 │ 0 │ 0 │ 0 │ 2 │ 0 │ 3 │
|
|
744
|
-
│ gha-workflow-linter │ 2 │ 0 │ 0 │ 0 │ 1 │ 0 │ 1 │ 2 │
|
|
745
|
-
│ harden-runner-block- │ 1 │ 0 │ 0 │ 1 │ 0 │ 0 │ 1 │ 1 │
|
|
746
|
-
│ action │ │ │ │ │ │ │ │ │
|
|
747
|
-
|
|
748
|
-
│ Total │ 14 │ 0 │ 0 │ 1 │ 1 │ 2 │ 2 │ 14 │
|
|
749
|
-
|
|
739
|
+
┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━┳━━━━━━┳━━━━━━━━━━┳━━━━━━┳━━━━━━━━┳━━━━━━━━━┳━━━━━━━┳━━━━━━━┓
|
|
740
|
+
┃ Repository ┃ Human ┃ Ext ┃ Auto ┃ Conflict ┃ Fail ┃ Review ┃ Copilot ┃ Draft ┃ Total ┃
|
|
741
|
+
┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━╇━━━━━━╇━━━━━━━━━━╇━━━━━━╇━━━━━━━━╇━━━━━━━━━╇━━━━━━━╇━━━━━━━┩
|
|
742
|
+
│ lftools-uv │ 8 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 8 │
|
|
743
|
+
│ dependamerge │ 3 │ 0 │ 0 │ 0 │ 0 │ 1 │ 2 │ 0 │ 3 │
|
|
744
|
+
│ gha-workflow-linter │ 2 │ 0 │ 0 │ 0 │ 1 │ 0 │ 0 │ 1 │ 2 │
|
|
745
|
+
│ harden-runner-block- │ 1 │ 0 │ 0 │ 1 │ 0 │ 0 │ 0 │ 1 │ 1 │
|
|
746
|
+
│ action │ │ │ │ │ │ │ │ │ │
|
|
747
|
+
├──────────────────────┼───────┼─────┼──────┼──────────┼──────┼────────┼─────────┼───────┼───────┤
|
|
748
|
+
│ Total │ 14 │ 0 │ 0 │ 1 │ 1 │ 1 │ 2 │ 2 │ 14 │
|
|
749
|
+
└──────────────────────┴───────┴─────┴──────┴──────────┴──────┴────────┴─────────┴───────┴───────┘
|
|
750
750
|
… and 9 more
|
|
751
751
|
❌ 13 With open pull requests
|
|
752
752
|
✅ 104 No open pull requests
|
|
@@ -760,45 +760,89 @@ The columns form **two independent groupings**:
|
|
|
760
760
|
| `Ext` | Human pull requests raised from outside the organisation — a **subset of Human**, which is why it sits beside it. |
|
|
761
761
|
| `Conflict` | Blocked on a merge conflict. |
|
|
762
762
|
| `Fail` | Latest checks did not pass. The rollup includes optional checks, so this is not by itself proof that the merge is blocked. |
|
|
763
|
+
| `Review` | A reviewer has **requested changes** — a person waiting on the author. |
|
|
763
764
|
| `Copilot` | Carries at least one **unresolved review thread** opened by GitHub's automated code reviewer. |
|
|
764
765
|
| `Draft` | Marked as a draft. |
|
|
765
766
|
|
|
766
|
-
`Conflict`, `Fail`, `Copilot` and `Draft` **overlap** each other and
|
|
767
|
-
split, so they do not sum to `Total` and are not meant to: one pull
|
|
768
|
-
is conflicting, failing *and* a draft is counted once in each of
|
|
769
|
-
columns, and once under `Human` or `Auto`. Only `Human` + `Auto`
|
|
770
|
-
the collected total. They are ordered worst-first — a conflict
|
|
771
|
-
rebase, a failing check may only need a re-run,
|
|
772
|
-
|
|
773
|
-
all.
|
|
767
|
+
`Conflict`, `Fail`, `Review`, `Copilot` and `Draft` **overlap** each other and
|
|
768
|
+
the author split, so they do not sum to `Total` and are not meant to: one pull
|
|
769
|
+
request that is conflicting, failing *and* a draft is counted once in each of
|
|
770
|
+
those three columns, and once under `Human` or `Auto`. Only `Human` + `Auto`
|
|
771
|
+
reconciles with the collected total. They are ordered worst-first — a conflict
|
|
772
|
+
needs a human to rebase, a failing check may only need a re-run, requested
|
|
773
|
+
changes need the author to act, unresolved automated feedback needs somebody but
|
|
774
|
+
does not hold the merge button down, and a draft is not blocked at all.
|
|
775
|
+
|
|
776
|
+
`Review` and `Copilot` sit together inside the blocker group, and deliberately
|
|
777
|
+
not beside `Human`: that column names who **raised** a pull request, whereas
|
|
778
|
+
these two name who is **waiting on** it. The grouping is what keeps those two
|
|
779
|
+
readings of "human" apart.
|
|
780
|
+
|
|
781
|
+
#### Requested changes
|
|
782
|
+
|
|
783
|
+
`Review` counts pull requests where **a person** has asked for changes and not
|
|
784
|
+
withdrawn it.
|
|
785
|
+
|
|
786
|
+
Two fields answer that, each covering the other's blind spot. `reviewDecision` is
|
|
787
|
+
GitHub's own verdict, computed over every review and accounting for dismissals,
|
|
788
|
+
so it is **exact at any review count** — but it names nobody, and a GitHub App
|
|
789
|
+
holding pull-request write permission can request changes exactly as a person
|
|
790
|
+
can. `latestOpinionatedReviews` names the reviewers, but is a bounded window of
|
|
791
|
+
five (one review per reviewer, so five reviewers rather than five reviews).
|
|
792
|
+
|
|
793
|
+
So the decision **gates** and the window **attributes**. Anything other than
|
|
794
|
+
`CHANGES_REQUESTED` is a definite zero whatever the window holds, and only once
|
|
795
|
+
GitHub says changes are outstanding does the reviewer's identity matter — which
|
|
796
|
+
keeps the cheap exact answer for almost every pull request and pays the window's
|
|
797
|
+
uncertainty on the few where it changes the reading. Automated reviewers are
|
|
798
|
+
recognised by the same rule as the `Auto` column.
|
|
799
|
+
|
|
800
|
+
Only `CHANGES_REQUESTED` gates. **`REVIEW_REQUIRED` is deliberately excluded**:
|
|
801
|
+
it reports that a branch rule demands a review, not that anybody objected, so on
|
|
802
|
+
an organisation that requires review by default it would mark nearly every human
|
|
803
|
+
pull request and distinguish none of them. The rule is written as an allow-list
|
|
804
|
+
of the one blocking state rather than as "anything that is not `APPROVED`", so a
|
|
805
|
+
state GitHub adds later arrives uncounted rather than pre-counted as a blocker.
|
|
806
|
+
|
|
807
|
+
Where a request for changes cannot be attributed — the reviews were unreadable,
|
|
808
|
+
the author is gone, or the window held only automated requests without covering
|
|
809
|
+
every reviewer — the pull request is left **uncounted rather than credited to a
|
|
810
|
+
person**, and the table says so in its description.
|
|
774
811
|
|
|
775
812
|
#### Unresolved Copilot feedback
|
|
776
813
|
|
|
777
|
-
`Copilot` counts the pull requests **waiting on
|
|
778
|
-
review thread counts when it was opened by GitHub's automated code
|
|
779
|
-
`copilot-pull-request-reviewer` bot) and nobody has resolved it.
|
|
780
|
-
identified from each thread's **opening** comment, since later
|
|
781
|
-
usually the human answering the review.
|
|
814
|
+
`Copilot` counts the pull requests **waiting on somebody to answer an automated
|
|
815
|
+
review**. A review thread counts when it was opened by GitHub's automated code
|
|
816
|
+
reviewer (the `copilot-pull-request-reviewer` bot) and nobody has resolved it.
|
|
817
|
+
The reviewer is identified from each thread's **opening** comment, since later
|
|
818
|
+
replies are usually the human answering the review.
|
|
782
819
|
|
|
783
820
|
An **outdated** thread still counts: GitHub marks a thread outdated when the
|
|
784
821
|
code beneath it changes, but that does not resolve it, so the feedback remains
|
|
785
822
|
unanswered.
|
|
786
823
|
|
|
787
|
-
Any non-zero count is coloured **
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
824
|
+
Any non-zero count is coloured **amber**, one step below the red of `Conflict`,
|
|
825
|
+
`Fail` and `Review`. Automated feedback is worth answering, but nobody is
|
|
826
|
+
waiting on it, and there is far more of it — colouring the two alike would let
|
|
827
|
+
the bot's threads mask the rarer case that actually holds a person up. The count
|
|
828
|
+
still feeds the row ranking, so a backlog awaiting review outranks an untouched
|
|
829
|
+
one of the same size.
|
|
830
|
+
|
|
831
|
+
The scan reads the **newest 20 review threads per pull request**, which is the
|
|
832
|
+
single most expensive part of the prefetch (see the cost figures above). The
|
|
833
|
+
newest end is deliberate: GitHub returns review threads oldest-first and offers
|
|
834
|
+
no way to order them, so reading from the front would window the threads a
|
|
835
|
+
review has already worked through — the resolved ones — and report a pull
|
|
836
|
+
request still waiting on its latest round as clear. Unanswered feedback is the
|
|
837
|
+
recent kind. Where a pull request carries more threads than the window and none
|
|
838
|
+
of the collected ones qualifies, it is treated as **indeterminate** rather than
|
|
839
|
+
clear — see below.
|
|
796
840
|
|
|
797
841
|
#### Automation backlog thresholds
|
|
798
842
|
|
|
799
843
|
On the terminal the counts are coloured so the table reads at a glance: `Human`
|
|
800
|
-
and `Ext` **green**, `Conflict`, `Fail` and `
|
|
801
|
-
coloured against two configured thresholds.
|
|
844
|
+
and `Ext` **green**, `Conflict`, `Fail` and `Review` **red**, `Copilot`
|
|
845
|
+
**amber**, and `Auto` coloured against two configured thresholds.
|
|
802
846
|
|
|
803
847
|
Dependabot stops raising pull requests once a repository reaches its
|
|
804
848
|
`open-pull-requests-limit`, so the repository quietly stops receiving dependency
|
|
@@ -1020,7 +1064,7 @@ reports as an App and any unrecognised login carrying the `[bot]` marker — so
|
|
|
1020
1064
|
future bot is classified as automation rather than mistaken for an outside
|
|
1021
1065
|
contributor.
|
|
1022
1066
|
|
|
1023
|
-
`Fail`, `Conflict` and `Copilot` count only **established** states. GitHub
|
|
1067
|
+
`Fail`, `Conflict`, `Review` and `Copilot` count only **established** states. GitHub
|
|
1024
1068
|
computes mergeability lazily and answers `UNKNOWN` until it settles, and reports
|
|
1025
1069
|
no check rollup at all when no checks have run; neither absence is evidence that
|
|
1026
1070
|
a pull request is ready, so neither is counted either way. `Copilot` follows the
|
|
@@ -1034,10 +1078,15 @@ holding any such reading says so in its description: `Copilot` then
|
|
|
1034
1078
|
**undercounts and should be read as a lower bound**, exactly as `Ext` does when
|
|
1035
1079
|
membership cannot be read.
|
|
1036
1080
|
|
|
1081
|
+
`Review` carries a weaker version of the same hedge, and earns it far less
|
|
1082
|
+
often: `reviewDecision` is exact, so only a pull request GitHub *does* report
|
|
1083
|
+
changes requested on can leave the question open, and then only when the
|
|
1084
|
+
opinionated-review window fails to attribute it to anybody.
|
|
1085
|
+
|
|
1037
1086
|
Repositories with no open pull requests are counted in the footer rather than
|
|
1038
1087
|
listed. Rows rank by total open pull requests, then by those failing,
|
|
1039
|
-
conflicting or awaiting
|
|
1040
|
-
overlap)
|
|
1088
|
+
conflicting or awaiting review — automated or human (counted once each, since
|
|
1089
|
+
the columns overlap) — so two repositories with equal backlogs surface the more
|
|
1041
1090
|
stuck one first.
|
|
1042
1091
|
|
|
1043
1092
|
> **Accuracy note.** As with the issues table, `Total` is exact at any size,
|
|
@@ -692,17 +692,17 @@ organisation](#inside-or-outside-the-organisation):
|
|
|
692
692
|
|
|
693
693
|
```text
|
|
694
694
|
Pull Requests
|
|
695
|
-
|
|
696
|
-
┃ Repository ┃ Human ┃ Ext ┃ Auto ┃ Conflict ┃ Fail ┃ Copilot ┃ Draft ┃ Total ┃
|
|
697
|
-
|
|
698
|
-
│ lftools-uv │ 8 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 8 │
|
|
699
|
-
│ dependamerge │ 3 │ 0 │ 0 │ 0 │ 0 │ 2 │ 0 │ 3 │
|
|
700
|
-
│ gha-workflow-linter │ 2 │ 0 │ 0 │ 0 │ 1 │ 0 │ 1 │ 2 │
|
|
701
|
-
│ harden-runner-block- │ 1 │ 0 │ 0 │ 1 │ 0 │ 0 │ 1 │ 1 │
|
|
702
|
-
│ action │ │ │ │ │ │ │ │ │
|
|
703
|
-
|
|
704
|
-
│ Total │ 14 │ 0 │ 0 │ 1 │ 1 │ 2 │ 2 │ 14 │
|
|
705
|
-
|
|
695
|
+
┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━┳━━━━━━┳━━━━━━━━━━┳━━━━━━┳━━━━━━━━┳━━━━━━━━━┳━━━━━━━┳━━━━━━━┓
|
|
696
|
+
┃ Repository ┃ Human ┃ Ext ┃ Auto ┃ Conflict ┃ Fail ┃ Review ┃ Copilot ┃ Draft ┃ Total ┃
|
|
697
|
+
┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━╇━━━━━━╇━━━━━━━━━━╇━━━━━━╇━━━━━━━━╇━━━━━━━━━╇━━━━━━━╇━━━━━━━┩
|
|
698
|
+
│ lftools-uv │ 8 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 8 │
|
|
699
|
+
│ dependamerge │ 3 │ 0 │ 0 │ 0 │ 0 │ 1 │ 2 │ 0 │ 3 │
|
|
700
|
+
│ gha-workflow-linter │ 2 │ 0 │ 0 │ 0 │ 1 │ 0 │ 0 │ 1 │ 2 │
|
|
701
|
+
│ harden-runner-block- │ 1 │ 0 │ 0 │ 1 │ 0 │ 0 │ 0 │ 1 │ 1 │
|
|
702
|
+
│ action │ │ │ │ │ │ │ │ │ │
|
|
703
|
+
├──────────────────────┼───────┼─────┼──────┼──────────┼──────┼────────┼─────────┼───────┼───────┤
|
|
704
|
+
│ Total │ 14 │ 0 │ 0 │ 1 │ 1 │ 1 │ 2 │ 2 │ 14 │
|
|
705
|
+
└──────────────────────┴───────┴─────┴──────┴──────────┴──────┴────────┴─────────┴───────┴───────┘
|
|
706
706
|
… and 9 more
|
|
707
707
|
❌ 13 With open pull requests
|
|
708
708
|
✅ 104 No open pull requests
|
|
@@ -716,45 +716,89 @@ The columns form **two independent groupings**:
|
|
|
716
716
|
| `Ext` | Human pull requests raised from outside the organisation — a **subset of Human**, which is why it sits beside it. |
|
|
717
717
|
| `Conflict` | Blocked on a merge conflict. |
|
|
718
718
|
| `Fail` | Latest checks did not pass. The rollup includes optional checks, so this is not by itself proof that the merge is blocked. |
|
|
719
|
+
| `Review` | A reviewer has **requested changes** — a person waiting on the author. |
|
|
719
720
|
| `Copilot` | Carries at least one **unresolved review thread** opened by GitHub's automated code reviewer. |
|
|
720
721
|
| `Draft` | Marked as a draft. |
|
|
721
722
|
|
|
722
|
-
`Conflict`, `Fail`, `Copilot` and `Draft` **overlap** each other and
|
|
723
|
-
split, so they do not sum to `Total` and are not meant to: one pull
|
|
724
|
-
is conflicting, failing *and* a draft is counted once in each of
|
|
725
|
-
columns, and once under `Human` or `Auto`. Only `Human` + `Auto`
|
|
726
|
-
the collected total. They are ordered worst-first — a conflict
|
|
727
|
-
rebase, a failing check may only need a re-run,
|
|
728
|
-
|
|
729
|
-
all.
|
|
723
|
+
`Conflict`, `Fail`, `Review`, `Copilot` and `Draft` **overlap** each other and
|
|
724
|
+
the author split, so they do not sum to `Total` and are not meant to: one pull
|
|
725
|
+
request that is conflicting, failing *and* a draft is counted once in each of
|
|
726
|
+
those three columns, and once under `Human` or `Auto`. Only `Human` + `Auto`
|
|
727
|
+
reconciles with the collected total. They are ordered worst-first — a conflict
|
|
728
|
+
needs a human to rebase, a failing check may only need a re-run, requested
|
|
729
|
+
changes need the author to act, unresolved automated feedback needs somebody but
|
|
730
|
+
does not hold the merge button down, and a draft is not blocked at all.
|
|
731
|
+
|
|
732
|
+
`Review` and `Copilot` sit together inside the blocker group, and deliberately
|
|
733
|
+
not beside `Human`: that column names who **raised** a pull request, whereas
|
|
734
|
+
these two name who is **waiting on** it. The grouping is what keeps those two
|
|
735
|
+
readings of "human" apart.
|
|
736
|
+
|
|
737
|
+
#### Requested changes
|
|
738
|
+
|
|
739
|
+
`Review` counts pull requests where **a person** has asked for changes and not
|
|
740
|
+
withdrawn it.
|
|
741
|
+
|
|
742
|
+
Two fields answer that, each covering the other's blind spot. `reviewDecision` is
|
|
743
|
+
GitHub's own verdict, computed over every review and accounting for dismissals,
|
|
744
|
+
so it is **exact at any review count** — but it names nobody, and a GitHub App
|
|
745
|
+
holding pull-request write permission can request changes exactly as a person
|
|
746
|
+
can. `latestOpinionatedReviews` names the reviewers, but is a bounded window of
|
|
747
|
+
five (one review per reviewer, so five reviewers rather than five reviews).
|
|
748
|
+
|
|
749
|
+
So the decision **gates** and the window **attributes**. Anything other than
|
|
750
|
+
`CHANGES_REQUESTED` is a definite zero whatever the window holds, and only once
|
|
751
|
+
GitHub says changes are outstanding does the reviewer's identity matter — which
|
|
752
|
+
keeps the cheap exact answer for almost every pull request and pays the window's
|
|
753
|
+
uncertainty on the few where it changes the reading. Automated reviewers are
|
|
754
|
+
recognised by the same rule as the `Auto` column.
|
|
755
|
+
|
|
756
|
+
Only `CHANGES_REQUESTED` gates. **`REVIEW_REQUIRED` is deliberately excluded**:
|
|
757
|
+
it reports that a branch rule demands a review, not that anybody objected, so on
|
|
758
|
+
an organisation that requires review by default it would mark nearly every human
|
|
759
|
+
pull request and distinguish none of them. The rule is written as an allow-list
|
|
760
|
+
of the one blocking state rather than as "anything that is not `APPROVED`", so a
|
|
761
|
+
state GitHub adds later arrives uncounted rather than pre-counted as a blocker.
|
|
762
|
+
|
|
763
|
+
Where a request for changes cannot be attributed — the reviews were unreadable,
|
|
764
|
+
the author is gone, or the window held only automated requests without covering
|
|
765
|
+
every reviewer — the pull request is left **uncounted rather than credited to a
|
|
766
|
+
person**, and the table says so in its description.
|
|
730
767
|
|
|
731
768
|
#### Unresolved Copilot feedback
|
|
732
769
|
|
|
733
|
-
`Copilot` counts the pull requests **waiting on
|
|
734
|
-
review thread counts when it was opened by GitHub's automated code
|
|
735
|
-
`copilot-pull-request-reviewer` bot) and nobody has resolved it.
|
|
736
|
-
identified from each thread's **opening** comment, since later
|
|
737
|
-
usually the human answering the review.
|
|
770
|
+
`Copilot` counts the pull requests **waiting on somebody to answer an automated
|
|
771
|
+
review**. A review thread counts when it was opened by GitHub's automated code
|
|
772
|
+
reviewer (the `copilot-pull-request-reviewer` bot) and nobody has resolved it.
|
|
773
|
+
The reviewer is identified from each thread's **opening** comment, since later
|
|
774
|
+
replies are usually the human answering the review.
|
|
738
775
|
|
|
739
776
|
An **outdated** thread still counts: GitHub marks a thread outdated when the
|
|
740
777
|
code beneath it changes, but that does not resolve it, so the feedback remains
|
|
741
778
|
unanswered.
|
|
742
779
|
|
|
743
|
-
Any non-zero count is coloured **
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
780
|
+
Any non-zero count is coloured **amber**, one step below the red of `Conflict`,
|
|
781
|
+
`Fail` and `Review`. Automated feedback is worth answering, but nobody is
|
|
782
|
+
waiting on it, and there is far more of it — colouring the two alike would let
|
|
783
|
+
the bot's threads mask the rarer case that actually holds a person up. The count
|
|
784
|
+
still feeds the row ranking, so a backlog awaiting review outranks an untouched
|
|
785
|
+
one of the same size.
|
|
786
|
+
|
|
787
|
+
The scan reads the **newest 20 review threads per pull request**, which is the
|
|
788
|
+
single most expensive part of the prefetch (see the cost figures above). The
|
|
789
|
+
newest end is deliberate: GitHub returns review threads oldest-first and offers
|
|
790
|
+
no way to order them, so reading from the front would window the threads a
|
|
791
|
+
review has already worked through — the resolved ones — and report a pull
|
|
792
|
+
request still waiting on its latest round as clear. Unanswered feedback is the
|
|
793
|
+
recent kind. Where a pull request carries more threads than the window and none
|
|
794
|
+
of the collected ones qualifies, it is treated as **indeterminate** rather than
|
|
795
|
+
clear — see below.
|
|
752
796
|
|
|
753
797
|
#### Automation backlog thresholds
|
|
754
798
|
|
|
755
799
|
On the terminal the counts are coloured so the table reads at a glance: `Human`
|
|
756
|
-
and `Ext` **green**, `Conflict`, `Fail` and `
|
|
757
|
-
coloured against two configured thresholds.
|
|
800
|
+
and `Ext` **green**, `Conflict`, `Fail` and `Review` **red**, `Copilot`
|
|
801
|
+
**amber**, and `Auto` coloured against two configured thresholds.
|
|
758
802
|
|
|
759
803
|
Dependabot stops raising pull requests once a repository reaches its
|
|
760
804
|
`open-pull-requests-limit`, so the repository quietly stops receiving dependency
|
|
@@ -976,7 +1020,7 @@ reports as an App and any unrecognised login carrying the `[bot]` marker — so
|
|
|
976
1020
|
future bot is classified as automation rather than mistaken for an outside
|
|
977
1021
|
contributor.
|
|
978
1022
|
|
|
979
|
-
`Fail`, `Conflict` and `Copilot` count only **established** states. GitHub
|
|
1023
|
+
`Fail`, `Conflict`, `Review` and `Copilot` count only **established** states. GitHub
|
|
980
1024
|
computes mergeability lazily and answers `UNKNOWN` until it settles, and reports
|
|
981
1025
|
no check rollup at all when no checks have run; neither absence is evidence that
|
|
982
1026
|
a pull request is ready, so neither is counted either way. `Copilot` follows the
|
|
@@ -990,10 +1034,15 @@ holding any such reading says so in its description: `Copilot` then
|
|
|
990
1034
|
**undercounts and should be read as a lower bound**, exactly as `Ext` does when
|
|
991
1035
|
membership cannot be read.
|
|
992
1036
|
|
|
1037
|
+
`Review` carries a weaker version of the same hedge, and earns it far less
|
|
1038
|
+
often: `reviewDecision` is exact, so only a pull request GitHub *does* report
|
|
1039
|
+
changes requested on can leave the question open, and then only when the
|
|
1040
|
+
opinionated-review window fails to attribute it to anybody.
|
|
1041
|
+
|
|
993
1042
|
Repositories with no open pull requests are counted in the footer rather than
|
|
994
1043
|
listed. Rows rank by total open pull requests, then by those failing,
|
|
995
|
-
conflicting or awaiting
|
|
996
|
-
overlap)
|
|
1044
|
+
conflicting or awaiting review — automated or human (counted once each, since
|
|
1045
|
+
the columns overlap) — so two repositories with equal backlogs surface the more
|
|
997
1046
|
stuck one first.
|
|
998
1047
|
|
|
999
1048
|
> **Accuracy note.** As with the issues table, `Total` is exact at any size,
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.
|
|
22
|
-
__version_tuple__ = version_tuple = (0,
|
|
21
|
+
__version__ = version = '0.15.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 15, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -311,9 +311,11 @@ _CATEGORIES: dict[CategoryKey, CategoryMeta] = {
|
|
|
311
311
|
"Conflict counts pull requests blocked on a merge conflict; Fail "
|
|
312
312
|
"counts those whose latest checks did not pass, which includes "
|
|
313
313
|
"optional checks and so is not by itself proof that a merge is "
|
|
314
|
-
"blocked;
|
|
315
|
-
"
|
|
316
|
-
"
|
|
314
|
+
"blocked; Review counts those a reviewer has asked for changes on, "
|
|
315
|
+
"which is a person waiting on the author; Copilot counts those "
|
|
316
|
+
"still carrying an unresolved review thread opened by GitHub's "
|
|
317
|
+
"automated code reviewer; Draft counts "
|
|
318
|
+
"those still marked as drafts. Those five "
|
|
317
319
|
"are independent of the author split and of each other, so one "
|
|
318
320
|
"pull request can appear in more than one of them. Ranked by total "
|
|
319
321
|
"open pull requests, then by those failing, conflicting or "
|
|
@@ -50,6 +50,11 @@ def _copilot_unresolved(node: dict) -> bool | None:
|
|
|
50
50
|
every thread was seen. This mirrors the rule ``mergeable`` and the check
|
|
51
51
|
rollup already follow: not established is not the same as nothing to report.
|
|
52
52
|
|
|
53
|
+
The window holds the *newest* threads (see ``_REVIEW_THREAD_WINDOW``), which
|
|
54
|
+
is what makes ``True`` reachable on a long review: unanswered feedback is
|
|
55
|
+
the recent kind, so a truncated window that finds nothing is a weak reading
|
|
56
|
+
rather than the usual one.
|
|
57
|
+
|
|
53
58
|
An outdated thread still counts. GitHub marks a thread outdated when the
|
|
54
59
|
code beneath it changes but does not resolve it, so the feedback remains
|
|
55
60
|
unanswered -- which is exactly what the column reports.
|
|
@@ -17,6 +17,7 @@ from typing import cast
|
|
|
17
17
|
import httpx
|
|
18
18
|
|
|
19
19
|
from github_security_report.client.copilot import _copilot_unresolved
|
|
20
|
+
from github_security_report.client.reviews import _changes_requested
|
|
20
21
|
from github_security_report.models import (
|
|
21
22
|
AuthorRef,
|
|
22
23
|
IssueRef,
|
|
@@ -341,6 +342,7 @@ def _pull_request_ref(node: object) -> PullRequestRef | None:
|
|
|
341
342
|
conflicting=conflicting,
|
|
342
343
|
failing=_check_rollup_failed(node),
|
|
343
344
|
copilot_unresolved=_copilot_unresolved(node),
|
|
345
|
+
changes_requested=_changes_requested(node),
|
|
344
346
|
)
|
|
345
347
|
|
|
346
348
|
|
|
@@ -111,6 +111,17 @@ _PULL_REQUEST_WINDOW = 25
|
|
|
111
111
|
# truncate, and the assignment breakdown is therefore exact for every collected
|
|
112
112
|
# pull request.
|
|
113
113
|
_ASSIGNEE_WINDOW = 10
|
|
114
|
+
# Opinionated reviews inspected per pull request, to tell whose request for
|
|
115
|
+
# changes is outstanding. ``reviewDecision`` says *that* changes are requested
|
|
116
|
+
# but names nobody, and a GitHub App with pull-request write permission can
|
|
117
|
+
# request them exactly as a person can -- so identity needs its own field.
|
|
118
|
+
#
|
|
119
|
+
# The connection returns at most one review per reviewer, so five covers a pull
|
|
120
|
+
# request with five opinionated reviewers rather than five reviews. Measured
|
|
121
|
+
# against a 25-repository batch this adds 6 points, which is the cheapest of the
|
|
122
|
+
# three windows here by an order of magnitude, and ``totalCount`` rides along
|
|
123
|
+
# free to keep it honest.
|
|
124
|
+
_OPINIONATED_REVIEW_WINDOW = 5
|
|
114
125
|
# Review threads inspected per pull request, to spot outstanding Copilot review
|
|
115
126
|
# feedback. This is the most expensive window in the fragment, because it
|
|
116
127
|
# multiplies the pull-request window and then again by the one comment needed to
|
|
@@ -120,8 +131,17 @@ _ASSIGNEE_WINDOW = 10
|
|
|
120
131
|
# without this connection and 57, 89 or 151 with a window of 5, 10 or 20. At 20
|
|
121
132
|
# a 118-repository organisation costs ~755 points of the 5,000-point hourly
|
|
122
133
|
# budget, which leaves room for the several organisations a scheduled run
|
|
123
|
-
# covers
|
|
124
|
-
#
|
|
134
|
+
# covers.
|
|
135
|
+
#
|
|
136
|
+
# The window is taken with ``last``, not ``first``, and which end it is anchored
|
|
137
|
+
# to decides whether it can answer the question at all. ``reviewThreads`` is
|
|
138
|
+
# ordered oldest-first and accepts no ``orderBy``, so ``first`` collects the
|
|
139
|
+
# threads a review has already worked through -- exactly the ones most likely to
|
|
140
|
+
# have been resolved. A pull request that has answered dozens of rounds and is
|
|
141
|
+
# now waiting on the latest would fill the window with settled threads and
|
|
142
|
+
# report the outstanding ones as never seen. Unresolved feedback ages into
|
|
143
|
+
# resolution rather than the other way round, so the newest threads are where
|
|
144
|
+
# the column's answer lives. Same node cost, opposite blind spot.
|
|
125
145
|
#
|
|
126
146
|
# ``totalCount`` rides along free of node cost and is what keeps the bounded
|
|
127
147
|
# window honest -- a pull request carrying more threads than the window returned
|
|
@@ -170,10 +190,15 @@ fragment RepoData on Repository {{
|
|
|
170
190
|
number
|
|
171
191
|
isDraft
|
|
172
192
|
mergeable
|
|
193
|
+
reviewDecision
|
|
173
194
|
authorAssociation
|
|
174
195
|
author {{ __typename login }}
|
|
175
196
|
assignees(first: {_ASSIGNEE_WINDOW}) {{ nodes {{ login }} }}
|
|
176
|
-
|
|
197
|
+
latestOpinionatedReviews(first: {_OPINIONATED_REVIEW_WINDOW}) {{
|
|
198
|
+
totalCount
|
|
199
|
+
nodes {{ state author {{ __typename login }} }}
|
|
200
|
+
}}
|
|
201
|
+
reviewThreads(last: {_REVIEW_THREAD_WINDOW}) {{
|
|
177
202
|
totalCount
|
|
178
203
|
nodes {{
|
|
179
204
|
isResolved
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# SPDX-FileCopyrightText: 2026 The Linux Foundation
|
|
3
|
+
"""Reading an outstanding request for changes from a pull-request node.
|
|
4
|
+
|
|
5
|
+
One question, asked of the two review fields the batched prefetch requests: is a
|
|
6
|
+
*person* waiting on this pull request's author? Kept apart from the general node
|
|
7
|
+
parsing for the same reason as the Copilot reading beside it -- it has to reason
|
|
8
|
+
about a bounded window, and so about what that window's edge does and does not
|
|
9
|
+
prove.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from github_security_report.authors import is_automation_author
|
|
15
|
+
|
|
16
|
+
# The one review decision that means somebody objected. An allow-list of that
|
|
17
|
+
# single state rather than "anything that is not APPROVED": REVIEW_REQUIRED says
|
|
18
|
+
# a branch rule demands a review, not that a reviewer objected, so counting it
|
|
19
|
+
# would mark nearly every pull request in an organisation that requires review by
|
|
20
|
+
# default. Writing the rule positively also means a state GitHub adds later
|
|
21
|
+
# arrives *uncounted* rather than pre-counted as a blocker.
|
|
22
|
+
_BLOCKING_DECISION = "CHANGES_REQUESTED"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _changes_requested(node: dict) -> bool | None:
|
|
26
|
+
"""Whether a *person* has asked for changes and not withdrawn it.
|
|
27
|
+
|
|
28
|
+
Two fields, each covering the other's blind spot. ``reviewDecision`` is
|
|
29
|
+
GitHub's own verdict, computed over every review and accounting for
|
|
30
|
+
dismissals, so it is exact however long the review ran -- but it names
|
|
31
|
+
nobody, and a GitHub App holding pull-request write permission can request
|
|
32
|
+
changes exactly as a person can. ``latestOpinionatedReviews`` names the
|
|
33
|
+
reviewers but is a bounded window.
|
|
34
|
+
|
|
35
|
+
So the decision *gates* and the window *attributes*. Anything other than
|
|
36
|
+
``CHANGES_REQUESTED`` is a definite False whatever the window holds, and
|
|
37
|
+
only once GitHub says changes are outstanding does identity matter. That
|
|
38
|
+
keeps the cheap exact answer for the overwhelming majority of pull requests
|
|
39
|
+
and pays the window's uncertainty on the few where it changes the reading.
|
|
40
|
+
|
|
41
|
+
``None`` is the open question: the fields could not be read, a requested
|
|
42
|
+
change carried no author to attribute it to, or the window held only
|
|
43
|
+
automated requests without covering every opinionated reviewer -- in which
|
|
44
|
+
case a person's may sit among the reviews this run never saw. As elsewhere,
|
|
45
|
+
None is "not established" rather than "nothing outstanding".
|
|
46
|
+
"""
|
|
47
|
+
if "reviewDecision" not in node:
|
|
48
|
+
return None
|
|
49
|
+
decision = node["reviewDecision"]
|
|
50
|
+
if not isinstance(decision, str) or decision != _BLOCKING_DECISION:
|
|
51
|
+
return False
|
|
52
|
+
reviews = node.get("latestOpinionatedReviews")
|
|
53
|
+
if not isinstance(reviews, dict):
|
|
54
|
+
return None
|
|
55
|
+
nodes = reviews.get("nodes")
|
|
56
|
+
if not isinstance(nodes, list):
|
|
57
|
+
return None
|
|
58
|
+
seen = 0
|
|
59
|
+
for review in nodes:
|
|
60
|
+
if not isinstance(review, dict):
|
|
61
|
+
continue
|
|
62
|
+
seen += 1
|
|
63
|
+
if review.get("state") != _BLOCKING_DECISION:
|
|
64
|
+
continue
|
|
65
|
+
author = review.get("author")
|
|
66
|
+
if not isinstance(author, dict):
|
|
67
|
+
# A request for changes we cannot attribute -- a deleted account,
|
|
68
|
+
# say. Counting it would guess at a person; skipping it would let
|
|
69
|
+
# the loop fall through to a confident False.
|
|
70
|
+
return None
|
|
71
|
+
if not is_automation_author(author.get("login"), author.get("__typename")):
|
|
72
|
+
return True
|
|
73
|
+
total = reviews.get("totalCount")
|
|
74
|
+
if not isinstance(total, int) or isinstance(total, bool) or total > seen:
|
|
75
|
+
return None
|
|
76
|
+
return False
|