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.
Files changed (82) hide show
  1. {github_security_report-0.14.1 → github_security_report-0.15.0}/PKG-INFO +88 -39
  2. {github_security_report-0.14.1 → github_security_report-0.15.0}/README.md +87 -38
  3. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/_version.py +2 -2
  4. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/categories.py +5 -3
  5. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/client/copilot.py +5 -0
  6. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/client/parsers.py +2 -0
  7. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/client/queries.py +28 -3
  8. github_security_report-0.15.0/src/github_security_report/client/reviews.py +76 -0
  9. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/models.py +22 -71
  10. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/ordering.py +2 -2
  11. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/pulls/__init__.py +4 -0
  12. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/pulls/columns.py +10 -3
  13. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/pulls/counting.py +24 -4
  14. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/pulls/presentation.py +21 -1
  15. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/pulls/table.py +4 -0
  16. github_security_report-0.15.0/src/github_security_report/ranking.py +86 -0
  17. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/report/__init__.py +1 -1
  18. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/report/aggregate.py +1 -1
  19. {github_security_report-0.14.1 → github_security_report-0.15.0}/.gitignore +0 -0
  20. {github_security_report-0.14.1 → github_security_report-0.15.0}/LICENSE +0 -0
  21. {github_security_report-0.14.1 → github_security_report-0.15.0}/LICENSES/Apache-2.0.txt +0 -0
  22. {github_security_report-0.14.1 → github_security_report-0.15.0}/pyproject.toml +0 -0
  23. {github_security_report-0.14.1 → github_security_report-0.15.0}/scripts/README.md +0 -0
  24. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/__init__.py +0 -0
  25. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/authors.py +0 -0
  26. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/classify.py +0 -0
  27. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/cli/__init__.py +0 -0
  28. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/cli/__main__.py +0 -0
  29. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/cli/app.py +0 -0
  30. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/cli/boundary.py +0 -0
  31. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/cli/modes.py +0 -0
  32. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/cli/options.py +0 -0
  33. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/cli/outputs.py +0 -0
  34. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/cli/publish.py +0 -0
  35. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/cli/serialise.py +0 -0
  36. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/client/__init__.py +0 -0
  37. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/client/alerts.py +0 -0
  38. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/client/batch_errors.py +0 -0
  39. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/client/endpoints.py +0 -0
  40. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/client/errors.py +0 -0
  41. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/client/org_reads.py +0 -0
  42. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/client/reads.py +0 -0
  43. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/client/transport.py +0 -0
  44. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/client/writes.py +0 -0
  45. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/collect/__init__.py +0 -0
  46. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/collect/context.py +0 -0
  47. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/collect/extras.py +0 -0
  48. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/collect/org.py +0 -0
  49. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/collect/protocols.py +0 -0
  50. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/collect/repo.py +0 -0
  51. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/config/__init__.py +0 -0
  52. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/config/loader.py +0 -0
  53. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/config/models.py +0 -0
  54. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/config/order.py +0 -0
  55. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/config/schema.py +0 -0
  56. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/gating.py +0 -0
  57. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/gitctx.py +0 -0
  58. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/issues.py +0 -0
  59. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/layout.py +0 -0
  60. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/posture/__init__.py +0 -0
  61. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/posture/enablement.py +0 -0
  62. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/posture/facts.py +0 -0
  63. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/posture/releases.py +0 -0
  64. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/py.typed +0 -0
  65. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/remediate.py +0 -0
  66. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/render/__init__.py +0 -0
  67. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/render/html.py +0 -0
  68. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/render/markdown.py +0 -0
  69. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/render/slack.py +0 -0
  70. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/render/slack_limits.py +0 -0
  71. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/render/terminal.py +0 -0
  72. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/report/display.py +0 -0
  73. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/report/signals.py +0 -0
  74. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/report/tables.py +0 -0
  75. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/rulesets.py +0 -0
  76. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/runner.py +0 -0
  77. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/scope.py +0 -0
  78. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/secret_patterns.py +0 -0
  79. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/severity.py +0 -0
  80. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/summary.py +0 -0
  81. {github_security_report-0.14.1 → github_security_report-0.15.0}/src/github_security_report/templates/index.html.j2 +0 -0
  82. {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.14.1
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 the author
767
- split, so they do not sum to `Total` and are not meant to: one pull request that
768
- is conflicting, failing *and* a draft is counted once in each of those three
769
- columns, and once under `Human` or `Auto`. Only `Human` + `Auto` reconciles with
770
- the collected total. They are ordered worst-first — a conflict needs a human to
771
- rebase, a failing check may only need a re-run, unresolved review feedback needs
772
- a human but does not hold the merge button down, and a draft is not blocked at
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 a human to answer a review**. A
778
- review thread counts when it was opened by GitHub's automated code reviewer (the
779
- `copilot-pull-request-reviewer` bot) and nobody has resolved it. The reviewer is
780
- identified from each thread's **opening** comment, since later replies are
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 **red**, like `Conflict` and `Fail` — it marks a
788
- pull request that cannot progress until somebody responds. The count also feeds
789
- the row ranking, so a backlog awaiting review outranks an untouched one of the
790
- same size.
791
-
792
- The scan reads up to **20 review threads per pull request**, which is the single
793
- most expensive part of the prefetch (see the cost figures above). Where a pull
794
- request carries more threads than that and none of the collected ones qualifies,
795
- the pull request is treated as **indeterminate** rather than clear — see below.
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 `Copilot` **red**, and `Auto`
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 Copilot feedback (counted once each, since the columns
1040
- overlap), so two repositories with equal backlogs surface the more
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 the author
723
- split, so they do not sum to `Total` and are not meant to: one pull request that
724
- is conflicting, failing *and* a draft is counted once in each of those three
725
- columns, and once under `Human` or `Auto`. Only `Human` + `Auto` reconciles with
726
- the collected total. They are ordered worst-first — a conflict needs a human to
727
- rebase, a failing check may only need a re-run, unresolved review feedback needs
728
- a human but does not hold the merge button down, and a draft is not blocked at
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 a human to answer a review**. A
734
- review thread counts when it was opened by GitHub's automated code reviewer (the
735
- `copilot-pull-request-reviewer` bot) and nobody has resolved it. The reviewer is
736
- identified from each thread's **opening** comment, since later replies are
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 **red**, like `Conflict` and `Fail` — it marks a
744
- pull request that cannot progress until somebody responds. The count also feeds
745
- the row ranking, so a backlog awaiting review outranks an untouched one of the
746
- same size.
747
-
748
- The scan reads up to **20 review threads per pull request**, which is the single
749
- most expensive part of the prefetch (see the cost figures above). Where a pull
750
- request carries more threads than that and none of the collected ones qualifies,
751
- the pull request is treated as **indeterminate** rather than clear — see below.
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 `Copilot` **red**, and `Auto`
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 Copilot feedback (counted once each, since the columns
996
- overlap), so two repositories with equal backlogs surface the more
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.14.1'
22
- __version_tuple__ = version_tuple = (0, 14, 1)
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; Copilot counts those still carrying an unresolved review "
315
- "thread opened by GitHub's automated code reviewer; Draft counts "
316
- "those still marked as drafts. Those four "
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, and 20 threads covers a realistic review cycle: this project's own
124
- # most-reviewed pull requests carry single-figure thread counts.
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
- reviewThreads(first: {_REVIEW_THREAD_WINDOW}) {{
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