github-security-report 0.20.0__tar.gz → 0.21.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 (120) hide show
  1. {github_security_report-0.20.0 → github_security_report-0.21.1}/PKG-INFO +32 -19
  2. {github_security_report-0.20.0 → github_security_report-0.21.1}/README.md +29 -16
  3. {github_security_report-0.20.0 → github_security_report-0.21.1}/pyproject.toml +4 -4
  4. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/_version.py +2 -2
  5. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/cli/progress.py +62 -42
  6. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/cli/serialise.py +2 -0
  7. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/malware.py +4 -2
  8. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/render/html.py +2 -0
  9. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/render/markdown.py +3 -1
  10. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/render/terminal.py +6 -1
  11. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/templates/report.html.j2 +1 -1
  12. {github_security_report-0.20.0 → github_security_report-0.21.1}/.gitignore +0 -0
  13. {github_security_report-0.20.0 → github_security_report-0.21.1}/LICENSE +0 -0
  14. {github_security_report-0.20.0 → github_security_report-0.21.1}/LICENSES/Apache-2.0.txt +0 -0
  15. {github_security_report-0.20.0 → github_security_report-0.21.1}/scripts/README.md +0 -0
  16. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/__init__.py +0 -0
  17. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/authors.py +0 -0
  18. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/categories/__init__.py +0 -0
  19. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/categories/keys.py +0 -0
  20. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/categories/signals.py +0 -0
  21. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/categories/tables.py +0 -0
  22. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/classify.py +0 -0
  23. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/cli/__init__.py +0 -0
  24. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/cli/__main__.py +0 -0
  25. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/cli/app.py +0 -0
  26. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/cli/boundary.py +0 -0
  27. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/cli/modes.py +0 -0
  28. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/cli/options.py +0 -0
  29. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/cli/outputs.py +0 -0
  30. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/cli/publish.py +0 -0
  31. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/cli/remediation.py +0 -0
  32. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/cli/summary.py +0 -0
  33. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/client/__init__.py +0 -0
  34. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/client/accounting.py +0 -0
  35. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/client/alerts.py +0 -0
  36. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/client/analyses.py +0 -0
  37. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/client/batch_errors.py +0 -0
  38. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/client/codeql_parsers.py +0 -0
  39. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/client/copilot.py +0 -0
  40. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/client/endpoints.py +0 -0
  41. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/client/errors.py +0 -0
  42. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/client/events.py +0 -0
  43. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/client/failures.py +0 -0
  44. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/client/org_reads.py +0 -0
  45. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/client/paging.py +0 -0
  46. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/client/parsers.py +0 -0
  47. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/client/queries.py +0 -0
  48. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/client/reads.py +0 -0
  49. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/client/repo_parser.py +0 -0
  50. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/client/reviews.py +0 -0
  51. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/client/transport.py +0 -0
  52. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/client/workflow_runs.py +0 -0
  53. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/client/writes.py +0 -0
  54. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/code_quality.py +0 -0
  55. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/codeql/__init__.py +0 -0
  56. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/codeql/cleanup.py +0 -0
  57. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/codeql/facts.py +0 -0
  58. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/codeql/tables.py +0 -0
  59. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/collect/__init__.py +0 -0
  60. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/collect/codeql.py +0 -0
  61. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/collect/context.py +0 -0
  62. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/collect/extras.py +0 -0
  63. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/collect/graph.py +0 -0
  64. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/collect/org.py +0 -0
  65. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/collect/protocols.py +0 -0
  66. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/collect/repo.py +0 -0
  67. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/collect/run_sync.py +0 -0
  68. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/collect/scoping.py +0 -0
  69. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/collect/workflow_runs.py +0 -0
  70. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/config/__init__.py +0 -0
  71. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/config/blocks.py +0 -0
  72. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/config/loader.py +0 -0
  73. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/config/models.py +0 -0
  74. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/config/order.py +0 -0
  75. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/config/schema.py +0 -0
  76. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/gating.py +0 -0
  77. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/gitctx.py +0 -0
  78. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/issue_presentation.py +0 -0
  79. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/issues.py +0 -0
  80. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/layout.py +0 -0
  81. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/models.py +0 -0
  82. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/ordering.py +0 -0
  83. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/posture/__init__.py +0 -0
  84. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/posture/enablement.py +0 -0
  85. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/posture/facts.py +0 -0
  86. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/posture/quality.py +0 -0
  87. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/posture/releases.py +0 -0
  88. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/pulls/__init__.py +0 -0
  89. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/pulls/columns.py +0 -0
  90. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/pulls/counting.py +0 -0
  91. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/pulls/presentation.py +0 -0
  92. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/pulls/table.py +0 -0
  93. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/py.typed +0 -0
  94. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/ranking.py +0 -0
  95. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/remediate/__init__.py +0 -0
  96. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/remediate/codeql.py +0 -0
  97. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/remediate/model.py +0 -0
  98. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/render/__init__.py +0 -0
  99. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/render/slack.py +0 -0
  100. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/render/slack_limits.py +0 -0
  101. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/render/slack_text.py +0 -0
  102. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/render/terminal_notices.py +0 -0
  103. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/report/__init__.py +0 -0
  104. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/report/aggregate.py +0 -0
  105. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/report/display.py +0 -0
  106. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/report/signals.py +0 -0
  107. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/report/tables.py +0 -0
  108. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/rulesets.py +0 -0
  109. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/runner.py +0 -0
  110. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/scope.py +0 -0
  111. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/secret_patterns.py +0 -0
  112. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/severity.py +0 -0
  113. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/summary.py +0 -0
  114. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/templates/index.html.j2 +0 -0
  115. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/workflow_runs/__init__.py +0 -0
  116. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/workflow_runs/aggregate.py +0 -0
  117. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/workflow_runs/models.py +0 -0
  118. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/workflow_runs/store.py +0 -0
  119. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/workflow_runs/store_codec.py +0 -0
  120. {github_security_report-0.20.0 → github_security_report-0.21.1}/src/github_security_report/workflow_runs/tables.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: github-security-report
