repomatic 7.6.0__tar.gz → 7.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. {repomatic-7.6.0 → repomatic-7.8.0}/PKG-INFO +8 -6
  2. {repomatic-7.6.0 → repomatic-7.8.0}/pyproject.toml +11 -6
  3. {repomatic-7.6.0 → repomatic-7.8.0}/pyproject.toml.orig +37 -8
  4. {repomatic-7.6.0 → repomatic-7.8.0}/readme.md +6 -4
  5. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/__init__.py +2 -2
  6. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/__main__.py +9 -8
  7. repomatic-7.8.0/repomatic/attestation.py +189 -0
  8. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/awesome_toc.py +3 -3
  9. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/binaries_page.py +36 -68
  10. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/binary.py +119 -7
  11. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/broken_links.py +34 -14
  12. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/cache.py +51 -7
  13. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/changelog.py +424 -153
  14. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/checksums.py +11 -5
  15. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/cli.py +179 -292
  16. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/config.py +6 -3
  17. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/_release-engine.yaml +79 -39
  18. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/autofix.yaml +62 -49
  19. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/cancel-runs.yaml +1 -1
  20. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/changelog.yaml +14 -14
  21. repomatic-7.8.0/repomatic/data/coverage.toml +24 -0
  22. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/debug.yaml +2 -2
  23. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/docs.yaml +6 -4
  24. repomatic-7.8.0/repomatic/data/labeller-content-based.yaml +31 -0
  25. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/labels.yaml +12 -10
  26. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/lint.yaml +18 -14
  27. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/pytest.toml +4 -5
  28. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/release.yaml +15 -5
  29. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/skills/awesome-triage/SKILL.md +10 -10
  30. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/skills/babysit-ci/SKILL.md +14 -4
  31. repomatic-7.8.0/repomatic/data/skills/github-housekeeping/SKILL.md +129 -0
  32. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/skills/repomatic-audit/SKILL.md +5 -5
  33. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/skills/repomatic-ship/SKILL.md +23 -5
  34. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/tests.yaml +19 -59
  35. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/unsubscribe.yaml +1 -1
  36. repomatic-7.8.0/repomatic/data/vt-trend-chart.js +52 -0
  37. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/dep_graph.py +14 -10
  38. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/dep_report.py +139 -64
  39. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/dep_sources.py +45 -57
  40. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/docs.py +28 -16
  41. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/git_ops.py +12 -5
  42. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/github/actions.py +24 -1
  43. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/github/dev_release.py +30 -7
  44. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/github/gh.py +24 -0
  45. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/github/issue.py +14 -5
  46. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/github/matrix.py +35 -6
  47. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/github/release_sync.py +8 -27
  48. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/github/releases.py +17 -24
  49. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/github/sponsor.py +31 -24
  50. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/github/token.py +26 -13
  51. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/github/unsubscribe.py +9 -27
  52. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/github/workflow_sync.py +126 -226
  53. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/http.py +11 -1
  54. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/images.py +68 -60
  55. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/init_project.py +189 -149
  56. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/labels.py +39 -20
  57. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/lint_repo.py +386 -70
  58. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/mailmap.py +24 -0
  59. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/matrix_axes.py +0 -7
  60. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/metadata.py +194 -159
  61. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/npm.py +5 -1
  62. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/plugin.py +16 -6
  63. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/prepare_release.py +101 -81
  64. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/pypi.py +110 -49
  65. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/pyproject.py +40 -1
  66. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/registry.py +84 -47
  67. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/setup_guide.py +19 -8
  68. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/sync_ops.py +205 -15
  69. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/yanked-admonition.md +2 -2
  70. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/tool_registry.py +85 -3
  71. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/tool_runner.py +182 -127
  72. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/tool_runner_page.py +7 -19
  73. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/uv.py +199 -106
  74. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/version_sync.py +74 -1
  75. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/virustotal.py +9 -2
  76. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/vulnerable_deps.py +29 -12
  77. repomatic-7.6.0/repomatic/data/codecov.yaml +0 -6
  78. repomatic-7.6.0/repomatic/data/labeller-content-based.yaml +0 -37
  79. {repomatic-7.6.0 → repomatic-7.8.0}/license +0 -0
  80. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/bundle.py +0 -0
  81. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/compat.py +0 -0
  82. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/__init__.py +0 -0
  83. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/action-publish-pypi.yaml +0 -0
  84. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/agent-grunt-qa.md +0 -0
  85. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/agent-qa-engineer.md +0 -0
  86. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/agent-sphinx-docs.md +0 -0
  87. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/autolock.yaml +0 -0
  88. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/awesome_template/.github/ISSUE_TEMPLATE/config.yml +0 -0
  89. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/awesome_template/.github/ISSUE_TEMPLATE/new-link.yaml +0 -0
  90. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/awesome_template/.github/code-of-conduct.md +0 -0
  91. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/awesome_template/.github/contributing.md +0 -0
  92. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/awesome_template/.github/contributing.zh.md +0 -0
  93. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/awesome_template/.github/funding.yml +0 -0
  94. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/awesome_template/.github/pull_request_template.md +0 -0
  95. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/awesome_template/__init__.py +0 -0
  96. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/awesome_template/license +0 -0
  97. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/bumpversion.toml +0 -0
  98. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/labeller-file-based.yaml +0 -0
  99. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/labels.toml +0 -0
  100. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/lychee.toml +0 -0
  101. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/mdformat.toml +0 -0
  102. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/mypy.toml +0 -0
  103. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/ruff.toml +0 -0
  104. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/skills/av-false-positive/SKILL.md +0 -0
  105. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/skills/benchmark-update/SKILL.md +0 -0
  106. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/skills/brand-assets/SKILL.md +0 -0
  107. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/skills/file-bug-report/SKILL.md +0 -0
  108. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/skills/repomatic-changelog/SKILL.md +0 -0
  109. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/skills/repomatic-deps/SKILL.md +0 -0
  110. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/skills/repomatic-init/SKILL.md +0 -0
  111. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/skills/repomatic-topics/SKILL.md +0 -0
  112. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/skills/sphinx-docs-sync/SKILL.md +0 -0
  113. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/skills/translation-sync/SKILL.md +0 -0
  114. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/skills/upstream-audit/SKILL.md +0 -0
  115. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/typos.toml +0 -0
  116. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/uv.toml +0 -0
  117. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/yamllint.yaml +0 -0
  118. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/zizmor.yaml +0 -0
  119. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/frontmatter.py +0 -0
  120. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/github/__init__.py +0 -0
  121. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/github/pr_body.py +0 -0
  122. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/github/status.py +0 -0
  123. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/gitignore.py +0 -0
  124. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/humanize.py +0 -0
  125. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/py.typed +0 -0
  126. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/__init__.py +0 -0
  127. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/available-admonition.md +0 -0
  128. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/broken-links-issue.md +0 -0
  129. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/bump-version.md +0 -0
  130. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/detect-squash-merge.md +0 -0
  131. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/development-warning.md +0 -0
  132. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/fix-changelog.md +0 -0
  133. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/fix-typos.md +0 -0
  134. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/fix-vulnerable-deps.md +0 -0
  135. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/format-images.md +0 -0
  136. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/format-json.md +0 -0
  137. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/format-markdown.md +0 -0
  138. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/format-pyproject.md +0 -0
  139. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/format-python.md +0 -0
  140. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/format-shell.md +0 -0
  141. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/generated-footer.md +0 -0
  142. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/github-releases.md +0 -0
  143. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/immutable-releases.md +0 -0
  144. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/pr-metadata.md.noformat +0 -0
  145. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/prepare-release.md.noformat +0 -0
  146. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/refresh-tip.md +0 -0
  147. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/release-notes.md +0 -0
  148. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/release-sync-report.md +0 -0
  149. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/setup-guide-branch-ruleset.md +0 -0
  150. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/setup-guide-dependabot.md +0 -0
  151. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/setup-guide-fork-pr-approval.md +0 -0
  152. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/setup-guide-notifications-pat.md +0 -0
  153. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/setup-guide-pages-source.md +0 -0
  154. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/setup-guide-pypi-trusted-publisher.md +0 -0
  155. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/setup-guide-sha-pinning-required.md +0 -0
  156. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/setup-guide-token.md +0 -0
  157. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/setup-guide-verify.md +0 -0
  158. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/setup-guide-virustotal.md +0 -0
  159. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/setup-guide.md +0 -0
  160. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/sync-action-pins.md +0 -0
  161. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/sync-bumpversion.md +0 -0
  162. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/sync-dep-sources.md +0 -0
  163. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/sync-gitignore.md +0 -0
  164. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/sync-mailmap.md +0 -0
  165. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/sync-repomatic.md +0 -0
  166. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/sync-tool-versions.md +0 -0
  167. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/sync-uv-lock.md +0 -0
  168. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/sync-workflow-pins.md +0 -0
  169. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/unavailable-admonition.md +0 -0
  170. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/unsubscribe-phase1.md +0 -0
  171. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/unsubscribe-phase2.md +0 -0
  172. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/update-dep-graph.md +0 -0
  173. {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/update-docs.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: repomatic
3
- Version: 7.6.0
3
+ Version: 7.8.0
4
4
  Summary: 🏭 Automate repository maintenance, releases, and CI/CD workflows
5
5
  Keywords: changelog-formatter,dependabot-alternative,github-actions,github-labels,github-workflow,gitignore,mailmap,mypy,nuitka,python-automation,release-automation,repository-automation,repository-management,reusable-workflows,software-factory,sphinx-doc,typo,uv,workflow-reusable
6
6
  Author: Kevin Deldycke
@@ -59,7 +59,7 @@ Requires-Dist: wcmatch>=10
59
59
  Requires-Python: >=3.10
60
60
  Project-URL: Changelog, https://github.com/kdeldycke/repomatic/blob/main/changelog.md
61
61
  Project-URL: Documentation, https://kdeldycke.github.io/repomatic
62
- Project-URL: Download, https://github.com/kdeldycke/repomatic/releases/tag/v7.6.0
62
+ Project-URL: Download, https://github.com/kdeldycke/repomatic/releases/tag/v7.8.0
63
63
  Project-URL: Funding, https://github.com/sponsors/kdeldycke
64
64
  Project-URL: Homepage, https://github.com/kdeldycke/repomatic
65
65
  Project-URL: Issues, https://github.com/kdeldycke/repomatic/issues
@@ -67,14 +67,13 @@ Project-URL: Repository, https://github.com/kdeldycke/repomatic
67
67
  Description-Content-Type: text/markdown
68
68
 
69
69
  <p align="center">
70
- <img src="docs/assets/logo-banner.svg" alt="repomatic">
70
+ <img src="https://raw.githubusercontent.com/kdeldycke/repomatic/main/docs/assets/logo-banner.svg" alt="repomatic">
71
71
  </p>
72
72
 
73
73
  [![Last release](https://img.shields.io/pypi/v/repomatic.svg)](https://pypi.org/project/repomatic/)
74
74
  [![Python versions](https://img.shields.io/pypi/pyversions/repomatic.svg)](https://pypi.org/project/repomatic/)
75
75
  [![Downloads](https://static.pepy.tech/badge/repomatic/month)](https://pepy.tech/projects/repomatic)
76
76
  [![Unittests status](https://img.shields.io/github/actions/workflow/status/kdeldycke/repomatic/tests.yaml?branch=main&label=%F0%9F%94%AC%20Tests)](https://github.com/kdeldycke/repomatic/actions/workflows/tests.yaml?query=branch%3Amain)
77
- [![Coverage status](https://codecov.io/gh/kdeldycke/repomatic/branch/main/graph/badge.svg)](https://app.codecov.io/gh/kdeldycke/repomatic)
78
77
  [![Documentation status](https://img.shields.io/github/actions/workflow/status/kdeldycke/repomatic/docs.yaml?branch=main&label=%F0%9F%93%9A%20Docs)](https://github.com/kdeldycke/repomatic/actions/workflows/docs.yaml?query=branch%3Amain)
79
78
 
80
79
  A Python CLI and `pyproject.toml` configuration that let you **release Python packages multiple times a day with only 2-clicks**. Designed for `uv`-based Python projects, but usable for other projects too. Every step is a CLI command: reusable GitHub Actions workflows are only the trigger, so the same automation runs on your machine.
@@ -160,8 +159,11 @@ Check these projects to get real-life examples of usage and inspiration:
160
159
  - ![GitHub stars](https://img.shields.io/github/stars/kdeldycke/meta-package-manager?label=%E2%AD%90&style=flat-square) [Meta Package Manager](https://github.com/kdeldycke/meta-package-manager) - A unifying CLI for multiple package managers.
161
160
  - ![GitHub stars](https://img.shields.io/github/stars/kdeldycke/mail-deduplicate?label=%E2%AD%90&style=flat-square) [Mail Deduplicate](https://github.com/kdeldycke/mail-deduplicate) - A CLI to deduplicate similar emails.
162
161
  - ![GitHub stars](https://img.shields.io/github/stars/kdeldycke/dotfiles?label=%E2%AD%90&style=flat-square) [dotfiles](https://github.com/kdeldycke/dotfiles) - macOS dotfiles for Python developers.
163
- - ![GitHub stars](https://img.shields.io/github/stars/kdeldycke/click-extra?label=%E2%AD%90&style=flat-square) [Click Extra](https://github.com/kdeldycke/click-extra) - Extra colorization and configuration loading for Click.
162
+ - ![GitHub stars](https://img.shields.io/github/stars/kdeldycke/click-extra?label=%E2%AD%90&style=flat-square) [Click Extra](https://github.com/kdeldycke/click-extra) - Drop-in replacement for Click to make user-friendly and colorful CLI.
164
163
  - ![GitHub stars](https://img.shields.io/github/stars/kdeldycke/repomatic?label=%E2%AD%90&style=flat-square) [repomatic](https://github.com/kdeldycke/repomatic) - Itself. Eat your own dog-food.
165
- - ![GitHub stars](https://img.shields.io/github/stars/kdeldycke/extra-platforms?label=%E2%AD%90&style=flat-square) [Extra Platforms](https://github.com/kdeldycke/extra-platforms) - Detect platforms and group them by family.
164
+ - ![GitHub stars](https://img.shields.io/github/stars/kdeldycke/plumage?label=%E2%AD%90&style=flat-square) [Plumage](https://github.com/kdeldycke/plumage) - Clean and tidy theme for Pelican, the static site generator.
165
+ - ![GitHub stars](https://img.shields.io/github/stars/kdeldycke/kevin-deldycke-blog?label=%E2%AD%90&style=flat-square) [Kevin Deldycke's blog](https://github.com/kdeldycke/kevin-deldycke-blog) - My personal blog, based on Pelican.
166
+ - ![GitHub stars](https://img.shields.io/github/stars/kdeldycke/extra-platforms?label=%E2%AD%90&style=flat-square) [Extra Platforms](https://github.com/kdeldycke/extra-platforms) - Detect architectures, platforms, shells, terminals, CI systems and agents, grouped by family.
167
+ - ![GitHub stars](https://img.shields.io/github/stars/kdeldycke/kdeldycke?label=%E2%AD%90&style=flat-square) [GitHub profile](https://github.com/kdeldycke/kdeldycke) - My GitHub profile page and short bio.
166
168
 
167
169
  Send a PR to add your project if you use repomatic.
@@ -4,7 +4,7 @@ requires = ["uv-build>=0.8"]
4
4
 
5
5
  [project]
6
6
  name = "repomatic"
7
- version = "7.6.0"
7
+ version = "7.8.0"
8
8
  description = "🏭 Automate repository maintenance, releases, and CI/CD workflows"
9
9
  readme = "readme.md"
10
10
  keywords = [
@@ -92,7 +92,7 @@ email = "kevin@deldycke.com"
92
92
  [project.urls]
93
93
  Changelog = "https://github.com/kdeldycke/repomatic/blob/main/changelog.md"
94
94
  Documentation = "https://kdeldycke.github.io/repomatic"
95
- Download = "https://github.com/kdeldycke/repomatic/releases/tag/v7.6.0"
95
+ Download = "https://github.com/kdeldycke/repomatic/releases/tag/v7.8.0"
96
96
  Funding = "https://github.com/sponsors/kdeldycke"
97
97
  Homepage = "https://github.com/kdeldycke/repomatic"
98
98
  Issues = "https://github.com/kdeldycke/repomatic/issues"
@@ -146,7 +146,7 @@ module-root = ""
146
146
  [tool.nuitka]
147
147
  copyright = "Kevin Deldycke <kevin@deldycke.com> and contributors. Distributed under GPL-2.0-or-later license."
148
148
  file-description = "🏭 Automate repository maintenance, releases, and CI/CD workflows"
149
- file-version = "7.6.0"
149
+ file-version = "7.8.0"
150
150
  include-data-dir = [
151
151
  "repomatic/data/awesome_template=repomatic/data/awesome_template",
152
152
  "repomatic/data/skills=repomatic/data/skills",
@@ -156,7 +156,7 @@ include-package-data = ["click_extra"]
156
156
  linux-icon = "docs/assets/icon.png"
157
157
  macos-app-icon = "docs/assets/icon.icns"
158
158
  product-name = "Repomatic"
159
- product-version = "7.6.0"
159
+ product-version = "7.8.0"
160
160
  windows-icon-from-ico = "docs/assets/icon.ico"
161
161
 
162
162
  [tool.ruff]
@@ -208,6 +208,7 @@ astroid = "astroid"
208
208
  Contribuer = "Contribuer"
209
209
  oject = "oject"
210
210
  PNGs = "PNGs"
211
+ quater = "quater"
211
212
  sur = "sur"
212
213
 
213
214
  [tool.mypy]
@@ -245,10 +246,11 @@ branch = true
245
246
  source = ["repomatic"]
246
247
 
247
248
  [tool.coverage.report]
249
+ fail_under = 80
248
250
  precision = 2
249
251
 
250
252
  [tool.bumpversion]
251
- current_version = "7.6.0"
253
+ current_version = "7.8.0"
252
254
  parse = '(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(\.dev(?P<dev>\d+))?'
253
255
  serialize = [
254
256
  "{major}.{minor}.{patch}.dev{dev}",
@@ -333,10 +335,13 @@ pypi-package-history = [
333
335
  "gha-utils",
334
336
  "repokit",
335
337
  ]
336
- release-assets = ["repomatic-plugin.zip"]
338
+ release-assets = ["repomatic-claude-plugin.zip"]
337
339
 
338
340
  [tool.repomatic.changelog]
339
341
  archive-location = "./docs/changelog-archive.md"
340
342
 
341
343
  [tool.repomatic.manpages]
342
344
  script = "repomatic.cli:repomatic"
345
+
346
+ [tool.actionlint.paths."**/workflows/*"]
347
+ ignore = ['property "job_workflow_sha" is not defined']
@@ -12,7 +12,7 @@ requires = [ "uv-build>=0.8" ]
12
12
  [project]
13
13
  # Docs: https://packaging.python.org/en/latest/guides/writing-pyproject-toml/
14
14
  name = "repomatic"
15
- version = "7.6.0"
15
+ version = "7.8.0"
16
16
  description = "🏭 Automate repository maintenance, releases, and CI/CD workflows"
17
17
  readme = "readme.md"
18
18
  keywords = [
@@ -145,7 +145,7 @@ dependencies = [
145
145
  ]
146
146
  urls.Changelog = "https://github.com/kdeldycke/repomatic/blob/main/changelog.md"
147
147
  urls.Documentation = "https://kdeldycke.github.io/repomatic"
148
- urls.Download = "https://github.com/kdeldycke/repomatic/releases/tag/v7.6.0"
148
+ urls.Download = "https://github.com/kdeldycke/repomatic/releases/tag/v7.8.0"
149
149
  urls.Funding = "https://github.com/sponsors/kdeldycke"
150
150
  urls.Homepage = "https://github.com/kdeldycke/repomatic"
151
151
  urls.Issues = "https://github.com/kdeldycke/repomatic/issues"
@@ -250,7 +250,7 @@ copyright = "Kevin Deldycke <kevin@deldycke.com> and contributors. Distributed u
250
250
  file-description = "🏭 Automate repository maintenance, releases, and CI/CD workflows"
251
251
  # Numeric only: Nuitka rejects PEP 440 .devN suffixes. Kept in sync with the
252
252
  # package version by the dedicated [tool.bumpversion] rule that strips the suffix.
253
- file-version = "7.6.0"
253
+ file-version = "7.8.0"
254
254
  include-data-dir = [
255
255
  "repomatic/data/awesome_template=repomatic/data/awesome_template",
256
256
  "repomatic/data/skills=repomatic/data/skills",
@@ -269,7 +269,7 @@ include-package-data = [ "click_extra" ]
269
269
  linux-icon = "docs/assets/icon.png"
270
270
  macos-app-icon = "docs/assets/icon.icns"
271
271
  product-name = "Repomatic"
272
- product-version = "7.6.0"
272
+ product-version = "7.8.0"
273
273
  windows-icon-from-ico = "docs/assets/icon.ico"
274
274
 
275
275
  [tool.ruff]
@@ -310,8 +310,11 @@ output-prefer-rule-codes = true
310
310
  [tool.typos]
311
311
  default.extend-identifiers = { Github = "GitHub", IOS = "iOS", Javascript = "JavaScript", MacOS = "macOS", PNGs = "PNGs", PyPi = "PyPI", Typescript = "TypeScript" }
312
312
  # False positives. "Contribuer" is the French for "Contributing", part of the
313
- # translated-readme fixture in tests/test_awesome_toc.py.
314
- default.extend-words = { astroid = "astroid", Contribuer = "Contribuer", oject = "oject", PNGs = "PNGs", sur = "sur" }
313
+ # translated-readme fixture in tests/test_awesome_toc.py. "quater" is the fourth
314
+ # term of the Latin ordinal series numbering the repo-lint checks (bis, ter,
315
+ # quater), not a misspelling of "quarter": the unattended fix-typos job rewrote
316
+ # it once already and had to be reverted by hand.
317
+ default.extend-words = { astroid = "astroid", Contribuer = "Contribuer", oject = "oject", PNGs = "PNGs", quater = "quater", sur = "sur" }
315
318
  # Skip content between <!-- typos:off --> and <!-- typos:on --> markers.
316
319
  default.extend-ignore-re = [
317
320
  "(?s)<!-- typos:off -->.*?<!-- typos:on -->",
@@ -392,10 +395,19 @@ xfail_strict = true
392
395
  # https://coverage.readthedocs.io/en/latest/config.html
393
396
  run.branch = true
394
397
  run.source = [ "repomatic" ]
398
+ # Coverage ratchet: the suite fails below this. The matrix floor sits near
399
+ # 81.6%, since the only cell-dependent tests (tests/test_sphinx_crossrefs.py)
400
+ # run on Linux + Python 3.14 alone and push that cell *up*; the package holds
401
+ # four platform-conditional lines total, so the OS spread is noise. ~1.6 points
402
+ # of margin absorbs that without waving a real regression through.
403
+ #
404
+ # The `once-tests` workflow job overrides this to 0, covering ~22% on its own
405
+ # by design. A focused local run trips it too, so pair those with `--no-cov`.
406
+ report.fail_under = 80
395
407
  report.precision = 2
396
408
 
397
409
  [tool.bumpversion]
398
- current_version = "7.6.0"
410
+ current_version = "7.8.0"
399
411
  # Parse versions with an optional .devN suffix (PEP 440).
400
412
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(\\.dev(?P<dev>\\d+))?"
401
413
  serialize = [
@@ -522,4 +534,21 @@ pypi-package-history = [ "gha-utils", "repokit" ]
522
534
  # attached to every GitHub release by the engine's extra-assets job. The
523
535
  # marketplace at .claude-plugin/marketplace.json installs the plugin straight
524
536
  # from this asset, so a release missing it breaks `/plugin install`.
525
- release-assets = [ "repomatic-plugin.zip" ]
537
+ release-assets = [ "repomatic-claude-plugin.zip" ]
538
+
539
+ [tool.actionlint]
540
+ # actionlint reads no pyproject section of its own: repomatic serializes this one
541
+ # to a temporary YAML config and passes it via `--config-file`. `paths` keys are
542
+ # globs relative to the repository root, and each `ignore` entry is a regular
543
+ # expression matched against the error message.
544
+ #
545
+ # `github.job_workflow_sha` is a real context property, holding the commit SHA of
546
+ # the reusable workflow file itself, which the tool-cache keys read so they do not
547
+ # rotate on every caller commit. GitHub documents it only as an OIDC token claim,
548
+ # never on the `github` context reference page, so actionlint's schema lacks it.
549
+ # Drop this entry once rhysd/actionlint#671 ships in a release.
550
+ #
551
+ # The glob carries no dot on purpose: click-extra expands every dotted key of
552
+ # `pyproject.toml`, and one starting with a dot makes it warn on each repomatic
553
+ # invocation. `**` matches dot-directories, so `.github/workflows/` is covered.
554
+ paths."**/workflows/*".ignore = [ 'property "job_workflow_sha" is not defined' ]
@@ -1,12 +1,11 @@
1
1
  <p align="center">
2
- <img src="docs/assets/logo-banner.svg" alt="repomatic">
2
+ <img src="https://raw.githubusercontent.com/kdeldycke/repomatic/main/docs/assets/logo-banner.svg" alt="repomatic">
3
3
  </p>
4
4
 
5
5
  [![Last release](https://img.shields.io/pypi/v/repomatic.svg)](https://pypi.org/project/repomatic/)
6
6
  [![Python versions](https://img.shields.io/pypi/pyversions/repomatic.svg)](https://pypi.org/project/repomatic/)
7
7
  [![Downloads](https://static.pepy.tech/badge/repomatic/month)](https://pepy.tech/projects/repomatic)
8
8
  [![Unittests status](https://img.shields.io/github/actions/workflow/status/kdeldycke/repomatic/tests.yaml?branch=main&label=%F0%9F%94%AC%20Tests)](https://github.com/kdeldycke/repomatic/actions/workflows/tests.yaml?query=branch%3Amain)
9
- [![Coverage status](https://codecov.io/gh/kdeldycke/repomatic/branch/main/graph/badge.svg)](https://app.codecov.io/gh/kdeldycke/repomatic)
10
9
  [![Documentation status](https://img.shields.io/github/actions/workflow/status/kdeldycke/repomatic/docs.yaml?branch=main&label=%F0%9F%93%9A%20Docs)](https://github.com/kdeldycke/repomatic/actions/workflows/docs.yaml?query=branch%3Amain)
11
10
 
12
11
  A Python CLI and `pyproject.toml` configuration that let you **release Python packages multiple times a day with only 2-clicks**. Designed for `uv`-based Python projects, but usable for other projects too. Every step is a CLI command: reusable GitHub Actions workflows are only the trigger, so the same automation runs on your machine.
@@ -92,8 +91,11 @@ Check these projects to get real-life examples of usage and inspiration:
92
91
  - ![GitHub stars](https://img.shields.io/github/stars/kdeldycke/meta-package-manager?label=%E2%AD%90&style=flat-square) [Meta Package Manager](https://github.com/kdeldycke/meta-package-manager) - A unifying CLI for multiple package managers.
93
92
  - ![GitHub stars](https://img.shields.io/github/stars/kdeldycke/mail-deduplicate?label=%E2%AD%90&style=flat-square) [Mail Deduplicate](https://github.com/kdeldycke/mail-deduplicate) - A CLI to deduplicate similar emails.
94
93
  - ![GitHub stars](https://img.shields.io/github/stars/kdeldycke/dotfiles?label=%E2%AD%90&style=flat-square) [dotfiles](https://github.com/kdeldycke/dotfiles) - macOS dotfiles for Python developers.
95
- - ![GitHub stars](https://img.shields.io/github/stars/kdeldycke/click-extra?label=%E2%AD%90&style=flat-square) [Click Extra](https://github.com/kdeldycke/click-extra) - Extra colorization and configuration loading for Click.
94
+ - ![GitHub stars](https://img.shields.io/github/stars/kdeldycke/click-extra?label=%E2%AD%90&style=flat-square) [Click Extra](https://github.com/kdeldycke/click-extra) - Drop-in replacement for Click to make user-friendly and colorful CLI.
96
95
  - ![GitHub stars](https://img.shields.io/github/stars/kdeldycke/repomatic?label=%E2%AD%90&style=flat-square) [repomatic](https://github.com/kdeldycke/repomatic) - Itself. Eat your own dog-food.
97
- - ![GitHub stars](https://img.shields.io/github/stars/kdeldycke/extra-platforms?label=%E2%AD%90&style=flat-square) [Extra Platforms](https://github.com/kdeldycke/extra-platforms) - Detect platforms and group them by family.
96
+ - ![GitHub stars](https://img.shields.io/github/stars/kdeldycke/plumage?label=%E2%AD%90&style=flat-square) [Plumage](https://github.com/kdeldycke/plumage) - Clean and tidy theme for Pelican, the static site generator.
97
+ - ![GitHub stars](https://img.shields.io/github/stars/kdeldycke/kevin-deldycke-blog?label=%E2%AD%90&style=flat-square) [Kevin Deldycke's blog](https://github.com/kdeldycke/kevin-deldycke-blog) - My personal blog, based on Pelican.
98
+ - ![GitHub stars](https://img.shields.io/github/stars/kdeldycke/extra-platforms?label=%E2%AD%90&style=flat-square) [Extra Platforms](https://github.com/kdeldycke/extra-platforms) - Detect architectures, platforms, shells, terminals, CI systems and agents, grouped by family.
99
+ - ![GitHub stars](https://img.shields.io/github/stars/kdeldycke/kdeldycke?label=%E2%AD%90&style=flat-square) [GitHub profile](https://github.com/kdeldycke/kdeldycke) - My GitHub profile page and short bio.
98
100
 
99
101
  Send a PR to add your project if you use repomatic.
@@ -17,5 +17,5 @@
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
- __version__ = "7.6.0"
21
- __git_tag_sha__ = "07e3ef8ae5685c7dfc4af43001b6fc67501396b1"
20
+ __version__ = "7.8.0"
21
+ __git_tag_sha__ = "ab43527b67361553b1c8539e8f3d9e6e2013b98c"
@@ -35,21 +35,22 @@ from repomatic.cli import repomatic
35
35
 
36
36
 
37
37
  def main():
38
- """Execute the CLI but force its name to not let Click defaults to:
38
+ """Execute the CLI under its canonical name, whatever the entry point.
39
+
40
+ Without `prog_name`, Click derives the displayed name from the invocation:
39
41
 
40
42
  ```{code-block} shell-session
41
43
  $ python -m repomatic --version
42
44
  python -m repomatic, version 4.0.0
43
45
  ```
44
46
 
45
- Indirection via this `main()` method was [required to reconcile](https://github.com/python-poetry/poetry/issues/5981):
46
-
47
- - plain inline package call: `python -m repomatic`,
48
- - Poetry's script entry point: `repomatic = 'repomatic.__main__:main`,
49
- - Nuitka's main module invocation requirement:
50
- `python -m nuitka (...) repomatic/__main__.py`
47
+ The `main()` indirection lets three invocation styles share one entry
48
+ point rendering the same name:
51
49
 
52
- That way we can deduce all three cases from the entry point.
50
+ - the plain module call: `python -m repomatic`,
51
+ - the `[project.scripts]` console script: `repomatic = "repomatic.__main__:main"`,
52
+ - Nuitka's main-module compilation requirement:
53
+ `python -m nuitka (...) repomatic/__main__.py`.
53
54
  """
54
55
  repomatic(prog_name=repomatic.name)
55
56
 
@@ -0,0 +1,189 @@
1
+ # Copyright Kevin Deldycke <kevin@deldycke.com> and contributors.
2
+ #
3
+ # This program is Free Software; you can redistribute it and/or
4
+ # modify it under the terms of the GNU General Public License
5
+ # as published by the Free Software Foundation; either version 2
6
+ # of the License, or (at your option) any later version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public License
14
+ # along with this program; if not, write to the Free Software
15
+ # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
16
+
17
+ """Naming and packing of the sigstore bundles attached to a release.
18
+
19
+ `actions/attest` writes every bundle to the same `attestation.json` basename,
20
+ whatever it signed, so each release job has to rename its own before the files
21
+ land in one directory. Three of them did, three different ways: the compiled
22
+ binaries appended the suffix to the full filename, the man-page tarball dropped
23
+ its `.tar.gz` first, and the consumer-declared extra assets were named after the
24
+ *job* rather than any file. A release page therefore carried
25
+ `repomatic-manpages.attestation.json` next to `repomatic-manpages.tar.gz`, and
26
+ `repomatic-extra-assets.attestation.json` next to `repomatic-claude-plugin.zip`.
27
+
28
+ This module holds the one rule instead: a bundle is named after the artifact it
29
+ attests. The subject list is read back out of the bundle rather than passed in,
30
+ so the name is derived from what was actually signed and no caller can spell it
31
+ differently. See {func}`bundle_filename` for the multi-subject case.
32
+
33
+ ```{note}
34
+ The signing itself stays in `actions/attest`: it needs the job's OIDC token, so
35
+ it cannot move here. This module runs immediately after it, in the same job.
36
+ ```
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ import base64
42
+ import binascii
43
+ import json
44
+ import shutil
45
+ from pathlib import Path
46
+
47
+ TYPE_CHECKING = False
48
+ if TYPE_CHECKING:
49
+ from collections.abc import Sequence
50
+ from typing import Final
51
+
52
+ ATTESTATION_SUFFIX: Final[str] = ".attestation.json"
53
+ """Extension carried by every attestation bundle attached to a release.
54
+
55
+ Not `.sigstore.json` (the ecosystem's own convention) because these files have
56
+ been published under this name since the first attested release, and a release
57
+ asset name is part of the surface users script against.
58
+ """
59
+
60
+
61
+ def bundle_subjects(bundle_path: Path) -> tuple[str, ...]:
62
+ """Filenames of the artifacts a sigstore bundle attests.
63
+
64
+ A bundle wraps a DSSE envelope whose base64 payload is an in-toto
65
+ Statement, and that statement's `subject` array names every file signed in
66
+ the same call. One entry for a single `subject-path`, several when
67
+ `actions/attest` was handed a glob: "If multiple subjects are being attested
68
+ at the same time, a single attestation will be created with references to
69
+ each of the supplied subjects."
70
+
71
+ :param bundle_path: The bundle `actions/attest` wrote.
72
+ :return: Subject filenames, in the order the statement lists them.
73
+ :raises ValueError: If the file is not a bundle carrying a readable
74
+ in-toto statement, or names a subject that is not a bare filename.
75
+ """
76
+ try:
77
+ bundle = json.loads(bundle_path.read_text(encoding="UTF-8"))
78
+ except json.JSONDecodeError as exc:
79
+ raise ValueError(f"{bundle_path} is not valid JSON: {exc}") from exc
80
+
81
+ payload = (bundle.get("dsseEnvelope") or {}).get("payload")
82
+ if not payload:
83
+ raise ValueError(
84
+ f"{bundle_path} carries no DSSE envelope payload: not an "
85
+ "attestation bundle."
86
+ )
87
+ # A payload that is not a base64 string lands in the same basket as one
88
+ # that is unreadable, so callers only ever catch ValueError here.
89
+ try:
90
+ statement = json.loads(base64.b64decode(payload, validate=True))
91
+ except (
92
+ TypeError,
93
+ binascii.Error,
94
+ UnicodeDecodeError,
95
+ json.JSONDecodeError,
96
+ ) as exc:
97
+ raise ValueError(f"{bundle_path} has an unreadable payload: {exc}") from exc
98
+
99
+ subjects = statement.get("subject")
100
+ if not isinstance(subjects, list) or not subjects:
101
+ raise ValueError(f"{bundle_path} attests no subject.")
102
+
103
+ names = []
104
+ for subject in subjects:
105
+ name = subject.get("name") if isinstance(subject, dict) else None
106
+ if not isinstance(name, str) or not name:
107
+ raise ValueError(f"{bundle_path} has a subject with no name.")
108
+ # A subject name is a bare filename everywhere the release lane
109
+ # produces one. Reject a path so the join in pack_attestation below
110
+ # cannot be walked out of the asset directory.
111
+ if Path(name).name != name:
112
+ raise ValueError(
113
+ f"{bundle_path} names subject {name!r}, which is not a bare filename."
114
+ )
115
+ names.append(name)
116
+ return tuple(names)
117
+
118
+
119
+ def bundle_filename(subjects: Sequence[str], set_name: str | None = None) -> str:
120
+ """Name a bundle after the artifact it attests.
121
+
122
+ A single subject gives the bundle its own name, suffix appended to the
123
+ whole filename so the two sort together on a release page listing assets
124
+ alphabetically (`papaya.tar.gz`, then `papaya.tar.gz.attestation.json`).
125
+
126
+ Several subjects have no such name to borrow, since one bundle covers them
127
+ all, so *set_name* is required and should describe the set rather than any
128
+ member of it. That case only arises when a job hands `actions/attest` a
129
+ glob.
130
+
131
+ :param subjects: Subject filenames, from {func}`bundle_subjects`.
132
+ :param set_name: Stem to use when *subjects* holds more than one entry.
133
+ :return: The bundle's filename.
134
+ :raises ValueError: If *subjects* is empty, or holds several entries with
135
+ no *set_name* to fall back on.
136
+ """
137
+ if not subjects:
138
+ raise ValueError("Cannot name a bundle that attests no subject.")
139
+ if len(subjects) == 1:
140
+ return f"{subjects[0]}{ATTESTATION_SUFFIX}"
141
+ if not set_name:
142
+ raise ValueError(
143
+ f"Bundle attests {len(subjects)} subjects "
144
+ f"({', '.join(sorted(subjects))}), so it cannot be named after any "
145
+ "one of them: pass --name to name the set."
146
+ )
147
+ return f"{set_name}{ATTESTATION_SUFFIX}"
148
+
149
+
150
+ def pack_attestation(
151
+ bundle_path: Path,
152
+ asset_dir: Path,
153
+ set_name: str | None = None,
154
+ ) -> list[Path]:
155
+ """Name a bundle after its subject and print what to upload with it.
156
+
157
+ Copies *bundle_path* into *asset_dir* under the name
158
+ {func}`bundle_filename` derives, then returns that bundle alongside every
159
+ artifact it attests, which is exactly the file list the release upload step
160
+ has to attach for the provenance to be verifiable offline.
161
+
162
+ Every subject must already sit in *asset_dir*: a bundle naming a file that
163
+ is not there means the job attested a different tree than the one it is
164
+ about to upload, which would publish an asset whose sidecar covers
165
+ something else. Immutable releases make that unfixable after the fact, so
166
+ it fails here instead.
167
+
168
+ Idempotent: re-running copies the same bytes over the same name.
169
+
170
+ :param bundle_path: The bundle `actions/attest` wrote.
171
+ :param asset_dir: Directory holding the attested artifacts.
172
+ :param set_name: Stem for the multi-subject case, see
173
+ {func}`bundle_filename`.
174
+ :return: Sorted paths to upload, the renamed bundle included.
175
+ :raises ValueError: If a subject is missing from *asset_dir*.
176
+ """
177
+ subjects = bundle_subjects(bundle_path)
178
+ assets = [asset_dir / name for name in subjects]
179
+
180
+ missing = [path.name for path in assets if not path.is_file()]
181
+ if missing:
182
+ raise ValueError(
183
+ f"{bundle_path} attests {', '.join(sorted(missing))}, missing from "
184
+ f"{asset_dir}."
185
+ )
186
+
187
+ target = asset_dir / bundle_filename(subjects, set_name)
188
+ shutil.copy2(bundle_path, target)
189
+ return sorted({*assets, target})
@@ -235,7 +235,7 @@ def fix_awesome_toc(root: Path | None = None) -> dict[Path, list[str]]:
235
235
  reference = root / REFERENCE_README
236
236
  reference_headings: list[str] | None = None
237
237
  if reference.is_file():
238
- reference_headings = headings(reference.read_text(encoding="utf-8"))
238
+ reference_headings = headings(reference.read_text(encoding="UTF-8"))
239
239
  else:
240
240
  logging.warning(
241
241
  f"No {REFERENCE_README} in {root}: translations can only be matched "
@@ -244,13 +244,13 @@ def fix_awesome_toc(root: Path | None = None) -> dict[Path, list[str]]:
244
244
 
245
245
  report: dict[Path, list[str]] = {}
246
246
  for readme in _readmes(root):
247
- content = readme.read_text(encoding="utf-8")
247
+ content = readme.read_text(encoding="UTF-8")
248
248
  forbidden = forbidden_headings_for(content, reference_headings)
249
249
  updated, removed = strip_toc_entries(content, forbidden)
250
250
  if not removed:
251
251
  logging.debug(f"No forbidden ToC entry in {readme}.")
252
252
  continue
253
- readme.write_text(updated, encoding="utf-8")
253
+ readme.write_text(updated, encoding="UTF-8")
254
254
  logging.info(f"Removed {len(removed)} ToC entries from {readme}.")
255
255
  report[readme] = removed
256
256
  return report
@@ -57,7 +57,8 @@ from pathlib import Path
57
57
 
58
58
  from click_extra.blocks import replace_region
59
59
 
60
- from .binary import NUITKA_BUILD_TARGETS
60
+ from .binary import BINARY_ASSET_SUFFIXES, NUITKA_BUILD_TARGETS
61
+ from .bundle import get_data_content
61
62
  from .github.releases import parse_release_version
62
63
  from .virustotal import VIRUSTOTAL_GUI_URL
63
64
 
@@ -68,11 +69,21 @@ if TYPE_CHECKING:
68
69
  from .github.releases import ReleaseAsset, ReleaseWithAssets
69
70
  from .virustotal import ScanRecord
70
71
 
71
- BINARY_ASSET_SUFFIXES = (".bin", ".exe")
72
- """File extensions identifying compiled binaries among release assets.
72
+ CHART_JS_URL = "https://cdn.jsdelivr.net/npm/chart.js@4.5.0/dist/chart.umd.min.js"
73
+ """Pinned CDN artifact drawing the detections trend chart.
73
74
 
74
- Same set the `scan-virustotal` command uploads and the release workflow
75
- downloads (`--pattern` flags in `_release-engine.yaml`).
75
+ The one external artifact this module publishes into every downstream
76
+ repository's docs, so it carries a checksum beside the pin: bump the version
77
+ by hand together with {data}`CHART_JS_SRI`.
78
+ """
79
+
80
+ CHART_JS_SRI = "sha384-XcdcwHqIPULERb2yDEM4R0XaQKU3YnDsrTmjACBZyfdVVqjh6xQ4/DCMd7XLcA6Y"
81
+ """Subresource Integrity digest of {data}`CHART_JS_URL`.
82
+
83
+ The browser refuses the script if the CDN bytes stop matching. Recompute on
84
+ every version bump as the sha384 of the exact artifact, verified against the
85
+ same file inside the npm registry tarball before trusting the CDN copy:
86
+ `hashlib.sha384(artifact_bytes)` then base64.
76
87
  """
77
88
 
78
89
  CSV_HEADERS = (
@@ -186,13 +197,14 @@ def _binary_assets(release: ReleaseWithAssets) -> list[ReleaseAsset]:
186
197
  """Return the release's compiled binaries, sorted by filename.
187
198
 
188
199
  Releases also carry versionless alias copies of each binary, backing the
189
- stable ``releases/latest/download`` URLs (see the upload step in
190
- ``_release-engine.yaml``). An alias shares its digest with its versioned
191
- sibling: collapse each digest group onto its longest name (the versioned
192
- one, since the alias is that same name minus the version segment) so the
193
- catalog lists each binary once. Assets without a recorded digest
194
- (uploaded before GitHub started recording them, mid-2025) predate the
195
- aliases and are kept as-is.
200
+ stable ``releases/latest/download`` URLs (created by
201
+ {func}`~repomatic.binary.pack_binary_assets`, which
202
+ `release-assets` runs in the release engine). An alias shares its digest
203
+ with its versioned sibling: collapse each digest group onto its longest
204
+ name (the versioned one, since {func}`~repomatic.binary.versionless_alias`
205
+ only ever strips the version segment) so the catalog lists each binary
206
+ once. Assets without a recorded digest (uploaded before GitHub started
207
+ recording them, mid-2025) predate the aliases and are kept as-is.
196
208
  """
197
209
  binaries = [a for a in release.assets if a.name.endswith(BINARY_ASSET_SUFFIXES)]
198
210
  digest_groups: dict[str, list[ReleaseAsset]] = {}
@@ -275,6 +287,15 @@ def render_chart_section(records: Sequence[ScanRecord]) -> str:
275
287
  ],
276
288
  sort_keys=True,
277
289
  )
290
+ # The chart script ships as a bundled data file so it reads (and lints) as
291
+ # JavaScript instead of sixty quoted Python strings. Placeholder tokens are
292
+ # substituted with str.replace, never str.format: the script is full of
293
+ # literal braces.
294
+ script = (
295
+ get_data_content("vt-trend-chart.js")
296
+ .replace("__VT_TREND_PAYLOAD__", payload)
297
+ .replace("__VT_DANGER_PCT__", str(FLAGGED_DANGER_PCT))
298
+ )
278
299
  return (
279
300
  "## VirusTotal detections\n\n"
280
301
  "Share of antivirus engine verdicts flagging the binaries of each "
@@ -283,62 +304,9 @@ def render_chart_section(records: Sequence[ScanRecord]) -> str:
283
304
  "there up.\n\n"
284
305
  "```{raw} html\n"
285
306
  '<div style="height: 320px;"><canvas id="vt-trend"></canvas></div>\n'
286
- '<script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.0/dist/'
287
- 'chart.umd.min.js"></script>\n'
288
- "<script>\n"
289
- f"const VT_TREND = {payload};\n"
290
- f"const VT_DANGER_PCT = {FLAGGED_DANGER_PCT};\n"
291
- "const vtCss = getComputedStyle(document.documentElement);\n"
292
- "const vtColor = (name, fallback) =>\n"
293
- " vtCss.getPropertyValue(name).trim() || fallback;\n"
294
- "const vtTint = (p) => {\n"
295
- ' if (p.pct === 0) { return vtColor("--sd-color-success", "#28a745"); }\n'
296
- " return p.pct >= VT_DANGER_PCT\n"
297
- ' ? vtColor("--sd-color-danger", "#dc3545")\n'
298
- ' : vtColor("--sd-color-warning", "#f0b37e");\n'
299
- "};\n"
300
- 'new Chart(document.getElementById("vt-trend"), {\n'
301
- ' type: "line",\n'
302
- " data: {\n"
303
- " datasets: [{\n"
304
- " data: VT_TREND.map((p) => ({x: Date.parse(p.date), y: p.pct})),\n"
305
- ' borderColor: "#88888866",\n'
306
- " pointBackgroundColor: VT_TREND.map(vtTint),\n"
307
- " pointBorderColor: VT_TREND.map(vtTint),\n"
308
- " pointRadius: 4,\n"
309
- " tension: 0.2,\n"
310
- " }],\n"
311
- " },\n"
312
- " options: {\n"
313
- " maintainAspectRatio: false,\n"
314
- " plugins: {\n"
315
- " legend: {display: false},\n"
316
- " tooltip: {callbacks: {\n"
317
- " title: (items) => VT_TREND[items[0].dataIndex].tag,\n"
318
- " label: (item) => {\n"
319
- " const p = VT_TREND[item.dataIndex];\n"
320
- ' return p.flagged + " / " + p.total\n'
321
- ' + " verdicts flagged (" + p.pct + "%)";\n'
322
- " },\n"
323
- " }},\n"
324
- " },\n"
325
- " scales: {\n"
326
- " x: {\n"
327
- ' type: "linear",\n'
328
- " ticks: {\n"
329
- " maxTicksLimit: 8,\n"
330
- " callback: (value) =>\n"
331
- " new Date(value).toISOString().slice(0, 10),\n"
332
- " },\n"
333
- " },\n"
334
- " y: {\n"
335
- " beginAtZero: true,\n"
336
- ' title: {display: true, text: "Flagged verdicts (%)"},\n'
337
- " },\n"
338
- " },\n"
339
- " },\n"
340
- "});\n"
341
- "</script>\n"
307
+ f'<script src="{CHART_JS_URL}" integrity="{CHART_JS_SRI}" '
308
+ 'crossorigin="anonymous"></script>\n'
309
+ f"<script>\n{script}</script>\n"
342
310
  "```"
343
311
  )
344
312