repomatic 7.0.0__tar.gz → 7.2.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 (177) hide show
  1. {repomatic-7.0.0 → repomatic-7.2.0}/PKG-INFO +3 -3
  2. {repomatic-7.0.0 → repomatic-7.2.0}/pyproject.toml +35 -17
  3. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/__init__.py +2 -2
  4. repomatic-7.2.0/repomatic/binaries_page.py +448 -0
  5. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/binary.py +7 -0
  6. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/broken_links.py +1 -1
  7. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/cache.py +103 -91
  8. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/changelog.py +47 -17
  9. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/checksums.py +17 -12
  10. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/cli.py +584 -650
  11. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/config.py +83 -172
  12. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/_release-engine.yaml +60 -31
  13. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/action-publish-pypi.yaml +5 -1
  14. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/agent-sphinx-docs.md +25 -28
  15. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/autofix.yaml +107 -61
  16. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/bumpversion.toml +6 -2
  17. repomatic-7.2.0/repomatic/data/cancel-runs.yaml +50 -0
  18. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/changelog.yaml +24 -18
  19. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/debug.yaml +4 -4
  20. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/docs.yaml +7 -7
  21. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/labels.yaml +12 -12
  22. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/lint.yaml +22 -19
  23. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/lychee.toml +3 -0
  24. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/skill-av-false-positive.md +7 -4
  25. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/skill-babysit-ci.md +20 -4
  26. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/skill-repomatic-ship.md +13 -10
  27. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/tests.yaml +5 -5
  28. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/unsubscribe.yaml +24 -2
  29. repomatic-7.2.0/repomatic/dep_sources.py +377 -0
  30. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/deps_graph.py +338 -349
  31. repomatic-7.2.0/repomatic/docs.py +172 -0
  32. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/git_ops.py +107 -36
  33. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/github/__init__.py +3 -6
  34. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/github/actions.py +47 -0
  35. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/github/dev_release.py +1 -2
  36. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/github/gh.py +100 -12
  37. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/github/issue.py +19 -18
  38. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/github/matrix.py +3 -0
  39. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/github/pr_body.py +226 -48
  40. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/github/release_sync.py +5 -50
  41. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/github/releases.py +188 -94
  42. {repomatic-7.0.0/repomatic → repomatic-7.2.0/repomatic/github}/sponsor.py +11 -49
  43. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/github/token.py +85 -174
  44. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/github/unsubscribe.py +25 -36
  45. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/github/workflow_sync.py +156 -125
  46. repomatic-7.2.0/repomatic/gitignore.py +88 -0
  47. repomatic-7.2.0/repomatic/http.py +80 -0
  48. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/images.py +2 -0
  49. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/init_project.py +10 -11
  50. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/lint_repo.py +31 -17
  51. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/metadata.py +59 -50
  52. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/npm.py +4 -7
  53. repomatic-7.0.0/repomatic/release_prep.py → repomatic-7.2.0/repomatic/prepare_release.py +18 -5
  54. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/pypi.py +22 -16
  55. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/pyproject.py +22 -28
  56. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/registry.py +46 -20
  57. repomatic-7.2.0/repomatic/setup_guide.py +383 -0
  58. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/sync_ops.py +481 -184
  59. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/templates/bump-version.md +2 -3
  60. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/templates/detect-squash-merge.md +4 -3
  61. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/templates/fix-changelog.md +2 -5
  62. repomatic-7.2.0/repomatic/templates/fix-typos.md +8 -0
  63. repomatic-7.2.0/repomatic/templates/fix-vulnerable-deps.md +15 -0
  64. repomatic-7.2.0/repomatic/templates/format-images.md +5 -0
  65. repomatic-7.2.0/repomatic/templates/format-json.md +8 -0
  66. repomatic-7.2.0/repomatic/templates/format-markdown.md +8 -0
  67. repomatic-7.2.0/repomatic/templates/format-pyproject.md +8 -0
  68. repomatic-7.2.0/repomatic/templates/format-python.md +8 -0
  69. repomatic-7.2.0/repomatic/templates/format-shell.md +8 -0
  70. repomatic-7.2.0/repomatic/templates/pr-metadata.md.noformat +15 -0
  71. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/templates/prepare-release.md +2 -3
  72. repomatic-7.2.0/repomatic/templates/setup-guide-notifications-pat.md +21 -0
  73. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/templates/setup-guide-virustotal.md +3 -0
  74. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/templates/setup-guide.md +3 -1
  75. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/templates/sync-action-pins.md +2 -5
  76. repomatic-7.2.0/repomatic/templates/sync-bumpversion.md +11 -0
  77. repomatic-7.2.0/repomatic/templates/sync-dep-sources.md +17 -0
  78. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/templates/sync-gitignore.md +2 -5
  79. repomatic-7.2.0/repomatic/templates/sync-mailmap.md +11 -0
  80. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/templates/sync-repomatic.md +2 -5
  81. repomatic-7.2.0/repomatic/templates/sync-tool-versions.md +15 -0
  82. repomatic-7.2.0/repomatic/templates/sync-uv-lock.md +14 -0
  83. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/templates/sync-workflow-pins.md +2 -5
  84. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/templates/update-deps-graph.md +2 -5
  85. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/templates/update-docs.md +2 -5
  86. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/test_matrix.py +3 -1
  87. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/tool_runner.py +305 -36
  88. repomatic-7.2.0/repomatic/tool_runner_page.py +217 -0
  89. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/uv.py +380 -556
  90. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/version_sync.py +69 -39
  91. repomatic-7.2.0/repomatic/virustotal.py +467 -0
  92. repomatic-7.2.0/repomatic/vulnerable_deps.py +592 -0
  93. repomatic-7.0.0/repomatic/data/cancel-runs.yaml +0 -55
  94. repomatic-7.0.0/repomatic/github/advisories.py +0 -131
  95. repomatic-7.0.0/repomatic/templates/fix-typos.md +0 -11
  96. repomatic-7.0.0/repomatic/templates/fix-vulnerable-deps.md +0 -18
  97. repomatic-7.0.0/repomatic/templates/format-images.md +0 -8
  98. repomatic-7.0.0/repomatic/templates/format-json.md +0 -11
  99. repomatic-7.0.0/repomatic/templates/format-markdown.md +0 -11
  100. repomatic-7.0.0/repomatic/templates/format-pyproject.md +0 -11
  101. repomatic-7.0.0/repomatic/templates/format-python.md +0 -11
  102. repomatic-7.0.0/repomatic/templates/format-shell.md +0 -11
  103. repomatic-7.0.0/repomatic/templates/pr-metadata.md.noformat +0 -17
  104. repomatic-7.0.0/repomatic/templates/sync-bumpversion.md +0 -14
  105. repomatic-7.0.0/repomatic/templates/sync-mailmap.md +0 -14
  106. repomatic-7.0.0/repomatic/templates/sync-tool-versions.md +0 -18
  107. repomatic-7.0.0/repomatic/templates/sync-uv-lock.md +0 -17
  108. repomatic-7.0.0/repomatic/virustotal.py +0 -417
  109. {repomatic-7.0.0 → repomatic-7.2.0}/license +0 -0
  110. {repomatic-7.0.0 → repomatic-7.2.0}/readme.md +0 -0
  111. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/__main__.py +0 -0
  112. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/__init__.py +0 -0
  113. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/agent-grunt-qa.md +0 -0
  114. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/agent-qa-engineer.md +0 -0
  115. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/autolock.yaml +0 -0
  116. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/awesome_template/.github/ISSUE_TEMPLATE/config.yml +0 -0
  117. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/awesome_template/.github/ISSUE_TEMPLATE/new-link.yaml +0 -0
  118. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/awesome_template/.github/code-of-conduct.md +0 -0
  119. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/awesome_template/.github/contributing.md +0 -0
  120. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/awesome_template/.github/contributing.zh.md +0 -0
  121. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/awesome_template/.github/funding.yml +0 -0
  122. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/awesome_template/.github/pull_request_template.md +0 -0
  123. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/awesome_template/__init__.py +0 -0
  124. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/awesome_template/license +0 -0
  125. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/codecov.yaml +0 -0
  126. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/labeller-content-based.yaml +0 -0
  127. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/labeller-file-based.yaml +0 -0
  128. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/labels.toml +0 -0
  129. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/mdformat.toml +0 -0
  130. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/mypy.toml +0 -0
  131. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/pytest.toml +0 -0
  132. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/release.yaml +0 -0
  133. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/ruff.toml +0 -0
  134. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/skill-awesome-triage.md +0 -0
  135. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/skill-benchmark-update.md +0 -0
  136. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/skill-brand-assets.md +0 -0
  137. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/skill-file-bug-report.md +0 -0
  138. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/skill-repomatic-audit.md +0 -0
  139. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/skill-repomatic-changelog.md +0 -0
  140. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/skill-repomatic-deps.md +0 -0
  141. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/skill-repomatic-init.md +0 -0
  142. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/skill-repomatic-topics.md +0 -0
  143. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/skill-sphinx-docs-sync.md +0 -0
  144. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/skill-translation-sync.md +0 -0
  145. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/skill-upstream-audit.md +0 -0
  146. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/typos.toml +0 -0
  147. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/uv.toml +0 -0
  148. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/yamllint.yaml +0 -0
  149. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/data/zizmor.yaml +0 -0
  150. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/github/pr.py +0 -0
  151. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/github/status.py +0 -0
  152. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/mailmap.py +0 -0
  153. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/myst_converter.py +0 -0
  154. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/myst_docstrings.py +0 -0
  155. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/py.typed +0 -0
  156. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/rst_to_myst.py +0 -0
  157. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/templates/__init__.py +0 -0
  158. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/templates/available-admonition.md +0 -0
  159. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/templates/broken-links-issue.md +0 -0
  160. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/templates/development-warning.md +0 -0
  161. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/templates/generated-footer.md +0 -0
  162. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/templates/github-releases.md +0 -0
  163. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/templates/immutable-releases.md +0 -0
  164. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/templates/refresh-tip.md +0 -0
  165. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/templates/release-notes.md +0 -0
  166. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/templates/release-sync-report.md +0 -0
  167. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/templates/setup-guide-branch-ruleset.md +0 -0
  168. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/templates/setup-guide-dependabot.md +0 -0
  169. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/templates/setup-guide-fork-pr-approval.md +0 -0
  170. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/templates/setup-guide-pages-source.md +0 -0
  171. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/templates/setup-guide-pypi-trusted-publisher.md +0 -0
  172. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/templates/setup-guide-token.md +0 -0
  173. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/templates/setup-guide-verify.md +0 -0
  174. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/templates/unavailable-admonition.md +0 -0
  175. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/templates/unsubscribe-phase1.md +0 -0
  176. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/templates/unsubscribe-phase2.md +0 -0
  177. {repomatic-7.0.0 → repomatic-7.2.0}/repomatic/templates/yanked-admonition.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: repomatic
