github-security-report 0.18.0__tar.gz → 0.18.1__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 (97) hide show
  1. github_security_report-0.18.0/README.md → github_security_report-0.18.1/PKG-INFO +121 -7
  2. github_security_report-0.18.0/PKG-INFO → github_security_report-0.18.1/README.md +77 -51
  3. {github_security_report-0.18.0 → github_security_report-0.18.1}/pyproject.toml +7 -7
  4. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/_version.py +2 -2
  5. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/cli/app.py +11 -0
  6. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/cli/boundary.py +8 -2
  7. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/cli/modes.py +25 -10
  8. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/cli/options.py +9 -7
  9. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/client/__init__.py +10 -7
  10. github_security_report-0.18.1/src/github_security_report/client/accounting.py +161 -0
  11. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/client/endpoints.py +8 -0
  12. github_security_report-0.18.1/src/github_security_report/client/failures.py +115 -0
  13. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/client/transport.py +41 -96
  14. github_security_report-0.18.1/src/github_security_report/collect/graph.py +106 -0
  15. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/collect/org.py +4 -60
  16. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/config/__init__.py +2 -0
  17. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/config/loader.py +1 -0
  18. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/config/models.py +7 -0
  19. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/config/schema.py +11 -0
  20. {github_security_report-0.18.0 → github_security_report-0.18.1}/.gitignore +0 -0
  21. {github_security_report-0.18.0 → github_security_report-0.18.1}/LICENSE +0 -0
  22. {github_security_report-0.18.0 → github_security_report-0.18.1}/LICENSES/Apache-2.0.txt +0 -0
  23. {github_security_report-0.18.0 → github_security_report-0.18.1}/scripts/README.md +0 -0
  24. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/__init__.py +0 -0
  25. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/authors.py +0 -0
  26. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/categories/__init__.py +0 -0
  27. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/categories/keys.py +0 -0
  28. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/categories/signals.py +0 -0
  29. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/categories/tables.py +0 -0
  30. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/classify.py +0 -0
  31. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/cli/__init__.py +0 -0
  32. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/cli/__main__.py +0 -0
  33. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/cli/outputs.py +0 -0
  34. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/cli/publish.py +0 -0
  35. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/cli/serialise.py +0 -0
  36. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/client/alerts.py +0 -0
  37. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/client/batch_errors.py +0 -0
  38. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/client/codeql_parsers.py +0 -0
  39. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/client/copilot.py +0 -0
  40. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/client/errors.py +0 -0
  41. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/client/org_reads.py +0 -0
  42. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/client/parsers.py +0 -0
  43. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/client/queries.py +0 -0
  44. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/client/reads.py +0 -0
  45. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/client/reviews.py +0 -0
  46. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/client/writes.py +0 -0
  47. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/codeql/__init__.py +0 -0
  48. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/codeql/cleanup.py +0 -0
  49. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/codeql/facts.py +0 -0
  50. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/codeql/tables.py +0 -0
  51. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/collect/__init__.py +0 -0
  52. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/collect/codeql.py +0 -0
  53. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/collect/context.py +0 -0
  54. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/collect/extras.py +0 -0
  55. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/collect/protocols.py +0 -0
  56. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/collect/repo.py +0 -0
  57. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/collect/scoping.py +0 -0
  58. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/config/order.py +0 -0
  59. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/gating.py +0 -0
  60. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/gitctx.py +0 -0
  61. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/issues.py +0 -0
  62. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/layout.py +0 -0
  63. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/models.py +0 -0
  64. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/ordering.py +0 -0
  65. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/posture/__init__.py +0 -0
  66. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/posture/enablement.py +0 -0
  67. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/posture/facts.py +0 -0
  68. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/posture/releases.py +0 -0
  69. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/pulls/__init__.py +0 -0
  70. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/pulls/columns.py +0 -0
  71. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/pulls/counting.py +0 -0
  72. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/pulls/presentation.py +0 -0
  73. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/pulls/table.py +0 -0
  74. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/py.typed +0 -0
  75. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/ranking.py +0 -0
  76. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/remediate/__init__.py +0 -0
  77. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/remediate/codeql.py +0 -0
  78. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/remediate/model.py +0 -0
  79. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/render/__init__.py +0 -0
  80. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/render/html.py +0 -0
  81. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/render/markdown.py +0 -0
  82. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/render/slack.py +0 -0
  83. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/render/slack_limits.py +0 -0
  84. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/render/terminal.py +0 -0
  85. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/report/__init__.py +0 -0
  86. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/report/aggregate.py +0 -0
  87. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/report/display.py +0 -0
  88. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/report/signals.py +0 -0
  89. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/report/tables.py +0 -0
  90. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/rulesets.py +0 -0
  91. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/runner.py +0 -0
  92. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/scope.py +0 -0
  93. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/secret_patterns.py +0 -0
  94. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/severity.py +0 -0
  95. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/summary.py +0 -0
  96. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/templates/index.html.j2 +0 -0
  97. {github_security_report-0.18.0 → github_security_report-0.18.1}/src/github_security_report/templates/report.html.j2 +0 -0
