github-security-report 0.17.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.
- github_security_report-0.17.0/README.md → github_security_report-0.18.1/PKG-INFO +155 -7
- github_security_report-0.17.0/PKG-INFO → github_security_report-0.18.1/README.md +111 -51
- {github_security_report-0.17.0 → github_security_report-0.18.1}/pyproject.toml +7 -7
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/_version.py +2 -2
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/cli/app.py +11 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/cli/boundary.py +8 -2
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/cli/modes.py +25 -10
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/cli/options.py +9 -7
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/client/__init__.py +10 -7
- github_security_report-0.18.1/src/github_security_report/client/accounting.py +161 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/client/endpoints.py +8 -0
- github_security_report-0.18.1/src/github_security_report/client/failures.py +115 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/client/transport.py +41 -96
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/collect/extras.py +2 -0
- github_security_report-0.18.1/src/github_security_report/collect/graph.py +106 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/collect/org.py +4 -60
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/config/__init__.py +2 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/config/loader.py +28 -16
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/config/models.py +13 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/config/schema.py +17 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/issues.py +73 -3
- {github_security_report-0.17.0 → github_security_report-0.18.1}/.gitignore +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/LICENSE +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/LICENSES/Apache-2.0.txt +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/scripts/README.md +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/__init__.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/authors.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/categories/__init__.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/categories/keys.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/categories/signals.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/categories/tables.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/classify.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/cli/__init__.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/cli/__main__.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/cli/outputs.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/cli/publish.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/cli/serialise.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/client/alerts.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/client/batch_errors.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/client/codeql_parsers.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/client/copilot.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/client/errors.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/client/org_reads.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/client/parsers.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/client/queries.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/client/reads.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/client/reviews.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/client/writes.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/codeql/__init__.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/codeql/cleanup.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/codeql/facts.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/codeql/tables.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/collect/__init__.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/collect/codeql.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/collect/context.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/collect/protocols.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/collect/repo.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/collect/scoping.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/config/order.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/gating.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/gitctx.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/layout.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/models.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/ordering.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/posture/__init__.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/posture/enablement.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/posture/facts.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/posture/releases.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/pulls/__init__.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/pulls/columns.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/pulls/counting.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/pulls/presentation.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/pulls/table.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/py.typed +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/ranking.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/remediate/__init__.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/remediate/codeql.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/remediate/model.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/render/__init__.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/render/html.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/render/markdown.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/render/slack.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/render/slack_limits.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/render/terminal.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/report/__init__.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/report/aggregate.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/report/display.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/report/signals.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/report/tables.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/rulesets.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/runner.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/scope.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/secret_patterns.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/severity.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/summary.py +0 -0
- {github_security_report-0.17.0 → github_security_report-0.18.1}/src/github_security_report/templates/index.html.j2 +0 -0
- {github_security_report-0.17.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
|
|
@@ -301,7 +345,10 @@ environment-variable name, never embedded.
|
|
|
301
345
|
"repo_min_age_days": 28,
|
|
302
346
|
"release_max_age_days": 60,
|
|
303
347
|
"codeql_stale_days": 30,
|
|
348
|
+
"issue_age_warn_days": 30,
|
|
349
|
+
"issue_age_error_days": 60,
|
|
304
350
|
"graph_batch": 10,
|
|
351
|
+
"concurrency": 9,
|
|
305
352
|
"order": { "style": "auto" }
|
|
306
353
|
},
|
|
307
354
|
"organizations": [
|
|
@@ -793,6 +840,38 @@ with HTTP 200 and this one field null, so affected repositories are reported as
|
|
|
793
840
|
`❓ Unknown` rather than counted as having no open issues — an unreadable backlog
|
|
794
841
|
is never presented as a clean one.
|
|
795
842
|
|
|
843
|
+
#### Issue colours and age thresholds
|
|
844
|
+
|
|
845
|
+
On the terminal the counts are coloured so the table reads at a glance: `Bug`
|
|
846
|
+
and `Untriaged` **red**, `Docs` **green**. As in the Pull Requests table, only
|
|
847
|
+
**non-zero** counts are coloured, so the eye lands on the rows with something
|
|
848
|
+
in them. The colours follow the column **name** (case-insensitively), so they
|
|
849
|
+
apply to a custom `issue_labels` that keeps a `Bug` or `Docs` column and simply
|
|
850
|
+
do not appear for one that renames them.
|
|
851
|
+
|
|
852
|
+
`Oldest` is coloured against two age thresholds, in days:
|
|
853
|
+
|
|
854
|
+
```json
|
|
855
|
+
{
|
|
856
|
+
"report": {
|
|
857
|
+
"issue_age_warn_days": 30,
|
|
858
|
+
"issue_age_error_days": 60
|
|
859
|
+
},
|
|
860
|
+
"organizations": [{ "name": "lfreleng-actions" }]
|
|
861
|
+
}
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
- **Green** while the oldest open issue is at most `issue_age_warn_days`
|
|
865
|
+
(default `30`) old.
|
|
866
|
+
- **Yellow** once it is older than `issue_age_warn_days`.
|
|
867
|
+
- **Red** once it is older than `issue_age_error_days` (default `60`).
|
|
868
|
+
|
|
869
|
+
An `unknown` age is never coloured. Either threshold may be set to `0` to
|
|
870
|
+
switch that level off; with both off the column is left uncoloured.
|
|
871
|
+
`issue_age_error_days` must be **strictly greater** than `issue_age_warn_days`
|
|
872
|
+
(or `0`), otherwise the yellow level could never be reached, so it is rejected
|
|
873
|
+
at load. Both are settable per organisation.
|
|
874
|
+
|
|
796
875
|
### Pull Requests
|
|
797
876
|
|
|
798
877
|
The `pull_requests` category counts each repository's **open pull requests**,
|
|
@@ -1267,16 +1346,25 @@ size is bounded by *latency*, not by rate-limit cost: 25 repositories measured
|
|
|
1267
1346
|
at ~9 s against `lfreleng-actions` and failed intermittently; 10 measured at
|
|
1268
1347
|
~3.5 s.
|
|
1269
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
|
+
|
|
1270
1353
|
`report.graph_batch` (default `10`; minimum `1`) sets the starting batch size,
|
|
1271
1354
|
globally or per organisation. It is a lever for a slow day rather than a hard
|
|
1272
1355
|
limit: a batch that still fails on size after the transport's own retries is
|
|
1273
|
-
re-issued at **half** the size, and
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
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.
|
|
1280
1368
|
|
|
1281
1369
|
The value can be set three ways, in this order of precedence:
|
|
1282
1370
|
|
|
@@ -1301,6 +1389,65 @@ a larger value buys nothing except a longer first failure.
|
|
|
1301
1389
|
> predating the flag will reject a set one with `No such option`, so set
|
|
1302
1390
|
> `GSR_GRAPH_BATCH` only once the pinned release supports it.
|
|
1303
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
|
+
|
|
1304
1451
|
### Pass/fail severity cutoff
|
|
1305
1452
|
|
|
1306
1453
|
The severity-ranked signals (CodeQL, Scorecard, Zizmor, aislop, Dependabot
|
|
@@ -1379,6 +1526,7 @@ and the Slack **bot token** is consumed by the workflow, not the CLI.
|
|
|
1379
1526
|
| `force_notify` | No | `false` | Post to Slack regardless of `report_day` |
|
|
1380
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 |
|
|
1381
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)) |
|
|
1382
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) |
|
|
1383
1531
|
| `use_local_source` | No | `false` | Run from the checked-out source instead of PyPI (for testing unreleased code from any event) |
|
|
1384
1532
|
|
|
@@ -1,47 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: github-security-report
|
|
3
|
-
Version: 0.17.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.8; 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
|
|
@@ -345,7 +301,10 @@ environment-variable name, never embedded.
|
|
|
345
301
|
"repo_min_age_days": 28,
|
|
346
302
|
"release_max_age_days": 60,
|
|
347
303
|
"codeql_stale_days": 30,
|
|
304
|
+
"issue_age_warn_days": 30,
|
|
305
|
+
"issue_age_error_days": 60,
|
|
348
306
|
"graph_batch": 10,
|
|
307
|
+
"concurrency": 9,
|
|
349
308
|
"order": { "style": "auto" }
|
|
350
309
|
},
|
|
351
310
|
"organizations": [
|
|
@@ -837,6 +796,38 @@ with HTTP 200 and this one field null, so affected repositories are reported as
|
|
|
837
796
|
`❓ Unknown` rather than counted as having no open issues — an unreadable backlog
|
|
838
797
|
is never presented as a clean one.
|
|
839
798
|
|
|
799
|
+
#### Issue colours and age thresholds
|
|
800
|
+
|
|
801
|
+
On the terminal the counts are coloured so the table reads at a glance: `Bug`
|
|
802
|
+
and `Untriaged` **red**, `Docs` **green**. As in the Pull Requests table, only
|
|
803
|
+
**non-zero** counts are coloured, so the eye lands on the rows with something
|
|
804
|
+
in them. The colours follow the column **name** (case-insensitively), so they
|
|
805
|
+
apply to a custom `issue_labels` that keeps a `Bug` or `Docs` column and simply
|
|
806
|
+
do not appear for one that renames them.
|
|
807
|
+
|
|
808
|
+
`Oldest` is coloured against two age thresholds, in days:
|
|
809
|
+
|
|
810
|
+
```json
|
|
811
|
+
{
|
|
812
|
+
"report": {
|
|
813
|
+
"issue_age_warn_days": 30,
|
|
814
|
+
"issue_age_error_days": 60
|
|
815
|
+
},
|
|
816
|
+
"organizations": [{ "name": "lfreleng-actions" }]
|
|
817
|
+
}
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
- **Green** while the oldest open issue is at most `issue_age_warn_days`
|
|
821
|
+
(default `30`) old.
|
|
822
|
+
- **Yellow** once it is older than `issue_age_warn_days`.
|
|
823
|
+
- **Red** once it is older than `issue_age_error_days` (default `60`).
|
|
824
|
+
|
|
825
|
+
An `unknown` age is never coloured. Either threshold may be set to `0` to
|
|
826
|
+
switch that level off; with both off the column is left uncoloured.
|
|
827
|
+
`issue_age_error_days` must be **strictly greater** than `issue_age_warn_days`
|
|
828
|
+
(or `0`), otherwise the yellow level could never be reached, so it is rejected
|
|
829
|
+
at load. Both are settable per organisation.
|
|
830
|
+
|
|
840
831
|
### Pull Requests
|
|
841
832
|
|
|
842
833
|
The `pull_requests` category counts each repository's **open pull requests**,
|
|
@@ -1311,16 +1302,25 @@ size is bounded by *latency*, not by rate-limit cost: 25 repositories measured
|
|
|
1311
1302
|
at ~9 s against `lfreleng-actions` and failed intermittently; 10 measured at
|
|
1312
1303
|
~3.5 s.
|
|
1313
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
|
+
|
|
1314
1309
|
`report.graph_batch` (default `10`; minimum `1`) sets the starting batch size,
|
|
1315
1310
|
globally or per organisation. It is a lever for a slow day rather than a hard
|
|
1316
1311
|
limit: a batch that still fails on size after the transport's own retries is
|
|
1317
|
-
re-issued at **half** the size, and
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
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.
|
|
1324
1324
|
|
|
1325
1325
|
The value can be set three ways, in this order of precedence:
|
|
1326
1326
|
|
|
@@ -1345,6 +1345,65 @@ a larger value buys nothing except a longer first failure.
|
|
|
1345
1345
|
> predating the flag will reject a set one with `No such option`, so set
|
|
1346
1346
|
> `GSR_GRAPH_BATCH` only once the pinned release supports it.
|
|
1347
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
|
+
|
|
1348
1407
|
### Pass/fail severity cutoff
|
|
1349
1408
|
|
|
1350
1409
|
The severity-ranked signals (CodeQL, Scorecard, Zizmor, aislop, Dependabot
|
|
@@ -1423,6 +1482,7 @@ and the Slack **bot token** is consumed by the workflow, not the CLI.
|
|
|
1423
1482
|
| `force_notify` | No | `false` | Post to Slack regardless of `report_day` |
|
|
1424
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 |
|
|
1425
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)) |
|
|
1426
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) |
|
|
1427
1487
|
| `use_local_source` | No | `false` | Run from the checked-out source instead of PyPI (for testing unreleased code from any event) |
|
|
1428
1488
|
|
|
@@ -45,7 +45,7 @@ keywords = [
|
|
|
45
45
|
]
|
|
46
46
|
dependencies = [
|
|
47
47
|
"httpx[http2]==0.28.1",
|
|
48
|
-
"typer==0.27.
|
|
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.
|
|
68
|
-
"ruff==0.
|
|
69
|
-
"types-jsonschema==4.26.0.
|
|
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.
|
|
100
|
-
"ruff==0.
|
|
101
|
-
"types-jsonschema==4.26.0.
|
|
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.
|
|
22
|
-
__version_tuple__ = version_tuple = (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(
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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``
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
organisation is what a reader of the flag expects, and
|
|
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:
|