3
- Version: 7.0.0
3
+ Version: 7.2.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
@@ -46,7 +46,7 @@ Classifier: Typing :: Typed
46
46
  Requires-Dist: arrow>=1.3
47
47
  Requires-Dist: backports-strenum>=1 ; python_full_version < '3.11'
48
48
  Requires-Dist: boltons>=25
49
- Requires-Dist: click-extra>=8.1
49
+ Requires-Dist: click-extra>=8.3
50
50
  Requires-Dist: extra-platforms>=12.0.2
51
51
  Requires-Dist: packaging>=17
52
52
  Requires-Dist: py-walk>=0.3.1
@@ -58,7 +58,7 @@ Requires-Dist: wcmatch>=10
58
58
  Requires-Python: >=3.10
59
59
  Project-URL: Changelog, https://github.com/kdeldycke/repomatic/blob/main/changelog.md
60
60
  Project-URL: Documentation, https://kdeldycke.github.io/repomatic
61
- Project-URL: Download, https://github.com/kdeldycke/repomatic/releases/tag/v7.0.0
61
+ Project-URL: Download, https://github.com/kdeldycke/repomatic/releases/tag/v7.2.0
62
62
  Project-URL: Funding, https://github.com/sponsors/kdeldycke
63
63
  Project-URL: Homepage, https://github.com/kdeldycke/repomatic