@@ -1,3 +1,47 @@
1
+ Metadata-Version: 2.5
2
+ Name: github-security-report
3
+ Version: 0.18.1
4
+ Summary: Security and quality reporting across GitHub organisations
5
+ Project-URL: Homepage, https://github.com/lfreleng-actions/github-security-report-action
6
+ Project-URL: Repository, https://github.com/lfreleng-actions/github-security-report-action
7
+ Project-URL: Issues, https://github.com/lfreleng-actions/github-security-report-action/issues
8
+ Author-email: The Linux Foundation <mwatkins@linuxfoundation.org>
9
+ License-Expression: Apache-2.0
10
+ License-File: LICENSE
11
+ Keywords: codeql,dependabot,github,github-actions,linux-foundation,reporting,scorecard,secret-scanning,security,zizmor
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Security
24
+ Classifier: Topic :: Software Development :: Quality Assurance
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.10
27
+ Requires-Dist: httpx[http2]==0.28.1
28
+ Requires-Dist: jinja2==3.1.6
29
+ Requires-Dist: jsonschema==4.26.0
30
+ Requires-Dist: pyyaml==6.0.3
31
+ Requires-Dist: rich==15.0.0
32
+ Requires-Dist: typer==0.27.3
33
+ Provides-Extra: dev
34
+ Requires-Dist: mypy==2.4.0; extra == 'dev'
35
+ Requires-Dist: pytest-asyncio==1.4.0; extra == 'dev'
36
+ Requires-Dist: pytest-cov==7.1.0; extra == 'dev'
37
+ Requires-Dist: pytest==9.1.1; extra == 'dev'
38
+ Requires-Dist: respx==0.23.1; extra == 'dev'
39
+ Requires-Dist: ruff==0.17.0; extra == 'dev'
40
+ Requires-Dist: syrupy==6.1.1; extra == 'dev'
41
+ Requires-Dist: types-jsonschema==4.26.0.20261006; extra == 'dev'
42
+ Requires-Dist: types-pyyaml==6.0.12.20250915; extra == 'dev'
43
+ Description-Content-Type: text/markdown
44
+
1
45
  <!--
2
46
  # SPDX-License-Identifier: Apache-2.0
3
47
  # SPDX-FileCopyrightText: 2026 The Linux Foundation
@@ -304,6 +348,7 @@ environment-variable name, never embedded.
304
348
  "issue_age_warn_days": 30,
305
349
  "issue_age_error_days": 60,
306
350
  "graph_batch": 10,
351
+ "concurrency": 9,
307
352
  "order": { "style": "auto" }
308
353
  },
309
354
  "organizations": [
@@ -1301,16 +1346,25 @@ size is bounded by *latency*, not by rate-limit cost: 25 repositories measured
1301
1346
  at ~9 s against `lfreleng-actions` and failed intermittently; 10 measured at
1302
1347
  ~3.5 s.
1303
1348
 
1349
+ Up to four of these queries run at once. Each takes a few seconds on GitHub's
1350
+ side, so a prefetch issued one query at a time left most of the client's
1351
+ concurrency idle; against `lfreleng-actions` it took about 50 s of a 100 s run.
1352
+
1304
1353
  `report.graph_batch` (default `10`; minimum `1`) sets the starting batch size,
1305
1354
  globally or per organisation. It is a lever for a slow day rather than a hard
1306
1355
  limit: a batch that still fails on size after the transport's own retries is
1307
- re-issued at **half** the size, and the smaller size is kept for the rest of
1308
- that organisation's collection (each organisation in a multi-org run starts
1309
- from its own configured size, since organisations differ in how much data a
1310
- repository carries). Only a single-repository query GitHub still cannot
1311
- answer, or a failure that a smaller query could not fix (a `403`, an
1312
- exhausted rate limit, or a `500`/`503` outage rather than a `502`/`504`
1313
- timeout), aborts the run with exit code `3`.
1356
+ re-issued at **half** the size, and every batch issued after that failure is
1357
+ seen uses the smaller size for the rest of that organisation's collection
1358
+ (each organisation in a multi-org run starts from its own configured size,
1359
+ since organisations differ in how much data a repository carries). The
1360
+ learned size only shrinks. Because queries overlap, the batches already in
1361
+ flight when the first failure arrives were issued at the old size, so on a
1362
+ slow day up to four oversized batches, rather than one, may fail before the
1363
+ smaller size takes effect; that bounded cost buys the overlap. Only a
1364
+ single-repository query GitHub still cannot answer, or a failure that a
1365
+ smaller query could not fix (a `403`, an exhausted rate limit, or a
1366
+ `500`/`503` outage rather than a `502`/`504` timeout), aborts the run with
1367
+ exit code `3`, and the queries still in flight are cancelled first.
1314
1368
 