3
- Version: 0.20.0
3
+ Version: 0.21.1
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
@@ -32,11 +32,11 @@ Requires-Dist: rich==15.0.0
32
32
  Requires-Dist: typer==0.27.3
33
33
  Provides-Extra: dev
34
34
  Requires-Dist: mypy==2.4.0; extra == 'dev'
35
- Requires-Dist: pytest-asyncio==1.4.0; extra == 'dev'
35
+ Requires-Dist: pytest-asyncio==1.4.1; extra == 'dev'
36
36
  Requires-Dist: pytest-cov==7.1.0; extra == 'dev'
37
37
  Requires-Dist: pytest==9.1.1; extra == 'dev'
38
38
  Requires-Dist: respx==0.23.1; extra == 'dev'
39
- Requires-Dist: ruff==0.17.1; extra == 'dev'
39
+ Requires-Dist: ruff==0.17.3; extra == 'dev'
40
40
  Requires-Dist: syrupy==6.1.1; extra == 'dev'
41
41
  Requires-Dist: types-jsonschema==4.26.0.20261006; extra == 'dev'
42
42
  Requires-Dist: types-pyyaml==6.0.12.20250915; extra == 'dev'
@@ -415,6 +415,18 @@ that only shape an organisation-wide run — `output_dir`, `pages_url`,
415
415
  misconfigured workflow says so instead of quietly producing a report that
416
416
  ignores them.
417
417
 
418
+ ### Runtime isolation
419
+
420
+ The action installs its own uv-managed Python and runs the tool from an
421
+ action-owned virtual environment with `python -I`, so interpreter settings an
422
+ earlier step of the caller's job exports (`UV_PYTHON`, `VIRTUAL_ENV`,
423
+ `CONDA_PREFIX`, `PYTHONPATH` and similar) cannot choose what runs.
424
+
425
+ The action trusts the caller's `PATH`. The runner resolves `bash` for every
426
+ `shell: bash` step on it, and setup-uv runs helpers such as `tar` from it, so
427
+ no composite action can defend against a caller that has put a directory from
428
+ its checkout on `PATH`.
429
+
418
430
  ## Configuration
419
431
 
420
432
  Configuration is JSON, supplied as a plain `vars.` entry or base64-encoded in a
@@ -1850,24 +1862,25 @@ Repo mode leaves nothing behind.
1850
1862
 
1851
1863
  ### Progress and run summary
1852
1864
 
