github-security-report 0.14.0__tar.gz → 0.14.2__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 (80) hide show
  1. {github_security_report-0.14.0 → github_security_report-0.14.2}/PKG-INFO +75 -8
  2. {github_security_report-0.14.0 → github_security_report-0.14.2}/README.md +73 -6
  3. {github_security_report-0.14.0 → github_security_report-0.14.2}/pyproject.toml +10 -1
  4. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/_version.py +2 -2
  5. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/cli/app.py +8 -0
  6. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/cli/boundary.py +12 -0
  7. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/cli/options.py +10 -3
  8. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/client/__init__.py +6 -2
  9. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/client/alerts.py +2 -1
  10. github_security_report-0.14.2/src/github_security_report/client/batch_errors.py +247 -0
  11. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/client/copilot.py +5 -0
  12. github_security_report-0.14.2/src/github_security_report/client/errors.py +79 -0
  13. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/client/org_reads.py +15 -95
  14. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/client/queries.py +14 -5
  15. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/client/transport.py +1 -32
  16. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/collect/__init__.py +6 -1
  17. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/collect/context.py +0 -4
  18. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/collect/org.py +51 -9
  19. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/config/loader.py +1 -0
  20. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/config/models.py +13 -0
  21. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/config/schema.py +8 -0
  22. {github_security_report-0.14.0 → github_security_report-0.14.2}/.gitignore +0 -0
  23. {github_security_report-0.14.0 → github_security_report-0.14.2}/LICENSE +0 -0
  24. {github_security_report-0.14.0 → github_security_report-0.14.2}/LICENSES/Apache-2.0.txt +0 -0
  25. {github_security_report-0.14.0 → github_security_report-0.14.2}/scripts/README.md +0 -0
  26. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/__init__.py +0 -0
  27. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/authors.py +0 -0
  28. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/categories.py +0 -0
  29. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/classify.py +0 -0
  30. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/cli/__init__.py +0 -0
  31. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/cli/__main__.py +0 -0
  32. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/cli/modes.py +0 -0
  33. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/cli/outputs.py +0 -0
  34. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/cli/publish.py +0 -0
  35. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/cli/serialise.py +0 -0
  36. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/client/endpoints.py +0 -0
  37. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/client/parsers.py +0 -0
  38. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/client/reads.py +0 -0
  39. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/client/writes.py +0 -0
  40. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/collect/extras.py +0 -0
  41. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/collect/protocols.py +0 -0
  42. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/collect/repo.py +0 -0
  43. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/config/__init__.py +0 -0
  44. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/config/order.py +0 -0
  45. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/gating.py +0 -0
  46. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/gitctx.py +0 -0
  47. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/issues.py +0 -0
  48. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/layout.py +0 -0
  49. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/models.py +0 -0
  50. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/ordering.py +0 -0
  51. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/posture/__init__.py +0 -0
  52. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/posture/enablement.py +0 -0
  53. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/posture/facts.py +0 -0
  54. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/posture/releases.py +0 -0
  55. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/pulls/__init__.py +0 -0
  56. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/pulls/columns.py +0 -0
  57. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/pulls/counting.py +0 -0
  58. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/pulls/presentation.py +0 -0
  59. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/pulls/table.py +0 -0
  60. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/py.typed +0 -0
  61. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/remediate.py +0 -0
  62. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/render/__init__.py +0 -0
  63. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/render/html.py +0 -0
  64. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/render/markdown.py +0 -0
  65. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/render/slack.py +0 -0
  66. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/render/slack_limits.py +0 -0
  67. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/render/terminal.py +0 -0
  68. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/report/__init__.py +0 -0
  69. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/report/aggregate.py +0 -0
  70. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/report/display.py +0 -0
  71. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/report/signals.py +0 -0
  72. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/report/tables.py +0 -0
  73. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/rulesets.py +0 -0
  74. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/runner.py +0 -0
  75. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/scope.py +0 -0
  76. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/secret_patterns.py +0 -0
  77. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/severity.py +0 -0
  78. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/summary.py +0 -0
  79. {github_security_report-0.14.0 → github_security_report-0.14.2}/src/github_security_report/templates/index.html.j2 +0 -0
  80. {github_security_report-0.14.0 → github_security_report-0.14.2}/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.0