1315
1369
  The value can be set three ways, in this order of precedence:
1316
1370
 
@@ -1335,6 +1389,65 @@ a larger value buys nothing except a longer first failure.
1335
1389
  > predating the flag will reject a set one with `No such option`, so set
1336
1390
  > `GSR_GRAPH_BATCH` only once the pinned release supports it.
1337
1391
 
1392
+ ### Client concurrency
1393
+
1394
+ Each organisation's run holds at most `report.concurrency` requests to the
1395
+ GitHub API open at once (default `9`; minimum `1`, maximum `32`), globally or
1396
+ per organisation. Reads from the external Scorecard API queue on a separate
1397
+ limit of four, so a slow third-party endpoint never holds a slot that a
1398
+ GitHub request is waiting for.
1399
+
1400
+ The value can be set three ways, in this order of precedence:
1401
+
1402
+ 1. `--concurrency N` on the command line (the action's `concurrency` input).
1403
+ 2. A **repository or organisation variable** passed to that input. The
1404
+ bundled `reporting.yaml` passes `vars.GSR_CONCURRENCY`, so the value can be
1405
+ adjusted from repository settings without a workflow edit or a release.
1406
+ 3. `report.concurrency` in the configuration; otherwise the built-in `9`.
1407
+
1408
+ `9` is a deliberate halfway point. GitHub documents its secondary rate limits
1409
+ as no more than 100 concurrent requests across REST and GraphQL, and no more
1410
+ than 900 points per minute for REST endpoints; this project has hit those
1411
+ limits before, so the default stays well clear of them. Measured against
1412
+ `lfreleng-actions` (143 repositories, about 1,400 requests; one run each in
1413
+ the same hour, so read ±10–15 % as GitHub latency noise), before the GraphQL
1414
+ prefetch ran concurrently:
1415
+
1416
+ <!-- markdownlint-disable MD013 -->
1417
+
1418
+ | Concurrency | Wall | Throughput | Mean time per request | Average slots busy | GitHub pushback |
1419
+ | ----------- | ---: | ---------: | --------------------: | -----------------: | --------------- |
1420
+ | 6 (former default) | 102.5 s | 13.7 req/s | 0.283 s | 3.9 / 6 | none |
1421
+ | 12 | 75.9 s | 18.5 req/s | 0.286 s | 5.3 / 12 | none |
1422
+
1423
+ <!-- markdownlint-enable MD013 -->
1424
+
1425
+ Raise it once a run shows no pushback; lower it if GitHub starts answering
1426
+ with secondary rate-limit `403`/`429` responses. To measure a change, run the
1427
+ CLI with `--verbose`: when each organisation's collection finishes it logs
1428
+ one line of what the run cost, for example
1429
+
1430
+ <!-- markdownlint-disable MD013 -->
1431
+
1432
+ ```text
1433
+ GitHub API for lfreleng-actions: 1402 requests (0 retries), 822 core points, 64 graphql points, 41.3 MiB received, 102.5 s
1434
+ ```
1435
+
1436
+ <!-- markdownlint-enable MD013 -->
1437
+
1438
+ The request count is every request the tool issued, including retries and
1439
+ attempts that never got an answer. The points come from GitHub's own
1440
+ `x-ratelimit-*` response headers rather than from counting: GitHub meters a
1441
+ token across more than one bucket per resource (alike requests seconds apart
1442
+ can carry different `x-ratelimit-reset` values with independent counters), so
1443
+ each bucket's spread between its lowest and highest reading is summed per
1444
+ resource. The lowest reading already includes that request's own cost, so the
1445
+ figure is short by the first request of each bucket — a measurement aid, not a
1446
+ bill. Nothing of this line reaches the rendered report or the step summary.
1447
+
1448
+ > As with `graph_batch`, an empty input is not passed to the tool, so set
1449
+ > `GSR_CONCURRENCY` only once the pinned release supports `--concurrency`.
1450
+
1338
1451
  ### Pass/fail severity cutoff
1339
1452
 
1340
1453
  The severity-ranked signals (CodeQL, Scorecard, Zizmor, aislop, Dependabot
@@ -1413,6 +1526,7 @@ and the Slack **bot token** is consumed by the workflow, not the CLI.
1413
1526
  | `force_notify` | No | `false` | Post to Slack regardless of `report_day` |
1414
1527
  | `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 |
1415
1528
  | `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)) |