1853
- On an interactive terminal an org-mode run keeps one live line at the bottom
1854
- of the screen while it collects: the organisation, the phase it is in, the
1855
- repository count where the phase works per repository, the REST and GraphQL
1856
- request counts with how many are in flight, and the elapsed time. A second
1857
- line appears while GitHub has the run paused for a rate limit, and while the
1865
+ On an interactive terminal an org-mode run keeps three live lines at the
1866
+ bottom of the screen while it collects, one kind of information each: where
1867
+ the run is (the organisation and the phase it is in), its API traffic (the
1868
+ REST and GraphQL request counts with how many are in flight, and external
1869
+ requests once there are any), and its progress (the elapsed time, the
1870
+ repository count where the phase works per repository, and a countdown while
1871
+ GitHub has the run paused for a rate limit). A fourth line appears while the
1858
1872
  workflow-run store is being built for the first time, since that one-off
1859
- dominates a cold run. Warnings logged in the meantime are held and replayed,
1860
- in order, once the line comes down.
1861
-
1862
- <!-- markdownlint-disable MD013 -->
1873
+ dominates a cold run. Each line stays one line: on a terminal too narrow for
1874
+ it, it is cut short with an ellipsis rather than wrapped, and the elapsed
1875
+ time, leading its line, is the last thing to go. Warnings logged in the
1876
+ meantime are held and replayed, in order, once the display comes down.
1863
1877
 
1864
1878
  ```text
1865
- ⠧ lfreleng-actions · probing repositories · 41/120 repos · REST 812 (9 in flight) · GraphQL 24 · 38.2 s
1866
- ⏳ rate limited by GitHub (core); resuming in 42 s
1879
+ ⠧ lfreleng-actions · probing repositories
1880
+ REST 812 (9 in flight) · GraphQL 24
1881
+ 38.2 s · 41/120 repos · ⏳ rate limited by GitHub (core); resuming in 42 s
1867
1882
  ```
1868
1883
 
1869
- <!-- markdownlint-enable MD013 -->
1870
-
1871
1884
  Before each organisation's report, a dim three-line summary says what the
1872
1885
  run cost and what the store holds, so a slow run explains itself without
1873
1886
  `--verbose`:
@@ -1890,9 +1903,9 @@ reads `in memory (not cached)` and, since its store is empty every time, is
1890
1903
  never a first run. The summary is for the terminal only: nothing of it
1891
1904
  reaches the Markdown, HTML, JSON or Slack output.
1892
1905
 
1893
- The live line is off under `--no-progress`, when `CI` is set, when stdout is
1894
- not a terminal, and with `--verbose`, whose log lines would fight it for the
1895
- screen. The summary prints in every case. The same figures reach the log at
1906
+ The live display is off under `--no-progress`, when `CI` is set, when stdout
1907
+ is not a terminal, and with `--verbose`, whose log lines would fight it for
1908
+ the screen. The summary prints in every case. The same figures reach the log at
1896
1909
  `INFO` under `--verbose`.
1897
1910
 
1898
1911
  ### GitHub Enterprise Server
@@ -371,6 +371,18 @@ that only shape an organisation-wide run — `output_dir`, `pages_url`,
371
371
  misconfigured workflow says so instead of quietly producing a report that
372
372
  ignores them.
373
373
 
374
+ ### Runtime isolation
375
+
376
+ The action installs its own uv-managed Python and runs the tool from an
377
+ action-owned virtual environment with `python -I`, so interpreter settings an
378
+ earlier step of the caller's job exports (`UV_PYTHON`, `VIRTUAL_ENV`,
379
+ `CONDA_PREFIX`, `PYTHONPATH` and similar) cannot choose what runs.
380
+
381
+ The action trusts the caller's `PATH`. The runner resolves `bash` for every
382
+ `shell: bash` step on it, and setup-uv runs helpers such as `tar` from it, so
383
+ no composite action can defend against a caller that has put a directory from
384
+ its checkout on `PATH`.
385
+
374
386
  ## Configuration
375
387
 
376
388
  Configuration is JSON, supplied as a plain `vars.` entry or base64-encoded in a
@@ -1806,24 +1818,25 @@ Repo mode leaves nothing behind.
1806
1818
 
1807
1819
  ### Progress and run summary
1808
1820
 