64
64
  Project-URL: Issues, https://github.com/kdeldycke/repomatic/issues
@@ -1,11 +1,11 @@
1
1
  [build-system]
2
2
  build-backend = "uv_build"
3
- requires = [ "uv-build>=0.9" ]
3
+ requires = [ "uv-build>=0.9,<0.12" ]
4
4
 
5
5
  [project]
6
6
  # Docs: https://packaging.python.org/en/latest/guides/writing-pyproject-toml/
7
7
  name = "repomatic"
8
- version = "7.0.0"
8
+ version = "7.2.0"
9
9
  description = "🏭 Automate repository maintenance, releases, and CI/CD workflows"
10
10
  readme = "readme.md"
11
11
  keywords = [
@@ -84,10 +84,10 @@ dependencies = [
84
84
  "backports-strenum>=1; python_version<'3.11'",
85
85
  # boltons 25.0.0 dropped Python 3.9, matching our requires-python >= 3.10.
86
86
  "boltons>=25",
87
- # 8.1.0 is the first release carrying the APIs repomatic uses: the `test-suite`
88
- # engine (renamed from `test-plan`) and the `CONFIG_PATH_METADATA_KEY` /
89
- # `NORMALIZE_KEYS_METADATA_KEY` schema-metadata constants.
90
- "click-extra>=8.1",
87
+ # click-extra 8.3 adds the `click:config` Sphinx directive and the
88
+ # `schema_field_infos()` introspection API that `configuration.md` and
89
+ # `show-config` are built on.
90
+ "click-extra>=8.3",
91
91
  # extra-platforms 12.0.2 reimplemented is_macos() with sys.platform, avoiding
92
92
  # the platform.platform() shell-out to ``cmd /c ver`` on Windows that broke
93
93
  # under patched subprocess in tests and triggered a CPython 3.10.x bug.
@@ -122,7 +122,7 @@ dependencies = [
122
122
  ]
123
123
  urls.Changelog = "https://github.com/kdeldycke/repomatic/blob/main/changelog.md"
124
124
  urls.Documentation = "https://kdeldycke.github.io/repomatic"
125
- urls.Download = "https://github.com/kdeldycke/repomatic/releases/tag/v7.0.0"
125
+ urls.Download = "https://github.com/kdeldycke/repomatic/releases/tag/v7.2.0"
126
126
  urls.Funding = "https://github.com/sponsors/kdeldycke"
127
127
  urls.Homepage = "https://github.com/kdeldycke/repomatic"
128
128
  urls.Issues = "https://github.com/kdeldycke/repomatic/issues"
@@ -162,6 +162,9 @@ docs = [
162
162
  "sphinx-autodoc-typehints>=2.4",
163
163
  # sphinx-copybutton 0.5.2 stabilized the copy button behavior for code blocks.
164
164
  "sphinx-copybutton>=0.5.2",
165
+ # sphinx-datatables 1.0.0 is the first stable release; makes the binaries
166
+ # catalog table searchable and sortable client-side (pulls sphinxcontrib-jquery).
167
+ "sphinx-datatables>=1",
165
168
  # sphinx-design 0.6 introduced the current directive syntax for cards, grids, and tabs.
166
169
  "sphinx-design>=0.6",
167
170
  # sphinxcontrib-mermaid 1.2.3 fixed mermaid_d3_zoom configuration support.
@@ -170,12 +173,14 @@ docs = [
170
173
  "sphinxext-opengraph>=0.13",
171
174
  ]
172
175
  # types-boltons: first stub release covering boltons 25.0 API (FrozenDict, unique, flatten,
173
- # strip_ansi). types-pyyaml: stub release covering PyYAML 6.0 (safe_load, safe_dump, full_load).
176
+ # strip_ansi). types-docutils: covers the docutils.nodes imports in docs/conf.py.
177
+ # types-pyyaml: stub release covering PyYAML 6.0 (safe_load, safe_dump, full_load).
174
178
  typing = [
175
179
  # backports-strenum: provides StrEnum stubs for mypy --python-version 3.10 analysis;
176
180
  # not in main deps (version-gated to Python<3.11 there), so added here unconditionally.
177
181
  "backports-strenum>=1",
178
182
  "types-boltons>=25.0.0.20250822",
183
+ "types-docutils>=0.22.3.20251115",
179
184
  "types-pyyaml>=6.0.12.9",
180
185
  ]
181
186
 
@@ -200,9 +205,7 @@ exclude-newer = "1 week"
200
205
  # bypasses at that date and prunes each entry once its held version ages past
201
206
  # `exclude-newer`, returning the package to the normal cooldown. repomatic is
202
207
  # pinned to git main (no PyPI release to freeze), so it keeps a permanent
203
- # "0 day" span. click-extra is held at its 8.1.0 release date so the lock adopts
204
- # it before the 1-week cooldown elapses; sync-uv-lock prunes this entry once
205
- # 8.1.0 ages past `exclude-newer`.
208
+ # "0 day" span.
206
209
  exclude-newer-package = { repomatic = "0 day" }
207
210
  # Package is at root level, not in "./src/".
208
211
  build-backend.module-root = ""
@@ -216,7 +219,7 @@ copyright = "Kevin Deldycke <kevin@deldycke.com> and contributors. Distributed u
216
219
  file-description = "🏭 Automate repository maintenance, releases, and CI/CD workflows"
217
220
  # Numeric only: Nuitka rejects PEP 440 .devN suffixes. Kept in sync with the
218
221
  # package version by the dedicated [tool.bumpversion] rule that strips the suffix.
219
- file-version = "7.0.0"
222
+ file-version = "7.2.0"
220
223
  include-data-dir = [
221
224
  "repomatic/data/awesome_template=repomatic/data/awesome_template",
222
225
  ]
@@ -234,7 +237,7 @@ include-package-data = [ "click_extra" ]
234
237
  linux-icon = "docs/assets/icon.png"
235
238
  macos-app-icon = "docs/assets/icon.icns"
236
239
  product-name = "Repomatic"
237
- product-version = "7.0.0"
240
+ product-version = "7.2.0"
238
241
  windows-icon-from-ico = "docs/assets/icon.ico"
239
242
 
240
243
  [tool.ruff]
@@ -273,7 +276,14 @@ default.extend-identifiers = { Github = "GitHub", IOS = "iOS", Javascript = "Jav
273
276
  # False positives.
274
277
  default.extend-words = { astroid = "astroid", oject = "oject", PNGs = "PNGs" }
275
278
  # Skip content between <!-- typos:off --> and <!-- typos:on --> markers.
276
- default.extend-ignore-re = [ "(?s)<!-- typos:off -->.*?<!-- typos:on -->" ]
279
+ default.extend-ignore-re = [
280
+ "(?s)<!-- typos:off -->.*?<!-- typos:on -->",
281
+ # `{qeury}` is the intentional-typo example in the typos tool's own docs_notes
282
+ # guidance (tool_runner.py), which recommends downstream projects guard such
283
+ # examples and encoded hashes from the unattended fix-typos pull request.
284
+ "\\{qeury\\}",
285
+ 'base32 "[0-9a-z]{52}"',
286
+ ]
277
287
  default.extend-ignore-identifiers-re = [
278
288
  # error: `certifi` should be `certify`, `certifies`, `certified`
279
289
  # ╭▸ ./docs/assets/dependencies.mmd:184:11
@@ -315,6 +325,7 @@ warn_unused_configs = true
315
325
  overrides = [
316
326
  { module = [
317
327
  "py_walk.*",
328
+ "sphinxcontrib.mermaid",
318
329
  "vt",
319
330
  ], ignore_missing_imports = true },
320
331
  ]
@@ -340,7 +351,7 @@ run.source = [ "repomatic" ]
340
351
  report.precision = 2
341
352
 
342
353
  [tool.bumpversion]
343
- current_version = "7.0.0"
354
+ current_version = "7.2.0"
344
355
  # Parse versions with an optional .devN suffix (PEP 440).
345
356
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(\\.dev(?P<dev>\\d+))?"
346
357
  serialize = [
@@ -395,9 +406,13 @@ search = "## [`{current_version}` (unreleased)]("
395
406
  replace = "## [`{new_version}` (unreleased)]("
396
407
 
397
408
  [[tool.bumpversion.files]]
398
- # Update the version in the citation file.
409
+ # Update the version in the citation file. Anchored to a line start (regex) so
410
+ # it does not also match the `cff-version:` schema field, which would otherwise
411
+ # be overwritten with the package version on every bump as soon as the two
412
+ # values coincide once.
399
413
  filename = "./citation.cff"
400
- search = "version: {current_version}"
414
+ regex = true
415
+ search = "(?m)^version: {current_version}"
401
416
  replace = "version: {new_version}"
402
417
 
403
418
  [[tool.bumpversion.files]]
@@ -422,6 +437,9 @@ exclude = [
422
437
  "archive\\.ph",
423
438
  # npmjs.com returns 403 to bots.
424
439
  "^https://www\\.npmjs\\.com/package/.*$",
440
+ # VirusTotal analysis pages are a JS app that rate-limits bots, and the
441
+ # binaries page links one per released binary.
442
+ "^https://www\\.virustotal\\.com/gui/.*$",
425
443
  # star-history.com fragments are JS-rendered and invisible to lychee.
426
444
  "^https://star-history\\.com/.*$",
427
445
  # githubstatus.com returns 405 to HEAD requests from bots.
@@ -17,5 +17,5 @@
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
- __version__ = "7.0.0"
21
- __git_tag_sha__ = "36523e5a56f287e814210042ca7b852147a95498"
20
+ __version__ = "7.2.0"
21
+ __git_tag_sha__ = "7336863d9f3f5faea26f887211f3f745dc963fb5"
@@ -0,0 +1,448 @@
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
+ """Generate the binaries catalog: a CSV data file and its `docs/binaries.md` page.
18
+
19
+ The catalog inventories every compiled binary the repository ever released,
20
+ one CSV row per binary: version (linking to the GitHub release), platform
21
+ target (linking to the direct download), release date, and the VirusTotal
22
+ detection snapshot (linking to the live analysis). It gives alpha and beta
23
+ testers a single place to grab binaries from, and the maintainer an overview
24
+ of how antivirus engines treat each release.
25
+
26
+ The data lives in `docs/assets/binaries.csv`, regenerated wholesale on every
27
+ release from the GitHub Releases API (the single source of truth for
28
+ published assets) and the JSON scan history maintained by `scan-virustotal`.
29
+ The Markdown page renders it through a single `csv-table` directive and is
30
+ otherwise static: it is created once from {data}`PAGE_TEMPLATE` and only its
31
+ marker-delimited region (the detection trend chart) is rewritten afterwards,
32
+ so the intro and section prose stay hand-editable per repository.
33
+
34
+ ```{note}
35
+ On the documentation site, the table is searchable and sortable client-side
36
+ via the `sphinx-datatables` extension, which activates on the
37
+ `sphinx-datatable` CSS class. The extension is optional: without it the
38
+ `csv-table` directive still renders a plain table, and on GitHub the CSV
39
+ file itself gets the built-in searchable grid viewer.
40
+ ```
41
+
42
+ ```{note}
43
+ Development builds are only linked, not cataloged: the rolling dev
44
+ pre-release is refreshed on every push to the default branch, so any row
45
+ frozen into the CSV would be stale within hours, while the workflow run
46
+ artifacts behind the link always are the current builds.
47
+ ```
48
+ """
49
+
50
+ from __future__ import annotations
51
+
52
+ import csv
53
+ import io
54
+ import json
55
+ import logging
56
+ from pathlib import Path
57
+
58
+ from packaging.version import InvalidVersion, Version
59
+
60
+ from .binary import NUITKA_BUILD_TARGETS
61
+ from .virustotal import VIRUSTOTAL_GUI_URL
62
+
63
+ TYPE_CHECKING = False
64
+ if TYPE_CHECKING:
65
+ from collections.abc import Sequence
66
+
67
+ from .github.releases import ReleaseAsset, ReleaseWithAssets
68
+ from .virustotal import ScanRecord
69
+
70
+ BINARY_ASSET_SUFFIXES = (".bin", ".exe")
71
+ """File extensions identifying compiled binaries among release assets.
72
+
73
+ Same set the `scan-virustotal` command uploads and the release workflow
74
+ downloads (`--pattern` flags in `_release-engine.yaml`).
75
+ """
76
+
77
+ CSV_HEADERS = (
78
+ "Version",
79
+ "Platform",
80
+ "Released",
81
+ "VirusTotal",
82
+ )
83
+ """Column headers of the binaries CSV.
84
+
85
+ Deliberately compact: the version cell carries the link to the GitHub
86
+ release, the platform cell the direct binary download, and the VirusTotal
87
+ cell the analysis link, so no column holds a bare URL, filename, or
88
+ 64-character checksum.
89
+ """
90
+
91
+ FLAGGED_DANGER_PCT = 10
92
+ """Flagged-verdict share (percent) at which the catalog shield turns red.
93
+
94
+ Below it, a flagged binary is the routine Nuitka false-positive tail worth a
95
+ warning tint; from one engine in ten upward, the release deserves a
96
+ false-positive submission round (see the `/av-false-positive` skill).
97
+ """
98
+
99
+ PAGE_END_MARKER = "<!-- binaries-end -->"
100
+ """Closing marker of the generated chart region in the binaries page."""
101
+
102
+ PAGE_START_MARKER = "<!-- binaries-start -->"
103
+ """Opening marker of the generated chart region in the binaries page."""
104
+
105
+ PAGE_TEMPLATE = """\
106
+ ---
107
+ orphan: true
108
+ ---
109
+
110
+ # Binaries
111
+
112
+ All standalone executables published by this repository, one row per binary, newest release first. The version links to its GitHub release, the platform to the direct binary download, and the VirusTotal cell to the file's public analysis.
113
+
114
+ Compiled Python binaries are regularly flagged by heuristic antivirus engines, so every release is submitted to [VirusTotal](https://www.virustotal.com/): this seeds vendor databases with the new signatures and keeps false positives in check. The VirusTotal cell tracks those false positives: a green check marks binaries no engine flags, and flagged binaries show the share of engine verdicts flagging them, snapshotted minutes after publication and before false-positive reports get processed. The live analysis behind the link supersedes it.
115
+
116
+ ## Development builds
117
+
118
+ Fresh binaries are compiled from every push to the default branch by the [release workflow]({repo_url}/actions/workflows/release.yaml). To try the latest development build: open the most recent successful run and download the artifact matching your platform (a GitHub account is required, and the binary comes wrapped in a zip). The same builds are also attached to a rolling dev pre-release, a draft only visible to repository maintainers.
119
+
120
+ <!-- binaries-start -->
121
+
122
+ <!-- binaries-end -->
123
+
124
+ ## Catalog
125
+
126
+ The table is searchable and sortable on the documentation site; the raw data lives in [`binaries.csv`](assets/binaries.csv).
127
+
128
+ ```{csv-table}
129
+ :file: assets/binaries.csv
130
+ :header-rows: 1
131
+ :class: sphinx-datatable
132
+ ```
133
+ """
134
+ """Initial page content, used when the page does not exist yet.
135
+
136
+ The `{repo_url}` placeholder is substituted with `str.replace` (not
137
+ `str.format`, which would choke on the csv-table directive's braces).
138
+ Everything outside the marker pair is written once and never touched again:
139
+ repositories can reword the prose without fighting the generator.
140
+
141
+ :meta hide-value:
142
+ """
143
+
144
+
145
+ def _platform_target(name: str) -> str:
146
+ """Extract the platform-arch target from a binary filename.
147
+
148
+ Matches against the known Nuitka build targets (`linux-arm64`, …), the
149
+ same vocabulary the compile matrix uses. Falls back to the full filename
150
+ for foreign naming schemes, so the cell always identifies the file.
151
+ """
152
+ for target in NUITKA_BUILD_TARGETS:
153
+ if target in name:
154
+ return target
155
+ return name
156
+
157
+
158
+ def _release_version(tag: str) -> Version | None:
159
+ """Parse a release tag as a version, or `None` for foreign tag schemes."""
160
+ try:
161
+ return Version(tag.removeprefix("v"))
162
+ except InvalidVersion:
163
+ return None
164
+
165
+
166
+ def _binary_assets(release: ReleaseWithAssets) -> list[ReleaseAsset]:
167
+ """Return the release's compiled binaries, sorted by filename."""
168
+ return sorted(
169
+ (a for a in release.assets if a.name.endswith(BINARY_ASSET_SUFFIXES)),
170
+ key=lambda asset: asset.name,
171
+ )
172
+
173
+
174
+ def _at_scan_records(records: Sequence[ScanRecord]) -> dict[str, ScanRecord]:
175
+ """Index records by SHA-256, keeping the earliest snapshot per file.
176
+
177
+ The earliest record is the at-release snapshot; later re-scans of the
178
+ same file reflect vendor whitelisting, not the state the binary shipped
179
+ in, so they are excluded from the catalog and chart.
180
+ """
181
+ earliest: dict[str, ScanRecord] = {}
182
+ for record in records:
183
+ current = earliest.get(record.sha256)
184
+ if current is None or record.scanned < current.scanned:
185
+ earliest[record.sha256] = record
186
+ return earliest
187
+
188
+
189
+ def render_chart_section(records: Sequence[ScanRecord]) -> str:
190
+ """Render the detection trend across releases as a Chart.js timeline.
191
+
192
+ Plots the share of antivirus engine verdicts flagging each release's
193
+ binaries (all platforms aggregated), using the at-release snapshot of
194
+ every file, on a true time axis: spacing reflects the actual gaps
195
+ between releases. Points reuse the catalog shields' color language,
196
+ read at view time from sphinx-design's CSS variables so they match the
197
+ theme exactly (with hardcoded fallbacks). The data is embedded in the
198
+ page rather than fetched, so the chart also works on `file://`
199
+ previews; only the Chart.js bundle comes from its CDN, mirroring how
200
+ the table's DataTables assets load.
201
+
202
+ :return: A `## VirusTotal detections` section with a `raw` HTML fence,
203
+ or an empty string when fewer than two releases have records (a
204
+ one-point trend is not a trend).
205
+ """
206
+ at_scan = _at_scan_records(records).values()
207
+ per_tag: dict[str, tuple[str, int, int]] = {}
208
+ for record in at_scan:
209
+ date, flagged, total = per_tag.get(record.tag, (record.scanned, 0, 0))
210
+ per_tag[record.tag] = (
211
+ min(date, record.scanned),
212
+ flagged + record.stats.flagged,
213
+ total + record.stats.total,
214
+ )
215
+
216
+ points = []
217
+ for tag, (date, flagged, total) in per_tag.items():
218
+ version = _release_version(tag)
219
+ if version is None:
220
+ continue
221
+ if total:
222
+ points.append((version, tag, date, flagged, total))
223
+ points.sort()
224
+ if len(points) < 2:
225
+ return ""
226
+
227
+ payload = json.dumps(
228
+ [
229
+ {
230
+ "date": date,
231
+ "flagged": flagged,
232
+ "pct": round(100 * flagged / total, 1),
233
+ "tag": tag,
234
+ "total": total,
235
+ }
236
+ for _version, tag, date, flagged, total in points
237
+ ],
238
+ sort_keys=True,
239
+ )
240
+ return (
241
+ "## VirusTotal detections\n\n"
242
+ "Share of antivirus engine verdicts flagging the binaries of each "
243
+ "release, at scan time. Colors follow the catalog shields: green "
244
+ f"for zero detections, amber below {FLAGGED_DANGER_PCT}%, red from "
245
+ "there up.\n\n"
246
+ "```{raw} html\n"
247
+ '<div style="height: 320px;"><canvas id="vt-trend"></canvas></div>\n'
248
+ '<script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.0/dist/'
249
+ 'chart.umd.min.js"></script>\n'
250
+ "<script>\n"
251
+ f"const VT_TREND = {payload};\n"
252
+ f"const VT_DANGER_PCT = {FLAGGED_DANGER_PCT};\n"
253
+ "const vtCss = getComputedStyle(document.documentElement);\n"
254
+ "const vtColor = (name, fallback) =>\n"
255
+ " vtCss.getPropertyValue(name).trim() || fallback;\n"
256
+ "const vtTint = (p) => {\n"
257
+ ' if (p.pct === 0) { return vtColor("--sd-color-success", "#28a745"); }\n'
258
+ " return p.pct >= VT_DANGER_PCT\n"
259
+ ' ? vtColor("--sd-color-danger", "#dc3545")\n'
260
+ ' : vtColor("--sd-color-warning", "#f0b37e");\n'
261
+ "};\n"
262
+ 'new Chart(document.getElementById("vt-trend"), {\n'
263
+ ' type: "line",\n'
264
+ " data: {\n"
265
+ " datasets: [{\n"
266
+ " data: VT_TREND.map((p) => ({x: Date.parse(p.date), y: p.pct})),\n"
267
+ ' borderColor: "#88888866",\n'
268
+ " pointBackgroundColor: VT_TREND.map(vtTint),\n"
269
+ " pointBorderColor: VT_TREND.map(vtTint),\n"
270
+ " pointRadius: 4,\n"
271
+ " tension: 0.2,\n"
272
+ " }],\n"
273
+ " },\n"
274
+ " options: {\n"
275
+ " maintainAspectRatio: false,\n"
276
+ " plugins: {\n"
277
+ " legend: {display: false},\n"
278
+ " tooltip: {callbacks: {\n"
279
+ " title: (items) => VT_TREND[items[0].dataIndex].tag,\n"
280
+ " label: (item) => {\n"
281
+ " const p = VT_TREND[item.dataIndex];\n"
282
+ ' return p.flagged + " / " + p.total\n'
283
+ ' + " verdicts flagged (" + p.pct + "%)";\n'
284
+ " },\n"
285
+ " }},\n"
286
+ " },\n"
287
+ " scales: {\n"
288
+ " x: {\n"
289
+ ' type: "linear",\n'
290
+ " ticks: {\n"
291
+ " maxTicksLimit: 8,\n"
292
+ " callback: (value) =>\n"
293
+ " new Date(value).toISOString().slice(0, 10),\n"
294
+ " },\n"
295
+ " },\n"
296
+ " y: {\n"
297
+ " beginAtZero: true,\n"
298
+ ' title: {display: true, text: "Flagged verdicts (%)"},\n'
299
+ " },\n"
300
+ " },\n"
301
+ " },\n"
302
+ "});\n"
303
+ "</script>\n"
304
+ "```"
305
+ )
306
+
307
+
308
+ def render_binaries_csv(
309
+ repo_slug: str,
310
+ releases: Sequence[ReleaseWithAssets],
311
+ records: Sequence[ScanRecord],
312
+ ) -> str:
313
+ """Render the catalog data as CSV, one row per released binary.
314
+
315
+ Rows cover every published release carrying compiled binaries, ordered
316
+ by descending version then filename. Cells hold Markdown links (parsed
317
+ by MyST inside the `csv-table` directive): the version to the GitHub
318
+ release, the platform to the binary download, and the VirusTotal cell to
319
+ the file's analysis. The VirusTotal cell renders the at-release snapshot
320
+ as a green check when no engine flags the binary, as the flagged-verdict
321
+ share (tinted by {data}`FLAGGED_DANGER_PCT`) otherwise, and as a bare
322
+ `analysis` link when no snapshot exists. Assets without a digest get an
323
+ empty VirusTotal cell.
324
+
325
+ ```{caution}
326
+ The version and platform cells decorate their links with sphinx-design's
327
+ `octicon` role, so the rendering repository needs `sphinx-design` in its
328
+ documentation build (already true across this ecosystem's docs stacks).
329
+ ```
330
+
331
+ :param repo_slug: Repository in `owner/repo` form.
332
+ :param releases: Releases from
333
+ {func}`repomatic.github.releases.get_releases_with_assets`.
334
+ :param records: Detection snapshots from the scan history file.
335
+ :return: The full CSV content, header row included.
336
+ """
337
+ versioned = [
338
+ (version, release)
339
+ for release in releases
340
+ if not release.draft and (version := _release_version(release.tag)) is not None
341
+ ]
342
+ versioned.sort(key=lambda pair: pair[0], reverse=True)
343
+
344
+ at_scan = _at_scan_records(records)
345
+ buffer = io.StringIO()
346
+ writer = csv.writer(buffer, lineterminator="\n")
347
+ writer.writerow(CSV_HEADERS)
348
+ for version, release in versioned:
349
+ release_url = f"https://github.com/{repo_slug}/releases/tag/{release.tag}"
350
+ for asset in _binary_assets(release):
351
+ if asset.sha256:
352
+ vt_url = VIRUSTOTAL_GUI_URL.format(sha256=asset.sha256)
353
+ record = at_scan.get(asset.sha256)
354
+ if record and record.stats.total:
355
+ # The KPI is the distance to the goal (zero false
356
+ # positives), so flagged rows show the failure share and
357
+ # clean rows reduce to a green check: exceptions stand
358
+ # out, the goal state stays calm.
359
+ if record.stats.flagged == 0:
360
+ vt_cell = (
361
+ f"[{{octicon}}`shield-check;1em;sd-text-success`]({vt_url})"
362
+ )
363
+ else:
364
+ pct = 100 * record.stats.flagged / record.stats.total
365
+ tint = (
366
+ "sd-text-danger"
367
+ if pct >= FLAGGED_DANGER_PCT
368
+ else "sd-text-warning"
369
+ )
370
+ vt_cell = (
371
+ f"[{{octicon}}`shield;1em;{tint}` {pct:.1f}%]({vt_url})"
372
+ )
373
+ else:
374
+ vt_cell = f"[{{octicon}}`shield` analysis]({vt_url})"
375
+ else:
376
+ vt_cell = ""
377
+ # The octicon role comes from sphinx-design, same as the icons in
378
+ # the docs page titles; the icons are part of the link text so
379
+ # they stay clickable with their label.
380
+ writer.writerow((
381
+ f"[`{version}` {{octicon}}`link-external`]({release_url})",
382
+ f"[{{octicon}}`download` `{_platform_target(asset.name)}`]"
383
+ f"({asset.download_url})",
384
+ release.date,
385
+ vt_cell,
386
+ ))
387
+ return buffer.getvalue()
388
+
389
+
390
+ def update_binaries_csv(csv_path: Path, content: str) -> bool:
391
+ """Write the catalog CSV, creating parent directories as needed.
392
+
393
+ :param csv_path: Path to the CSV file.
394
+ :param content: Rendered CSV from {func}`render_binaries_csv`.
395
+ :return: `True` when the file was created or its content changed.
396
+ """
397
+ if csv_path.exists() and csv_path.read_text(encoding="UTF-8") == content:
398
+ logging.info(f"Binaries CSV {csv_path} already up to date.")
399
+ return False
400
+ csv_path.parent.mkdir(parents=True, exist_ok=True)
401
+ csv_path.write_text(content, encoding="UTF-8")
402
+ logging.info(f"Wrote binaries CSV {csv_path}.")
403
+ return True
404
+
405
+
406
+ def update_binaries_page(page_path: Path, chart_section: str, repo_slug: str) -> bool:
407
+ """Create the binaries page if missing and refresh its chart region.
408
+
409
+ A missing page is created (with parent directories) from
410
+ {data}`PAGE_TEMPLATE`. On an existing page only the region between
411
+ {data}`PAGE_START_MARKER` and {data}`PAGE_END_MARKER` is replaced,
412
+ leaving all surrounding prose untouched.
413
+
414
+ :param page_path: Path to the Markdown page.
415
+ :param chart_section: Rendered chart from {func}`render_chart_section`,
416
+ or an empty string to leave the region empty.
417
+ :param repo_slug: Repository in `owner/repo` form, interpolated into the
418
+ template on first creation.
419
+ :return: `True` when the file was created or its content changed.
420
+ :raises ValueError: When the page exists but lacks the markers. Loud on
421
+ purpose: a page not written by this generator must never be
422
+ overwritten.
423
+ """
424
+ original = None
425
+ if page_path.exists():
426
+ original = page_path.read_text(encoding="UTF-8")
427
+ if PAGE_START_MARKER not in original or PAGE_END_MARKER not in original:
428
+ raise ValueError(
429
+ f"{page_path} lacks the {PAGE_START_MARKER} / {PAGE_END_MARKER} "
430
+ "markers, refusing to overwrite it."
431
+ )
432
+ text = original
433
+ else:
434
+ text = PAGE_TEMPLATE.replace("{repo_url}", f"https://github.com/{repo_slug}")
435
+
436
+ before, rest = text.split(PAGE_START_MARKER, 1)
437
+ _, after = rest.split(PAGE_END_MARKER, 1)
438
+ section = chart_section.strip()
439
+ between = f"\n\n{section}\n\n" if section else "\n\n"
440
+ new_text = before + PAGE_START_MARKER + between + PAGE_END_MARKER + after
441
+
442
+ if new_text == original:
443
+ logging.info(f"Binaries page {page_path} already up to date.")
444
+ return False
445
+ page_path.parent.mkdir(parents=True, exist_ok=True)
446
+ page_path.write_text(new_text, encoding="UTF-8")
447
+ logging.info(f"Wrote binaries page {page_path}.")
448
+ return True
@@ -112,6 +112,7 @@ FLAT_BUILD_TARGETS = [
112
112
 
113
113
 
114
114
  BINARY_AFFECTING_PATHS: Final[tuple[str, ...]] = (
115
+ ".github/workflows/_release-engine.yaml",
115
116
  ".github/workflows/release.yaml",
116
117
  "pyproject.toml",
117
118
  "tests/",
@@ -122,6 +123,12 @@ BINARY_AFFECTING_PATHS: Final[tuple[str, ...]] = (
122
123
  Project-specific source directories (derived from `[project.scripts]` in
123
124
  `pyproject.toml`) are added dynamically by
124
125
  {attr}`~repomatic.metadata.Metadata.binary_affecting_paths`.
126
+
127
+ The release workflow entries cover both layouts: upstream keeps the
128
+ `_release-engine.yaml` lane (which defines the Nuitka compile and binary
129
+ self-test jobs) in-repo, while downstream repos call the engine cross-repo
130
+ from their generated `release.yaml`, so a pin bump there rightly triggers a
131
+ rebuild.
125
132
  """
126
133
 
127
134
  SKIP_BINARY_BUILD_BRANCHES: Final[frozenset[str]] = frozenset((
@@ -21,7 +21,7 @@ GitHub issue. Sphinx linkcheck parsing detects broken auto-generated links
21
21
  (intersphinx, autodoc, type annotations) that Lychee cannot see because they
22
22
  only exist in the rendered HTML output.
23
23
 
24
- Issue lifecycle management is delegated to {mod}`~repomatic.issue`.
24
+ Issue lifecycle management is delegated to {mod}`~repomatic.github.issue`.
25
25
  """
26
26
 
27
27
  from __future__ import annotations