1529
+ | `concurrency` | No | `""` | Concurrent GitHub API requests per organisation (`1` to `32`; default: config, else `9`). Pass a repository or organisation variable such as `${{ vars.GSR_CONCURRENCY }}` to adjust it from settings (see [Client concurrency](#client-concurrency)) |
1416
1530
  | `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) |
1417
1531
  | `use_local_source` | No | `false` | Run from the checked-out source instead of PyPI (for testing unreleased code from any event) |
1418
1532
 
@@ -1,47 +1,3 @@
1
- Metadata-Version: 2.5
2
- Name: github-security-report
3
- Version: 0.18.0
4
- Summary: Security and quality reporting across GitHub organisations
5
- Project-URL: Homepage, https://github.com/lfreleng-actions/github-security-report-action
6
- Project-URL: Repository, https://github.com/lfreleng-actions/github-security-report-action
7
- Project-URL: Issues, https://github.com/lfreleng-actions/github-security-report-action/issues
8
- Author-email: The Linux Foundation <mwatkins@linuxfoundation.org>
9
- License-Expression: Apache-2.0
10
- License-File: LICENSE
11
- Keywords: codeql,dependabot,github,github-actions,linux-foundation,reporting,scorecard,secret-scanning,security,zizmor
12
- Classifier: Development Status :: 3 - Alpha
13
- Classifier: Intended Audience :: Developers
14
- Classifier: Operating System :: OS Independent
15
- Classifier: Programming Language :: Python
16
- Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3 :: Only
18
- Classifier: Programming Language :: Python :: 3.10
19
- Classifier: Programming Language :: Python :: 3.11
20
- Classifier: Programming Language :: Python :: 3.12
21
- Classifier: Programming Language :: Python :: 3.13
22
- Classifier: Programming Language :: Python :: 3.14
23
- Classifier: Topic :: Security
24
- Classifier: Topic :: Software Development :: Quality Assurance
25
- Classifier: Typing :: Typed
26
- Requires-Python: >=3.10
27
- Requires-Dist: httpx[http2]==0.28.1
28
- Requires-Dist: jinja2==3.1.6
29
- Requires-Dist: jsonschema==4.26.0
30
- Requires-Dist: pyyaml==6.0.3
31
- Requires-Dist: rich==15.0.0
32
- Requires-Dist: typer==0.27.2
33
- Provides-Extra: dev
34
- Requires-Dist: mypy==2.3.1; extra == 'dev'
35
- Requires-Dist: pytest-asyncio==1.4.0; extra == 'dev'
36
- Requires-Dist: pytest-cov==7.1.0; extra == 'dev'
37
- Requires-Dist: pytest==9.1.1; extra == 'dev'
38
- Requires-Dist: respx==0.23.1; extra == 'dev'
39
- Requires-Dist: ruff==0.16.9; extra == 'dev'
40
- Requires-Dist: syrupy==6.1.1; extra == 'dev'
41
- Requires-Dist: types-jsonschema==4.26.0.20260518; extra == 'dev'
42
- Requires-Dist: types-pyyaml==6.0.12.20250915; extra == 'dev'
43
- Description-Content-Type: text/markdown
44
-
45
1
  <!--
46
2
  # SPDX-License-Identifier: Apache-2.0
47
3
  # SPDX-FileCopyrightText: 2026 The Linux Foundation
@@ -348,6 +304,7 @@ environment-variable name, never embedded.
348
304
  "issue_age_warn_days": 30,
349
305
  "issue_age_error_days": 60,
350
306
  "graph_batch": 10,
307
+ "concurrency": 9,
351
308
  "order": { "style": "auto" }
352
309
  },
353
310
  "organizations": [
@@ -1345,16 +1302,25 @@ size is bounded by *latency*, not by rate-limit cost: 25 repositories measured
1345
1302
  at ~9 s against `lfreleng-actions` and failed intermittently; 10 measured at
1346
1303
  ~3.5 s.
1347
1304
 
1305
+ Up to four of these queries run at once. Each takes a few seconds on GitHub's
1306
+ side, so a prefetch issued one query at a time left most of the client's
1307
+ concurrency idle; against `lfreleng-actions` it took about 50 s of a 100 s run.
1308
+
1348
1309
  `report.graph_batch` (default `10`; minimum `1`) sets the starting batch size,
1349
1310
  globally or per organisation. It is a lever for a slow day rather than a hard
1350
1311
  limit: a batch that still fails on size after the transport's own retries is
1351
- re-issued at **half** the size, and the smaller size is kept for the rest of
1352
- that organisation's collection (each organisation in a multi-org run starts
1353
- from its own configured size, since organisations differ in how much data a
1354
- repository carries). Only a single-repository query GitHub still cannot
1355
- answer, or a failure that a smaller query could not fix (a `403`, an
1356
- exhausted rate limit, or a `500`/`503` outage rather than a `502`/`504`
1357
- timeout), aborts the run with exit code `3`.
1312
+ re-issued at **half** the size, and every batch issued after that failure is
1313
+ seen uses the smaller size for the rest of that organisation's collection
1314
+ (each organisation in a multi-org run starts from its own configured size,
1315
+ since organisations differ in how much data a repository carries). The
1316
+ learned size only shrinks. Because queries overlap, the batches already in
1317
+ flight when the first failure arrives were issued at the old size, so on a
1318
+ slow day up to four oversized batches, rather than one, may fail before the
1319
+ smaller size takes effect; that bounded cost buys the overlap. Only a
1320
+ single-repository query GitHub still cannot answer, or a failure that a
1321
+ smaller query could not fix (a `403`, an exhausted rate limit, or a
1322
+ `500`/`503` outage rather than a `502`/`504` timeout), aborts the run with
1323
+ exit code `3`, and the queries still in flight are cancelled first.
1358
1324
 
1359
1325
  The value can be set three ways, in this order of precedence:
1360
1326
 
@@ -1379,6 +1345,65 @@ a larger value buys nothing except a longer first failure.
1379
1345
  > predating the flag will reject a set one with `No such option`, so set
1380
1346
  > `GSR_GRAPH_BATCH` only once the pinned release supports it.
1381
1347
 
1348
+ ### Client concurrency
1349
+
1350
+ Each organisation's run holds at most `report.concurrency` requests to the
1351
+ GitHub API open at once (default `9`; minimum `1`, maximum `32`), globally or
1352
+ per organisation. Reads from the external Scorecard API queue on a separate
1353
+ limit of four, so a slow third-party endpoint never holds a slot that a
1354
+ GitHub request is waiting for.
1355
+
1356
+ The value can be set three ways, in this order of precedence:
1357
+
1358
+ 1. `--concurrency N` on the command line (the action's `concurrency` input).
1359
+ 2. A **repository or organisation variable** passed to that input. The
1360
+ bundled `reporting.yaml` passes `vars.GSR_CONCURRENCY`, so the value can be
1361
+ adjusted from repository settings without a workflow edit or a release.
1362
+ 3. `report.concurrency` in the configuration; otherwise the built-in `9`.
1363
+
1364
+ `9` is a deliberate halfway point. GitHub documents its secondary rate limits
1365
+ as no more than 100 concurrent requests across REST and GraphQL, and no more
1366
+ than 900 points per minute for REST endpoints; this project has hit those
1367
+ limits before, so the default stays well clear of them. Measured against
1368
+ `lfreleng-actions` (143 repositories, about 1,400 requests; one run each in
1369
+ the same hour, so read ±10–15 % as GitHub latency noise), before the GraphQL
1370
+ prefetch ran concurrently:
1371
+
1372
+ <!-- markdownlint-disable MD013 -->
1373
+
1374
+ | Concurrency | Wall | Throughput | Mean time per request | Average slots busy | GitHub pushback |
1375
+ | ----------- | ---: | ---------: | --------------------: | -----------------: | --------------- |
1376
+ | 6 (former default) | 102.5 s | 13.7 req/s | 0.283 s | 3.9 / 6 | none |
1377
+ | 12 | 75.9 s | 18.5 req/s | 0.286 s | 5.3 / 12 | none |
1378
+
1379
+ <!-- markdownlint-enable MD013 -->
1380
+
1381
+ Raise it once a run shows no pushback; lower it if GitHub starts answering
1382
+ with secondary rate-limit `403`/`429` responses. To measure a change, run the
1383
+ CLI with `--verbose`: when each organisation's collection finishes it logs
1384
+ one line of what the run cost, for example
1385
+
1386
+ <!-- markdownlint-disable MD013 -->
1387
+
1388
+ ```text
1389
+ GitHub API for lfreleng-actions: 1402 requests (0 retries), 822 core points, 64 graphql points, 41.3 MiB received, 102.5 s
1390
+ ```
1391
+
1392
+ <!-- markdownlint-enable MD013 -->
1393
+
1394
+ The request count is every request the tool issued, including retries and
1395
+ attempts that never got an answer. The points come from GitHub's own
1396
+ `x-ratelimit-*` response headers rather than from counting: GitHub meters a
1397
+ token across more than one bucket per resource (alike requests seconds apart
1398
+ can carry different `x-ratelimit-reset` values with independent counters), so
1399
+ each bucket's spread between its lowest and highest reading is summed per
1400
+ resource. The lowest reading already includes that request's own cost, so the
1401
+ figure is short by the first request of each bucket — a measurement aid, not a
1402
+ bill. Nothing of this line reaches the rendered report or the step summary.
1403
+
1404
+ > As with `graph_batch`, an empty input is not passed to the tool, so set
1405
+ > `GSR_CONCURRENCY` only once the pinned release supports `--concurrency`.
1406
+
1382
1407
  ### Pass/fail severity cutoff
1383
1408
 
1384
1409
  The severity-ranked signals (CodeQL, Scorecard, Zizmor, aislop, Dependabot
@@ -1457,6 +1482,7 @@ and the Slack **bot token** is consumed by the workflow, not the CLI.
1457
1482
  | `force_notify` | No | `false` | Post to Slack regardless of `report_day` |
1458
1483
  | `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 |
1459
1484
  | `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)) |
1485
+ | `concurrency` | No | `""` | Concurrent GitHub API requests per organisation (`1` to `32`; default: config, else `9`). Pass a repository or organisation variable such as `${{ vars.GSR_CONCURRENCY }}` to adjust it from settings (see [Client concurrency](#client-concurrency)) |
1460
1486
  | `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) |
1461
1487
  | `use_local_source` | No | `false` | Run from the checked-out source instead of PyPI (for testing unreleased code from any event) |
1462
1488
 
@@ -45,7 +45,7 @@ keywords = [
45
45
  ]
46
46
  dependencies = [
47
47
  "httpx[http2]==0.28.1",
48
- "typer==0.27.2",
48
+ "typer==0.27.3",
49
49
  "rich==15.0.0",
50
50
  "jinja2==3.1.6",
51
51
  "jsonschema==4.26.0",
@@ -64,9 +64,9 @@ dev = [
64
64
  "pytest-cov==7.1.0",
65
65
  "respx==0.23.1",
66
66
  "syrupy==6.1.1",
67
- "mypy==2.3.1",
68
- "ruff==0.16.9",
69
- "types-jsonschema==4.26.0.20260518",
67
+ "mypy==2.4.0",
68
+ "ruff==0.17.0",
69
+ "types-jsonschema==4.26.0.20261006",
70
70
  "types-PyYAML==6.0.12.20250915",
71
71
  ]
72
72
 
@@ -96,9 +96,9 @@ dev = [
96
96
  "pytest-cov==7.1.0",
97
97
  "respx==0.23.1",
98
98
  "syrupy==6.1.1",
99
- "mypy==2.3.1",
100
- "ruff==0.16.9",
101
- "types-jsonschema==4.26.0.20260518",
99
+ "mypy==2.4.0",
100
+ "ruff==0.17.0",
101
+ "types-jsonschema==4.26.0.20261006",
102
102
  "types-PyYAML==6.0.12.20250915",
103
103
  ]
104
104
 
@@ -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.18.0'
22
- __version_tuple__ = version_tuple = (0, 18, 0)
21
+ __version__ = version = '0.18.1'
22
+ __version_tuple__ = version_tuple = (0, 18, 1)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -27,6 +27,7 @@ from github_security_report.cli.modes import (
27
27
  )
28
28
  from github_security_report.cli.options import OrgRunOptions, ReportOverrides
29
29
  from github_security_report.cli.outputs import TopNLimits
30
+ from github_security_report.config import CONCURRENCY_MAX
30
31
  from github_security_report.scope import RepoSelectionError, parse_repo_selection
31
32
 
32
33
  app = typer.Typer(
@@ -172,6 +173,11 @@ def report(
172
173
  "--graph-batch",
173
174
  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.",
174
175
  ),
176
+ concurrency: int | None = typer.Option(
177
+ None,
178
+ "--concurrency",
179
+ help=f"Concurrent GitHub API requests per organisation (1-{CONCURRENCY_MAX}; default: config, else 9). Raise it to shorten a run, lower it if GitHub's secondary rate limits push back.",
180
+ ),
175
181
  no_color: bool = typer.Option(False, "--no-color", help="Disable coloured output."),
176
182
  ) -> None:
177
183
  """Generate a security and quality report."""
@@ -188,6 +194,9 @@ def report(
188
194
  boundary.check_non_negative(console, "--repo-min-age-days", repo_min_age_days)
189
195
  boundary.check_non_negative(console, "--release-max-age-days", release_max_age_days)
190
196
  boundary.check_positive(console, "--graph-batch", graph_batch)
197
+ boundary.check_positive(
198
+ console, "--concurrency", concurrency, maximum=CONCURRENCY_MAX
199
+ )
191
200
  hidden = boundary.resolve_hidden(console, hide)
192
201
 
193
202
  cfg = _load_config(config_file, config_data, org, token_env, console=console)
@@ -218,6 +227,7 @@ def report(
218
227
  include_archived=True if include_archived else None,
219
228
  include_test=True if include_test else None,
220
229
  graph_batch=graph_batch,
230
+ concurrency=concurrency,
221
231
  ),
222
232
  hidden=hidden,
223
233
  )
@@ -245,6 +255,7 @@ def report(
245
255
  ("--include-archived", include_archived),
246
256
  ("--include-test", include_test),
247
257
  ("--graph-batch", graph_batch is not None),
258
+ ("--concurrency", concurrency is not None),
248
259
  )
249
260
  if supplied
250
261
  ],
@@ -41,16 +41,22 @@ 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:
44
+ def check_positive(
45
+ console: Console, name: str, value: int | None, *, maximum: int | None = None
46
+ ) -> None:
45
47
  """Reject a zero or negative override where 0 has no meaning.
46
48
 
47
49
  For a batch size, unlike a row limit, there is no "unlimited" reading of
48
50
  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.
51
+ matching the config schema's minimum for the same control. ``maximum``,
52
+ when given, mirrors the schema's ceiling for the same control.
50
53
  """
51
54
  if value is not None and value < 1:
52
55
  console.print(f"[red]{name} must be 1 or greater[/red]")
53
56
  raise typer.Exit(2)
57
+ if value is not None and maximum is not None and value > maximum:
58
+ console.print(f"[red]{name} must be {maximum} or less[/red]")
59
+ raise typer.Exit(2)
54
60
 
55
61
 
56
62
  def check_limits(console: Console, limits: Sequence[tuple[str, int | None]]) -> None:
@@ -125,7 +125,16 @@ async def _collect_reports(
125
125
  now: dt.datetime,
126
126
  overrides: ReportOverrides,
127
127
  ) -> list[OrgPair] | None:
128
- """Collect every configured org's report, or None if a token is missing."""
128
+ """Collect every configured org's report, or None if a token is missing.
129
+
130
+ Once each organisation's client closes, what its requests cost is logged
131
+ at INFO: visible with ``--verbose``, which is how a run is measured, and
132
+ silent otherwise. Nothing of it reaches the rendered report. It is logged
133
+ whether or not the collection finished: a run that aborts on a rate limit,
134
+ an outage or a rejected token is exactly the run whose request, retry and
135
+ timing figures are wanted, so the exit path logs them before the error
136
+ propagates.
137
+ """
129
138
  pairs: list[OrgPair] = []
130
139
  for org_cfg in cfg.organizations:
131
140
  token = config.resolve_token(org_cfg)
@@ -135,15 +144,21 @@ async def _collect_reports(
135
144
  )
136
145
  return None
137
146
  effective_cfg, report_cfg = overrides.apply(org_cfg)
138
- async with GitHubClient(token) as client:
139
- pairs.append(
140
- (
141
- org_cfg,
142
- await collect.collect_org(
143
- client, effective_cfg, report_cfg, generated_at=now
144
- ),
147
+ client = GitHubClient(token, concurrency=report_cfg.concurrency)
148
+ try:
149
+ async with client:
150
+ pairs.append(
151
+ (
152
+ org_cfg,
153
+ await collect.collect_org(
154
+ client, effective_cfg, report_cfg, generated_at=now
155
+ ),
156
+ )
145
157
  )
146
- )
158
+ finally:
159
+ # After the context has closed the client, so the elapsed time
160
+ # covers the whole run; the error, if any, carries on from here.
161
+ log.info("GitHub API for %s: %s", org_cfg.name, client.stats.summary())
147
162
  return pairs
148
163
 
149
164
 
@@ -204,7 +219,7 @@ async def _run_repo(
204
219
  if not token:
205
220
  console.print(f"[red]No token in ${token_env}[/red]")
206
221
  return 2
207
- async with GitHubClient(token) as client:
222
+ async with GitHubClient(token, concurrency=cfg.concurrency) as client:
208
223
  repo, signals = await collect.collect_repo(
209
224
  client, owner, repo_name, ruleset_workflows=cfg.ruleset_workflows
210
225
  )
@@ -35,9 +35,9 @@ class ReportOverrides:
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
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.
38
+ ``graph_batch`` and ``concurrency`` are operational levers rather than
39
+ report policy: they change how the requests are issued, never what the
40
+ report says, so they may move in either direction.
41
41
  """
42
42
 
43
43
  repo_min_age_days: int | None = None
@@ -47,14 +47,15 @@ class ReportOverrides:
47
47
  include_archived: bool | None = None
48
48
  include_test: bool | None = None
49
49
  graph_batch: int | None = None
50
+ concurrency: int | None = None
50
51
 
51
52
  def apply(self, org_cfg: OrgConfig) -> tuple[OrgConfig, ReportConfig]:
52
53
  """The org and report configs to collect with, overrides applied.
53
54
 
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.
55
+ The two age thresholds, the three booleans, the batch size and the
56
+ concurrency are scalars, so applying one uniformly across every
57
+ configured organisation is what a reader of the flag expects, and
58
+ matches how ``--top-n`` already behaves.
58
59
 
59
60
  ``releases_exclude`` is not scalar: it is a curated per-organisation
60
61
  list, and one flag replacing all of them loses data the config
@@ -70,6 +71,7 @@ class ReportOverrides:
70
71
  "include_archived",
71
72
  "include_test",
72
73
  "graph_batch",
74
+ "concurrency",
73
75
  ):
74
76
  value = getattr(self, name)
75
77
  if value is not None:
@@ -11,9 +11,10 @@ See ``docs/BRIEF.md`` sections 9, 13 and ``docs/phase0-findings.md``.
11
11
  The implementation is split across ``endpoints`` (environment-resolved API URLs
12
12
  and the shared retry/backoff policy), ``queries`` (GraphQL documents),
13
13
  ``parsers`` (pure header/node parsing), ``transport`` (connection lifecycle and
14
- the retrying request primitives), ``alerts`` (the org-bulk sweeps and the
15
- two-pass secret-scanning read), ``org_reads`` (the remaining reads issued once
16
- per organisation), ``reads`` (the per-repository reads) and ``writes`` (the
14
+ the retrying request primitives), ``accounting`` (what a run's requests
15
+ cost), ``failures`` (the errors a request that cannot succeed ends in),
16
+ ``alerts`` (the org-bulk sweeps and the two-pass secret-scanning read),
17
+ ``org_reads`` (the remaining reads issued once per organisation), ``reads`` (the per-repository reads) and ``writes`` (the
17
18
  remediation writes plus the public :class:`GitHubClient`). This module
18
19
  re-exports the public surface, so importing from
19
20
  ``github_security_report.client`` is unchanged.
@@ -21,11 +22,13 @@ re-exports the public surface, so importing from
21
22
 
22
23
  from __future__ import annotations
23
24
 
25
+ from github_security_report.client.accounting import RequestStats
24
26
  from github_security_report.client.alerts import AlertReads
25
27
  from github_security_report.client.endpoints import (
26
28
  _SCORECARD_DEFAULT,
27
29
  API_BACKOFF_FACTOR,
28
30
  API_BACKOFF_INITIAL_SECONDS,
31
+ API_DEFAULT_CONCURRENCY,
29
32
  API_DIAGNOSTIC_DNS_TIMEOUT_SECONDS,
30
33
  API_MAX_RETRIES,
31
34
  API_MAX_TOTAL_WAIT_SECONDS,
@@ -40,6 +43,7 @@ from github_security_report.client.errors import (
40
43
  GraphBatchError,
41
44
  NetworkError,
42
45
  )
46
+ from github_security_report.client.failures import _endpoint_diagnostics
43
47
  from github_security_report.client.org_reads import OrgReadClient
44
48
  from github_security_report.client.parsers import (
45
49
  _last_published,
@@ -56,15 +60,13 @@ from github_security_report.client.queries import (
56
60
  _REPO_GRAPH_FRAGMENT,
57
61
  )
58
62
  from github_security_report.client.reads import ReadClient
59
- from github_security_report.client.transport import (
60
- Transport,
61
- _endpoint_diagnostics,
62
- )
63
+ from github_security_report.client.transport import Transport
63
64
  from github_security_report.client.writes import GitHubClient
64
65
 
65
66
  __all__ = [
66
67
  "API_BACKOFF_FACTOR",
67
68
  "API_BACKOFF_INITIAL_SECONDS",
69
+ "API_DEFAULT_CONCURRENCY",
68
70
  "API_DIAGNOSTIC_DNS_TIMEOUT_SECONDS",
69
71
  "API_MAX_RETRIES",
70
72
  "API_MAX_TOTAL_WAIT_SECONDS",
@@ -79,6 +81,7 @@ __all__ = [
79
81
  "NetworkError",
80
82
  "OrgReadClient",
81
83
  "ReadClient",
84
+ "RequestStats",
82
85
  "Transport",
83
86
  "_CODE_SCANNING_SIGNAL_TOOLS",
84
87
  "_DEPENDABOT_ENABLED_QUERY",