1809
- On an interactive terminal an org-mode run keeps one live line at the bottom
1810
- of the screen while it collects: the organisation, the phase it is in, the
1811
- repository count where the phase works per repository, the REST and GraphQL
1812
- request counts with how many are in flight, and the elapsed time. A second
1813
- line appears while GitHub has the run paused for a rate limit, and while the
1821
+ On an interactive terminal an org-mode run keeps three live lines at the
1822
+ bottom of the screen while it collects, one kind of information each: where
1823
+ the run is (the organisation and the phase it is in), its API traffic (the
1824
+ REST and GraphQL request counts with how many are in flight, and external
1825
+ requests once there are any), and its progress (the elapsed time, the
1826
+ repository count where the phase works per repository, and a countdown while
1827
+ GitHub has the run paused for a rate limit). A fourth line appears while the
1814
1828
  workflow-run store is being built for the first time, since that one-off
1815
- dominates a cold run. Warnings logged in the meantime are held and replayed,
1816
- in order, once the line comes down.
1817
-
1818
- <!-- markdownlint-disable MD013 -->
1829
+ dominates a cold run. Each line stays one line: on a terminal too narrow for
1830
+ it, it is cut short with an ellipsis rather than wrapped, and the elapsed
1831
+ time, leading its line, is the last thing to go. Warnings logged in the
1832
+ meantime are held and replayed, in order, once the display comes down.
1819
1833
 
1820
1834
  ```text
1821
- ⠧ lfreleng-actions · probing repositories · 41/120 repos · REST 812 (9 in flight) · GraphQL 24 · 38.2 s
1822
- ⏳ rate limited by GitHub (core); resuming in 42 s
1835
+ ⠧ lfreleng-actions · probing repositories
1836
+ REST 812 (9 in flight) · GraphQL 24
1837
+ 38.2 s · 41/120 repos · ⏳ rate limited by GitHub (core); resuming in 42 s
1823
1838
  ```
1824
1839
 
1825
- <!-- markdownlint-enable MD013 -->
1826
-
1827
1840
  Before each organisation's report, a dim three-line summary says what the
1828
1841
  run cost and what the store holds, so a slow run explains itself without
1829
1842
  `--verbose`:
@@ -1846,9 +1859,9 @@ reads `in memory (not cached)` and, since its store is empty every time, is
1846
1859
  never a first run. The summary is for the terminal only: nothing of it
1847
1860
  reaches the Markdown, HTML, JSON or Slack output.
1848
1861
 
1849
- The live line is off under `--no-progress`, when `CI` is set, when stdout is
1850
- not a terminal, and with `--verbose`, whose log lines would fight it for the
1851
- screen. The summary prints in every case. The same figures reach the log at
1862
+ The live display is off under `--no-progress`, when `CI` is set, when stdout
1863
+ is not a terminal, and with `--verbose`, whose log lines would fight it for
1864
+ the screen. The summary prints in every case. The same figures reach the log at
1852
1865
  `INFO` under `--verbose`.
1853
1866
 
1854
1867
  ### GitHub Enterprise Server
@@ -60,12 +60,12 @@ Issues = "https://github.com/lfreleng-actions/github-security-report-action/issu
60
60
  [project.optional-dependencies]
61
61
  dev = [
62
62
  "pytest==9.1.1",
63
- "pytest-asyncio==1.4.0",
63
+ "pytest-asyncio==1.4.1",
64
64
  "pytest-cov==7.1.0",
65
65
  "respx==0.23.1",
66
66
  "syrupy==6.1.1",
67
67
  "mypy==2.4.0",
68
- "ruff==0.17.1",
68
+ "ruff==0.17.3",
69
69
  "types-jsonschema==4.26.0.20261006",
70
70
  "types-PyYAML==6.0.12.20250915",
71
71
  ]
@@ -92,12 +92,12 @@ include = ["src", "README.md", "LICENSE*", "pyproject.toml"]
92
92
  [dependency-groups]
93
93
  dev = [
94
94
  "pytest==9.1.1",
95
- "pytest-asyncio==1.4.0",
95
+ "pytest-asyncio==1.4.1",
96
96
  "pytest-cov==7.1.0",
97
97
  "respx==0.23.1",
98
98
  "syrupy==6.1.1",
99
99
  "mypy==2.4.0",
100
- "ruff==0.17.1",
100
+ "ruff==0.17.3",
101
101
  "types-jsonschema==4.26.0.20261006",
102
102
  "types-PyYAML==6.0.12.20250915",
103
103
  ]