3
+ Version: 0.14.2
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
@@ -29,7 +29,7 @@ Requires-Dist: jinja2==3.1.6
29
29
  Requires-Dist: jsonschema==4.26.0
30
30
  Requires-Dist: pyyaml==6.0.3
31
31
  Requires-Dist: rich==15.0.0
32
- Requires-Dist: typer==0.27.1
32
+ Requires-Dist: typer==0.27.2
33
33
  Provides-Extra: dev
34
34
  Requires-Dist: mypy==2.3.1; extra == 'dev'
35
35
  Requires-Dist: pytest-asyncio==1.4.0; extra == 'dev'
@@ -302,6 +302,7 @@ environment-variable name, never embedded.
302
302
  "include_test": false,
303
303
  "repo_min_age_days": 28,
304
304
  "release_max_age_days": 60,
305
+ "graph_batch": 10,
305
306
  "order": { "style": "auto" }
306
307
  },
307
308
  "organizations": [
@@ -788,10 +789,15 @@ pull request that cannot progress until somebody responds. The count also feeds
788
789
  the row ranking, so a backlog awaiting review outranks an untouched one of the
789
790
  same size.
790
791
 
791
- The scan reads up to **20 review threads per pull request**, which is the single
792
- most expensive part of the prefetch (see the cost figures above). Where a pull
793
- request carries more threads than that and none of the collected ones qualifies,
794
- the pull request is treated as **indeterminate** rather than clear — see below.
792
+ The scan reads the **newest 20 review threads per pull request**, which is the
793
+ single most expensive part of the prefetch (see the cost figures above). The
794
+ newest end is deliberate: GitHub returns review threads oldest-first and offers
795
+ no way to order them, so reading from the front would window the threads a
796
+ review has already worked through — the resolved ones — and report a pull
797
+ request still waiting on its latest round as clear. Unanswered feedback is the
798
+ recent kind. Where a pull request carries more threads than the window and none
799
+ of the collected ones qualifies, it is treated as **indeterminate** rather than
800
+ clear — see below.
795
801
 
796
802
  #### Automation backlog thresholds
797
803
 
@@ -1127,6 +1133,55 @@ Gating decides **collection**; the per-category render toggles above decide
1127
1133
  **presentation**. A skipped section still renders (as the one-line notice)
1128
1134
  unless its category is also disabled.
1129
1135
 
1136
+ ### GraphQL prefetch batch size
1137
+
1138
+ The releases/tags, Dependabot-enablement, open-issues and pull-request data
1139
+ for every in-scope repository comes from one aliased GraphQL query per batch
1140
+ of repositories rather than a round-trip per repository. GitHub bounds how
1141
+ much work a single GraphQL query may do — roughly ten seconds of execution —
1142
+ and reports a breach in one of three shapes, each after the query has already
1143
+ run for the full limit: an **HTTP 502** (or 504) gateway timeout from the
1144
+ edge, an HTTP 200 carrying no `data`, or an HTTP 200 whose `errors` say
1145
+ `Resource limits for this query exceeded` with the unresolved fields nulled.
1146
+ Each aliased repository adds a few hundred milliseconds, so the batch
1147
+ size is bounded by *latency*, not by rate-limit cost: 25 repositories measured
1148
+ at ~9 s against `lfreleng-actions` and failed intermittently; 10 measured at
1149
+ ~3.5 s.
1150
+
1151
+ `report.graph_batch` (default `10`; minimum `1`) sets the starting batch size,
1152
+ globally or per organisation. It is a lever for a slow day rather than a hard
1153
+ limit: a batch that still fails on size after the transport's own retries is
1154
+ re-issued at **half** the size, and the smaller size is kept for the rest of
1155
+ that organisation's collection (each organisation in a multi-org run starts
1156
+ from its own configured size, since organisations differ in how much data a
1157
+ repository carries). Only a single-repository query GitHub still cannot
1158
+ answer, or a failure that a smaller query could not fix (a `403`, an
1159
+ exhausted rate limit, or a `500`/`503` outage rather than a `502`/`504`
1160
+ timeout), aborts the run with exit code `3`.
1161
+
1162
+ The value can be set three ways, in this order of precedence:
1163
+
1164
+ 1. `--graph-batch N` on the command line (the action's `graph_batch` input).
1165
+ 2. A **repository or organisation variable** passed to that input. The
1166
+ bundled `reporting.yaml` passes `vars.GSR_GRAPH_BATCH`, so the size can be
1167
+ adjusted from repository settings without a workflow edit or a release;
1168
+ a caller's own workflow can do the same. (The `vars` context is not
1169
+ available inside a composite action, so the action cannot read it itself.)
1170
+ 3. `report.graph_batch` in the configuration; otherwise the built-in `10`.
1171
+
1172
+ Batching changes how many requests carry the data, not how many GraphQL nodes
1173
+ are resolved, so the node work is the same whatever the batch size. A smaller
1174
+ batch does cost slightly more of the rate-limit budget — each query is charged
1175
+ at least one point, and per-query cost rounding adds a little when one query
1176
+ becomes several — and a few more requests (a 124-repository organisation is 13
1177
+ queries at 10, 5 at 25). Against the 5,000-point hourly budget that is noise;
1178
+ a larger value buys nothing except a longer first failure.
1179
+
1180
+ > The flag and input are recent additions. An empty input is not passed to
1181
+ > the tool, so an unset variable cannot break a run; but a pinned tool version
1182
+ > predating the flag will reject a set one with `No such option`, so set
1183
+ > `GSR_GRAPH_BATCH` only once the pinned release supports it.
1184
+
1130
1185
  ### Pass/fail severity cutoff