@@ -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.20.0'
22
- __version_tuple__ = version_tuple = (0, 20, 0)
21
+ __version__ = version = '0.21.1'
22
+ __version_tuple__ = version_tuple = (0, 21, 1)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -6,14 +6,20 @@
6
6
  :class:`~github_security_report.client.events.ProgressSink` the transport
7
7
  reports requests to and the
8
8
  :class:`~github_security_report.collect.context.CollectProgress` the
9
- collectors name their phases through, and renders what it hears as one Rich
10
- ``Live`` line per organisation: a spinner, the phase, the repository count
11
- where the phase is per-repository, the REST and GraphQL request counts with
12
- how many are in flight, and the clock. A second line appears only while
13
- something exceptional lasts: a rate-limit backoff, or a first-time build of
14
- the workflow-run store.
15
-
16
- While the line is on screen, WARNINGs bound for the terminal are held and
9
+ collectors name their phases through, and renders what it hears as a Rich
10
+ ``Live`` display of three lines per organisation, one category each so none
11
+ is cut short on an 80-column terminal:
12
+
13
+ 1. where the run is: a spinner, the organisation and the phase;
14
+ 2. API traffic: the REST and GraphQL request counts with how many are in
15
+ flight, and the external count once one has been made;
16
+ 3. progress: the clock, the repository count where the phase is
17
+ per-repository, and a rate-limit backoff's countdown while it lasts.
18
+
19
+ A fourth line appears only while the workflow-run store is built for the
20
+ first time, a notice too long to share the third.
21
+
22
+ While the display is on screen, WARNINGs bound for the terminal are held and
17
23
  replayed once it stops, so a transport warning cannot tear through it and the
18
24
  operator still reads every one, in order, in a single block before the run
19
25
  summary. Off (a :class:`NullTracker`) in CI, when stdout is not a terminal,
@@ -167,7 +173,7 @@ class _LogHold:
167
173
 
168
174
 
169
175
  class RunTracker:
170
- """One organisation's live progress line (see the module docstring)."""
176
+ """One organisation's live progress display (see the module docstring)."""
171
177
 
172
178
  def __init__(
173
179
  self, console: Console, *, clock: Callable[[], float] = time.monotonic
@@ -193,7 +199,7 @@ class RunTracker:
193
199
 
194
200
  # -- lifecycle ----------------------------------------------------------
195
201
  def start(self, org: str) -> None:
196
- """Reset the counters for ``org`` and put the line on screen."""
202
+ """Reset the counters for ``org`` and put the display on screen."""
197
203
  self._org = org
198
204
  self._started = self._clock()
199
205
  self._stopped = None
@@ -221,7 +227,7 @@ class RunTracker:
221
227
  self._live = live
222
228
 
223
229
  def stop(self) -> None:
224
- """Freeze the line, take the display down and replay held warnings.
230
+ """Freeze the display, take it down and replay held warnings.
225
231
 
226
232
  Best effort: callers run this from ``finally``, so a display that
227
233
  fails to draw its last frame must neither replace the error the run
@@ -294,56 +300,70 @@ class RunTracker:
294
300
  def _clock_text(self) -> str:
295
301
  return f"{self._elapsed():.1f} s"
296
302
 
297
- def _body(self) -> str:
298
- """The line without its clock: what the renderer may truncate."""
299
- parts = [self._org, self._phase if self._stopped is None else "done"]
300
- if self._repos_total is not None:
301
- parts.append(f"{self._repos_done}/{self._repos_total} repos")
302
- parts.append(self._count("REST", "rest"))
303
- parts.append(self._count("GraphQL", "graphql"))
303
+ def _where(self) -> str:
304
+ """The first line's text: the organisation and the phase."""
305
+ phase = self._phase if self._stopped is None else "done"
306
+ return f"{self._org} · {phase}"
307
+
308
+ def _traffic(self) -> str:
309
+ """The second line: the request counts by kind."""
310
+ parts = [self._count("REST", "rest"), self._count("GraphQL", "graphql")]
304
311
  if self._requests.get("external"):
305
312
  parts.append(self._count("external", "external"))
306
313
  return " · ".join(parts)
307
314
 
308
- def line(self) -> str:
309
- """The progress line as plain text: what the spinner follows."""
310
- return f"{self._body()} · {self._clock_text()}"
315
+ def _backoff(self) -> str | None:
316
+ """The rate-limit countdown, while a backoff is in progress."""
317
+ if not self._backoffs:
318
+ return None
319
+ wait = max(0.0, self._resume_at - self._clock())
320
+ where = f" ({self._limited_resource})" if self._limited_resource else ""
321
+ return f"⏳ rate limited by GitHub{where}; resuming in {wait:.0f} s"
322
+
323
+ def _progress(self) -> str:
324
+ """The third line, clock first so truncation takes it last."""
325
+ parts = [self._clock_text()]
326
+ if self._repos_total is not None:
327
+ parts.append(f"{self._repos_done}/{self._repos_total} repos")
328
+ backoff = self._backoff()
329
+ if backoff is not None:
330
+ parts.append(backoff)
331
+ return " · ".join(parts)
311
332
 
312
- def exceptional(self) -> tuple[str, str] | None:
313
- """The second line, as ``(marker, text)``, while something warrants it."""
314
- if self._backoffs:
315
- wait = max(0.0, self._resume_at - self._clock())
316
- where = f" ({self._limited_resource})" if self._limited_resource else ""
317
- return "⏳", f"rate limited by GitHub{where}; resuming in {wait:.0f} s"
333
+ def lines(self) -> list[str]:
334
+ """The display as plain text, one string per row, without the spinner."""
335
+ rows = [self._where(), self._traffic(), self._progress()]
318
336
  if self._notice:
319
- return "📦", self._notice
320
- return None
337
+ rows.append(f"📦 {self._notice}")
338
+ return rows
321
339
 
322
340
  @staticmethod
323
341
  def _cell(text: str, style: str = "") -> Text:
324
342
  return Text(text, style=style, no_wrap=True, overflow="ellipsis")
325
343
 
326
344
  def render(self) -> RenderableType:
327
- """The one or two physical lines, however narrow the terminal.
328
-
329
- Each row is one line: the text in the middle column truncates with an
330
- ellipsis rather than wrapping, since a wrapped line is as unreadable
331
- as a torn one. The clock has a column of its own so it is the last
332
- thing to go, not the first: it is what tells a stalled run from a
333
- slow one once the counts are cut off.
345
+ """Three or four physical lines, however narrow the terminal.
346
+
347
+ Each row is one line: its text truncates with an ellipsis rather than
348
+ wrapping, since a wrapped line is as unreadable as a torn one. The
349
+ clock leads the third row so it is the last thing on it to go: it is
350
+ what tells a stalled run from a slow one. The rows after the first
351
+ leave the spinner's column empty, so their text aligns under the
352
+ first row's.
334
353
  """
335
354
  grid = Table.grid(padding=(0, 1))
336
355
  grid.add_column(no_wrap=True)
337
356
  grid.add_column()
338
- grid.add_column(no_wrap=True)
339
357
  marker: RenderableType = (
340
358
  self._spinner if self._stopped is None else Text("✓", style="green")
341
359
  )
342
- grid.add_row(marker, self._cell(self._body()), f"· {self._clock_text()}")
343
- second = self.exceptional()
344
- if second is not None:
345
- second_marker, text = second
346
- grid.add_row(Text(second_marker), self._cell(text, "yellow"), "")
360
+ where, traffic, progress, *notice = self.lines()
361
+ grid.add_row(marker, self._cell(where))
362
+ grid.add_row("", self._cell(traffic))
363
+ style = "yellow" if self._backoffs else ""
364
+ grid.add_row("", self._cell(progress, style))
365
+ for text in notice:
366
+ grid.add_row("", self._cell(text, "yellow"))
347
367
  return grid
348
368
 
349
369
 
@@ -40,6 +40,8 @@ def _table_to_dict(section: TableSection) -> dict:
40
40
  "fail_count": section.fail_count,
41
41
  "unknown_count": section.unknown_count,
42
42
  "not_applicable_count": section.not_applicable_count,
43
+ # One string per note; a note with a detail line carries it after a
44
+ # newline (the malware coverage note's headline and detail).
43
45
  "notes": list(section.notes),
44
46
  "description": section.resolved_description(),
45
47
  # Aggregate rows beneath the totals, over every row (report.json is the
@@ -26,9 +26,11 @@ _COLUMNS = ("Repository", "Package", "Ecosystem", "Manifest", "Severity", "Age")
26
26
  # Severity and Age sort on numbers (the severity rung, the days), so a
27
27
  # configured sort on either defaults to worst or oldest first.
28
28
  _NUMERIC_COLUMNS = frozenset({4, 5})
29
+ # A headline and its detail, split by a newline: each surface sets the detail
30
+ # on a line of its own under the headline (see the summary renderers).
29
31
  COVERAGE_NOTE = (
30
- "GitHub's API does not expose whether malware alerts are enabled; "
31
- "a repository without findings may have the feature off."
32
+ "Malware alert enablement/status NOT currently available through APIs\n"
33
+ "Repositories without findings may have the feature off so coverage unknown"
32
34
  )
33
35
 
34
36
 
@@ -139,6 +139,8 @@ def _summary_context(
139
139
  "kind": line.kind,
140
140
  "emoji": SUMMARY_EMOJI[line.kind],
141
141
  "text": line.text,
142
+ # A multi-line note: the template breaks between the parts.
143
+ "parts": line.text.split("\n"),
142
144
  "label": line.names_label,
143
145
  "names": names,
144
146
  "names_hidden": hidden,
@@ -185,7 +185,9 @@ def _summary_lines(
185
185
  """
186
186
  out: list[str] = []
187
187
  for line in lines:
188
- out.append(f"{SUMMARY_EMOJI[line.kind]} {line.text}")
188
+ # A multi-line note keeps its detail on a line of its own.
189
+ text = line.text.replace("\n", "<br>\n")
190
+ out.append(f"{SUMMARY_EMOJI[line.kind]} {text}")
189
191
  out.append("")
190
192
  for line in lines:
191
193
  if not line.listed:
@@ -13,6 +13,7 @@ from __future__ import annotations
13
13
  from collections.abc import Callable, Sequence
14
14
  from pathlib import Path
15
15
 
16
+ from rich.cells import cell_len
16
17
  from rich.console import Console
17
18
  from rich.markup import escape
18
19
  from rich.table import Table
@@ -140,7 +141,11 @@ def _render_summary(
140
141
  """
141
142
  for line in lines:
142
143
  style = _SUMMARY_STYLE[line.kind]
143
- console.print(f" [{style}]{SUMMARY_EMOJI[line.kind]} {line.text}[/{style}]")
144
+ emoji = SUMMARY_EMOJI[line.kind]
145
+ # A multi-line note's detail aligns under its headline's text.
146
+ indent = " " * (2 + cell_len(emoji) + 1)
147
+ text = line.text.replace("\n", f"\n{indent}")
148
+ console.print(f" [{style}]{emoji} {text}[/{style}]")
144
149
  for line in lines:
145
150
  if line.listed:
146
151
  style = _SUMMARY_STYLE[line.kind]
@@ -56,7 +56,7 @@
56
56
  {%- macro render_summary(summary) -%}
57
57
  {% if summary %}
58
58
  <ul class="summary">
59
- {% for line in summary %}<li class="kind-{{ line.kind }}"><span aria-hidden="true">{{ line.emoji }}</span> {{ line.text }}</li>{% endfor %}
59
+ {% for line in summary %}<li class="kind-{{ line.kind }}"><span aria-hidden="true">{{ line.emoji }}</span> {% for part in line.parts %}{% if not loop.first %}<br>{% endif %}{{ part }}{% endfor %}</li>{% endfor %}
60
60
  {% for line in summary %}{% if line.label and line.names %}<li class="names"><strong>{{ line.label }}:</strong> {% for r in line.names %}{% if r.url %}<a href="{{ r.url }}">{{ r.name }}</a>{% else %}{{ r.name }}{% endif %}{% if not loop.last %}, {% endif %}{% endfor %}{% if line.names_hidden %} … (+{{ line.names_hidden }} more){% endif %}</li>{% endif %}{% endfor %}
61
61
  </ul>
62
62
  {% endif %}