1131
1186
 
1132
1187
  The severity-ranked signals (CodeQL, Scorecard, Zizmor, aislop, Dependabot
@@ -1204,6 +1259,7 @@ and the Slack **bot token** is consumed by the workflow, not the CLI.
1204
1259
  | `fail_threshold` | No | `none` | `none`/`low`/`medium`/`high`/`critical`/`any` (repo mode) |
1205
1260
  | `force_notify` | No | `false` | Post to Slack regardless of `report_day` |
1206
1261
  | `hide` | No | `""` | Category keys to suppress on every output (space- or comma-separated). Overrides config, and is one-way: it cannot re-enable a disabled category |
1262
+ | `graph_batch` | No | `""` | Repositories per batched GraphQL prefetch query (minimum `1`; default: config, else `10`). Pass a repository or organisation variable such as `${{ vars.GSR_GRAPH_BATCH }}` to adjust it from settings. A batch that still fails is halved automatically, so this sets the starting size (see [GraphQL prefetch batch size](#graphql-prefetch-batch-size)) |
1207
1263
  | `tool_version` | No | `""` | Published PyPI version to install. Empty (the default) uses the Dependabot-managed pin in `.github/runtime-pin/requirements.txt`; set a specific version to override. Ignored on pull requests or when `use_local_source` is `true` (both run from source) |
1208
1264
  | `use_local_source` | No | `false` | Run from the checked-out source instead of PyPI (for testing unreleased code from any event) |
1209
1265
 
@@ -1247,7 +1303,7 @@ API in the same way.
1247
1303
  | `0` | The report ran. |
1248
1304
  | `1` | Repo mode only: findings met or exceeded `--fail-threshold`. |
1249
1305
  | `2` | Usage or configuration error (bad flag, unreadable config). |
1250
- | `3` | The GitHub API was unreachable after the retry budget. |
1306
+ | `3` | The GitHub API was unusable after the retry budget (see below). |
1251
1307
  | `4` | GitHub rejected the credentials (HTTP 401). |
1252
1308
 
1253
1309
  Codes `3` and `4` are **aborts, not reports**: nothing is written and no Pages
@@ -1258,8 +1314,19 @@ section `No data` or `All Clean` — and a scheduled job would then publish it
1258
1314
  over the last good one. Reporting false data is worse than reporting none, so
1259
1315
  the run stops at the first rejected request.
1260
1316
 
1317
+ Code `3` covers a GitHub API that could not be reached at all, and a GraphQL
1318
+ prefetch that failed as a whole: either a size-shaped failure (`502`/`504`
1319
+ timeout, `200` with no data, resource limits exceeded) that persisted even
1320
+ after the batch had been halved down to one repository, or a failure that
1321
+ halving could not have fixed (`403`, an exhausted GraphQL rate limit, a
1322
+ `500`/`503` outage), which aborts at once. The message reports the status or
1323
+ cause and ends with the matching next step — a smaller `--graph-batch`, retry
1324
+ later, wait for the rate-limit budget to reset, or check the token's
1325
+ permissions — so read it rather than assuming "retry later" (see
1326
+ [GraphQL prefetch batch size](#graphql-prefetch-batch-size)).
1327
+
1261
1328
  The two are separate codes because the remedy differs: `4` means rotate or fix
1262
- the token, `3` means retry later.
1329
+ the token, `3` usually means retry later.
1263
1330
 
1264
1331
  ## Remediation
1265
1332
 
@@ -258,6 +258,7 @@ environment-variable name, never embedded.
258
258
  "include_test": false,
259
259
  "repo_min_age_days": 28,
260
260
  "release_max_age_days": 60,
261
+ "graph_batch": 10,
261
262
  "order": { "style": "auto" }
262
263
  },
263
264
  "organizations": [
@@ -744,10 +745,15 @@ pull request that cannot progress until somebody responds. The count also feeds
744
745
  the row ranking, so a backlog awaiting review outranks an untouched one of the
745
746
  same size.
746
747
 
747
- The scan reads up to **20 review threads per pull request**, which is the single
748
- most expensive part of the prefetch (see the cost figures above). Where a pull
749
- request carries more threads than that and none of the collected ones qualifies,
750
- the pull request is treated as **indeterminate** rather than clear — see below.
748
+ The scan reads the **newest 20 review threads per pull request**, which is the
749
+ single most expensive part of the prefetch (see the cost figures above). The
750
+ newest end is deliberate: GitHub returns review threads oldest-first and offers
751
+ no way to order them, so reading from the front would window the threads a
752
+ review has already worked through — the resolved ones — and report a pull
753
+ request still waiting on its latest round as clear. Unanswered feedback is the
754
+ recent kind. Where a pull request carries more threads than the window and none
755
+ of the collected ones qualifies, it is treated as **indeterminate** rather than
756
+ clear — see below.
751
757
 
752
758
  #### Automation backlog thresholds
753
759
 
@@ -1083,6 +1089,55 @@ Gating decides **collection**; the per-category render toggles above decide
1083
1089
  **presentation**. A skipped section still renders (as the one-line notice)
1084
1090
  unless its category is also disabled.
1085
1091
 
1092
+ ### GraphQL prefetch batch size
1093
+
1094
+ The releases/tags, Dependabot-enablement, open-issues and pull-request data
1095
+ for every in-scope repository comes from one aliased GraphQL query per batch
1096
+ of repositories rather than a round-trip per repository. GitHub bounds how
1097
+ much work a single GraphQL query may do — roughly ten seconds of execution —
1098
+ and reports a breach in one of three shapes, each after the query has already
1099
+ run for the full limit: an **HTTP 502** (or 504) gateway timeout from the
1100
+ edge, an HTTP 200 carrying no `data`, or an HTTP 200 whose `errors` say
1101
+ `Resource limits for this query exceeded` with the unresolved fields nulled.
1102
+ Each aliased repository adds a few hundred milliseconds, so the batch
1103
+ size is bounded by *latency*, not by rate-limit cost: 25 repositories measured
1104
+ at ~9 s against `lfreleng-actions` and failed intermittently; 10 measured at
1105
+ ~3.5 s.
1106
+
1107
+ `report.graph_batch` (default `10`; minimum `1`) sets the starting batch size,
1108
+ globally or per organisation. It is a lever for a slow day rather than a hard
1109
+ limit: a batch that still fails on size after the transport's own retries is
1110
+ re-issued at **half** the size, and the smaller size is kept for the rest of
1111
+ that organisation's collection (each organisation in a multi-org run starts
1112
+ from its own configured size, since organisations differ in how much data a
1113
+ repository carries). Only a single-repository query GitHub still cannot
1114
+ answer, or a failure that a smaller query could not fix (a `403`, an
1115
+ exhausted rate limit, or a `500`/`503` outage rather than a `502`/`504`
1116
+ timeout), aborts the run with exit code `3`.
1117
+
1118
+ The value can be set three ways, in this order of precedence:
1119
+
1120
+ 1. `--graph-batch N` on the command line (the action's `graph_batch` input).
1121
+ 2. A **repository or organisation variable** passed to that input. The
1122
+ bundled `reporting.yaml` passes `vars.GSR_GRAPH_BATCH`, so the size can be
1123
+ adjusted from repository settings without a workflow edit or a release;
1124
+ a caller's own workflow can do the same. (The `vars` context is not
1125
+ available inside a composite action, so the action cannot read it itself.)
1126
+ 3. `report.graph_batch` in the configuration; otherwise the built-in `10`.
1127
+
1128
+ Batching changes how many requests carry the data, not how many GraphQL nodes
1129
+ are resolved, so the node work is the same whatever the batch size. A smaller
1130
+ batch does cost slightly more of the rate-limit budget — each query is charged
1131
+ at least one point, and per-query cost rounding adds a little when one query
1132
+ becomes several — and a few more requests (a 124-repository organisation is 13
1133
+ queries at 10, 5 at 25). Against the 5,000-point hourly budget that is noise;
1134
+ a larger value buys nothing except a longer first failure.
1135
+
1136
+ > The flag and input are recent additions. An empty input is not passed to
1137
+ > the tool, so an unset variable cannot break a run; but a pinned tool version
1138
+ > predating the flag will reject a set one with `No such option`, so set
1139
+ > `GSR_GRAPH_BATCH` only once the pinned release supports it.
1140
+
1086
1141
  ### Pass/fail severity cutoff
1087
1142
 
1088
1143
  The severity-ranked signals (CodeQL, Scorecard, Zizmor, aislop, Dependabot
@@ -1160,6 +1215,7 @@ and the Slack **bot token** is consumed by the workflow, not the CLI.
1160
1215
  | `fail_threshold` | No | `none` | `none`/`low`/`medium`/`high`/`critical`/`any` (repo mode) |
1161
1216
  | `force_notify` | No | `false` | Post to Slack regardless of `report_day` |
1162
1217
  | `hide` | No | `""` | Category keys to suppress on every output (space- or comma-separated). Overrides config, and is one-way: it cannot re-enable a disabled category |
1218
+ | `graph_batch` | No | `""` | Repositories per batched GraphQL prefetch query (minimum `1`; default: config, else `10`). Pass a repository or organisation variable such as `${{ vars.GSR_GRAPH_BATCH }}` to adjust it from settings. A batch that still fails is halved automatically, so this sets the starting size (see [GraphQL prefetch batch size](#graphql-prefetch-batch-size)) |
1163
1219
  | `tool_version` | No | `""` | Published PyPI version to install. Empty (the default) uses the Dependabot-managed pin in `.github/runtime-pin/requirements.txt`; set a specific version to override. Ignored on pull requests or when `use_local_source` is `true` (both run from source) |
1164
1220
  | `use_local_source` | No | `false` | Run from the checked-out source instead of PyPI (for testing unreleased code from any event) |
1165
1221
 
@@ -1203,7 +1259,7 @@ API in the same way.
1203
1259
  | `0` | The report ran. |
1204
1260
  | `1` | Repo mode only: findings met or exceeded `--fail-threshold`. |
1205
1261
  | `2` | Usage or configuration error (bad flag, unreadable config). |
1206
- | `3` | The GitHub API was unreachable after the retry budget. |
1262
+ | `3` | The GitHub API was unusable after the retry budget (see below). |
1207
1263
  | `4` | GitHub rejected the credentials (HTTP 401). |
1208
1264
 
1209
1265
  Codes `3` and `4` are **aborts, not reports**: nothing is written and no Pages
@@ -1214,8 +1270,19 @@ section `No data` or `All Clean` — and a scheduled job would then publish it
1214
1270
  over the last good one. Reporting false data is worse than reporting none, so
1215
1271
  the run stops at the first rejected request.
1216
1272
 
1273
+ Code `3` covers a GitHub API that could not be reached at all, and a GraphQL
1274
+ prefetch that failed as a whole: either a size-shaped failure (`502`/`504`
1275
+ timeout, `200` with no data, resource limits exceeded) that persisted even
1276
+ after the batch had been halved down to one repository, or a failure that
1277
+ halving could not have fixed (`403`, an exhausted GraphQL rate limit, a
1278
+ `500`/`503` outage), which aborts at once. The message reports the status or
1279
+ cause and ends with the matching next step — a smaller `--graph-batch`, retry
1280
+ later, wait for the rate-limit budget to reset, or check the token's
1281
+ permissions — so read it rather than assuming "retry later" (see
1282
+ [GraphQL prefetch batch size](#graphql-prefetch-batch-size)).
1283
+
1217
1284
  The two are separate codes because the remedy differs: `4` means rotate or fix
1218
- the token, `3` means retry later.
1285
+ the token, `3` usually means retry later.
1219
1286
 
1220
1287
  ## Remediation
1221
1288
 
@@ -45,7 +45,7 @@ keywords = [
45
45
  ]
46
46
  dependencies = [
47
47
  "httpx[http2]==0.28.1",
48
- "typer==0.27.1",
48
+ "typer==0.27.2",
49
49
  "rich==15.0.0",
50
50
  "jinja2==3.1.6",
51
51
  "jsonschema==4.26.0",
@@ -129,6 +129,15 @@ markers = [
129
129
  [tool.coverage.run]
130
130
  source = ["github_security_report"]
131
131
  omit = ["tests/*"]
132
+ # Keep the transient coverage database out of the working tree.
133
+ # coverage.py resolves a relative data_file against the current
134
+ # directory, so the default '.coverage' lands wherever pytest runs and
135
+ # can be picked up by tests that inspect on-disk content. TEMP is
136
+ # set on Windows, where /tmp does not exist; elsewhere it is normally
137
+ # unset and the /tmp fallback applies. An explicit value also
138
+ # satisfies python-test-action's data_file configuration check.
139
+ # COVERAGE_FILE (env) still takes precedence when set.
140
+ data_file = "${TEMP-/tmp}/.coverage.github-security-report"
132
141
 
133
142
  [tool.coverage.report]
134
143
  show_missing = true
@@ -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.0'
22
- __version_tuple__ = version_tuple = (0, 14, 0)
21
+ __version__ = version = '0.14.2'
22
+ __version_tuple__ = version_tuple = (0, 14, 2)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -166,6 +166,11 @@ def report(
166
166
  "--include-test",
167
167
  help="Analyse test repositories, which are excluded by default.",
168
168
  ),
169
+ graph_batch: int | None = typer.Option(
170
+ None,
171
+ "--graph-batch",
172
+ help="Repositories per batched GraphQL prefetch query (minimum 1; default: config, else 10). GitHub bounds how long one query may run, so a batch that still fails is halved automatically; this sets the starting size.",
173
+ ),
169
174
  no_color: bool = typer.Option(False, "--no-color", help="Disable coloured output."),
170
175
  ) -> None:
171
176
  """Generate a security and quality report."""
@@ -181,6 +186,7 @@ def report(
181
186
  )
182
187
  boundary.check_non_negative(console, "--repo-min-age-days", repo_min_age_days)
183
188
  boundary.check_non_negative(console, "--release-max-age-days", release_max_age_days)
189
+ boundary.check_positive(console, "--graph-batch", graph_batch)
184
190
  hidden = boundary.resolve_hidden(console, hide)
185
191
 
186
192
  cfg = _load_config(config_file, config_data, org, token_env, console=console)
@@ -210,6 +216,7 @@ def report(
210
216
  gating=False if no_gating else None,
211
217
  include_archived=True if include_archived else None,
212
218
  include_test=True if include_test else None,
219
+ graph_batch=graph_batch,
213
220
  ),
214
221
  hidden=hidden,
215
222
  )
@@ -236,6 +243,7 @@ def report(
236
243
  ("--no-gating", no_gating),
237
244
  ("--include-archived", include_archived),
238
245
  ("--include-test", include_test),
246
+ ("--graph-batch", graph_batch is not None),
239
247
  )
240
248
  if supplied
241
249
  ],
@@ -41,6 +41,18 @@ def check_non_negative(console: Console, name: str, value: int | None) -> None:
41
41
  raise typer.Exit(2)
42
42
 
43
43
 
44
+ def check_positive(console: Console, name: str, value: int | None) -> None:
45
+ """Reject a zero or negative override where 0 has no meaning.
46
+
47
+ For a batch size, unlike a row limit, there is no "unlimited" reading of
48
+ 0 -- a query carrying no repositories is not a query -- so the floor is 1,
49
+ matching the config schema's minimum for the same control.
50
+ """
51
+ if value is not None and value < 1:
52
+ console.print(f"[red]{name} must be 1 or greater[/red]")
53
+ raise typer.Exit(2)
54
+
55
+
44
56
  def check_limits(console: Console, limits: Sequence[tuple[str, int | None]]) -> None:
45
57
  """Reject a negative row limit, naming the flag and the 0 convention."""
46
58
  for name, value in limits:
@@ -34,6 +34,10 @@ class ReportOverrides:
34
34
  not on, and ``--include-archived`` / ``--include-test`` can widen the scope
35
35
  but not narrow it, so a flag can loosen what the configuration asked for
36
36
  without being able to tighten it behind the operator's back.
37
+
38
+ ``graph_batch`` is an operational lever rather than report policy: it
39
+ changes how the GraphQL prefetch is issued, never what the report says, so
40
+ it may move in either direction.
37
41
  """
38
42
 
39
43
  repo_min_age_days: int | None = None
@@ -42,13 +46,15 @@ class ReportOverrides:
42
46
  gating: bool | None = None
43
47
  include_archived: bool | None = None
44
48
  include_test: bool | None = None
49
+ graph_batch: int | None = None
45
50
 
46
51
  def apply(self, org_cfg: OrgConfig) -> tuple[OrgConfig, ReportConfig]:
47
52
  """The org and report configs to collect with, overrides applied.
48
53
 
49
- The two age thresholds and the three booleans are scalar policy, so
50
- applying one uniformly across every configured organisation is what a
51
- reader of the flag expects, and matches how ``--top-n`` already behaves.
54
+ The two age thresholds, the three booleans and the batch size are
55
+ scalar policy, so applying one uniformly across every configured
56
+ organisation is what a reader of the flag expects, and matches how
57
+ ``--top-n`` already behaves.
52
58
 
53
59
  ``releases_exclude`` is not scalar: it is a curated per-organisation
54
60
  list, and one flag replacing all of them loses data the config
@@ -63,6 +69,7 @@ class ReportOverrides:
63
69
  "gating",
64
70
  "include_archived",
65
71
  "include_test",
72
+ "graph_batch",
66
73
  ):
67
74
  value = getattr(self, name)
68
75
  if value is not None:
@@ -35,6 +35,11 @@ from github_security_report.client.endpoints import (
35
35
  SCORECARD_API,
36
36
  _https_endpoint,
37
37
  )
38
+ from github_security_report.client.errors import (
39
+ AuthError,
40
+ GraphBatchError,
41
+ NetworkError,
42
+ )
38
43
  from github_security_report.client.org_reads import OrgReadClient
39
44
  from github_security_report.client.parsers import (
40
45
  _last_published,
@@ -52,8 +57,6 @@ from github_security_report.client.queries import (
52
57
  )
53
58
  from github_security_report.client.reads import ReadClient
54
59
  from github_security_report.client.transport import (
55
- AuthError,
56
- NetworkError,
57
60
  Transport,
58
61
  _endpoint_diagnostics,
59
62
  )
@@ -72,6 +75,7 @@ __all__ = [
72
75
  "AlertReads",
73
76
  "AuthError",
74
77
  "GitHubClient",
78
+ "GraphBatchError",
75
79
  "NetworkError",
76
80
  "OrgReadClient",
77
81
  "ReadClient",
@@ -27,7 +27,8 @@ import logging
27
27
  from typing import NamedTuple
28
28
 
29
29
  from github_security_report.client.endpoints import BULK_KINDS
30
- from github_security_report.client.transport import AuthError, NetworkError, Transport
30
+ from github_security_report.client.errors import AuthError, NetworkError
31
+ from github_security_report.client.transport import Transport
31
32
  from github_security_report.secret_patterns import (
32
33
  GENERIC_SECRET_TYPES,
33
34
  PATTERN_CONFIG_PATH,