repomatic 7.6.0__tar.gz → 7.7.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 (171) hide show
  1. {repomatic-7.6.0 → repomatic-7.7.0}/PKG-INFO +2 -2
  2. {repomatic-7.6.0 → repomatic-7.7.0}/pyproject.toml +10 -6
  3. {repomatic-7.6.0 → repomatic-7.7.0}/pyproject.toml.orig +28 -8
  4. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/__init__.py +2 -2
  5. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/__main__.py +9 -8
  6. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/awesome_toc.py +3 -3
  7. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/binaries_page.py +36 -68
  8. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/binary.py +119 -7
  9. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/broken_links.py +34 -14
  10. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/cache.py +51 -7
  11. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/changelog.py +257 -117
  12. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/checksums.py +11 -5
  13. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/cli.py +124 -292
  14. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/config.py +6 -3
  15. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/_release-engine.yaml +47 -25
  16. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/autofix.yaml +57 -47
  17. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/cancel-runs.yaml +1 -1
  18. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/changelog.yaml +14 -14
  19. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/debug.yaml +2 -2
  20. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/docs.yaml +6 -4
  21. repomatic-7.7.0/repomatic/data/labeller-content-based.yaml +31 -0
  22. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/labels.yaml +12 -10
  23. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/lint.yaml +18 -14
  24. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/release.yaml +15 -5
  25. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/skills/awesome-triage/SKILL.md +10 -10
  26. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/skills/babysit-ci/SKILL.md +10 -4
  27. repomatic-7.7.0/repomatic/data/skills/github-housekeeping/SKILL.md +129 -0
  28. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/skills/repomatic-audit/SKILL.md +1 -1
  29. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/skills/repomatic-ship/SKILL.md +18 -2
  30. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/tests.yaml +8 -5
  31. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/unsubscribe.yaml +1 -1
  32. repomatic-7.7.0/repomatic/data/vt-trend-chart.js +52 -0
  33. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/dep_graph.py +14 -10
  34. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/dep_report.py +139 -64
  35. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/dep_sources.py +45 -57
  36. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/docs.py +28 -16
  37. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/git_ops.py +12 -5
  38. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/github/actions.py +24 -1
  39. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/github/dev_release.py +30 -7
  40. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/github/gh.py +24 -0
  41. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/github/issue.py +14 -5
  42. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/github/matrix.py +35 -6
  43. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/github/release_sync.py +8 -27
  44. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/github/releases.py +6 -22
  45. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/github/sponsor.py +31 -24
  46. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/github/token.py +26 -13
  47. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/github/unsubscribe.py +9 -27
  48. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/github/workflow_sync.py +126 -226
  49. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/images.py +68 -60
  50. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/init_project.py +189 -149
  51. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/labels.py +39 -20
  52. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/lint_repo.py +304 -70
  53. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/mailmap.py +24 -0
  54. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/matrix_axes.py +0 -7
  55. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/metadata.py +194 -136
  56. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/npm.py +5 -1
  57. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/plugin.py +16 -6
  58. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/prepare_release.py +72 -76
  59. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/pypi.py +94 -47
  60. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/pyproject.py +40 -1
  61. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/registry.py +49 -13
  62. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/setup_guide.py +19 -8
  63. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/sync_ops.py +205 -15
  64. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/yanked-admonition.md +2 -2
  65. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/tool_registry.py +85 -3
  66. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/tool_runner.py +182 -127
  67. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/tool_runner_page.py +7 -19
  68. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/uv.py +199 -106
  69. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/version_sync.py +73 -0
  70. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/virustotal.py +9 -2
  71. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/vulnerable_deps.py +29 -12
  72. repomatic-7.6.0/repomatic/data/labeller-content-based.yaml +0 -37
  73. {repomatic-7.6.0 → repomatic-7.7.0}/license +0 -0
  74. {repomatic-7.6.0 → repomatic-7.7.0}/readme.md +0 -0
  75. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/bundle.py +0 -0
  76. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/compat.py +0 -0
  77. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/__init__.py +0 -0
  78. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/action-publish-pypi.yaml +0 -0
  79. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/agent-grunt-qa.md +0 -0
  80. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/agent-qa-engineer.md +0 -0
  81. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/agent-sphinx-docs.md +0 -0
  82. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/autolock.yaml +0 -0
  83. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/awesome_template/.github/ISSUE_TEMPLATE/config.yml +0 -0
  84. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/awesome_template/.github/ISSUE_TEMPLATE/new-link.yaml +0 -0
  85. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/awesome_template/.github/code-of-conduct.md +0 -0
  86. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/awesome_template/.github/contributing.md +0 -0
  87. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/awesome_template/.github/contributing.zh.md +0 -0
  88. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/awesome_template/.github/funding.yml +0 -0
  89. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/awesome_template/.github/pull_request_template.md +0 -0
  90. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/awesome_template/__init__.py +0 -0
  91. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/awesome_template/license +0 -0
  92. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/bumpversion.toml +0 -0
  93. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/codecov.yaml +0 -0
  94. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/labeller-file-based.yaml +0 -0
  95. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/labels.toml +0 -0
  96. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/lychee.toml +0 -0
  97. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/mdformat.toml +0 -0
  98. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/mypy.toml +0 -0
  99. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/pytest.toml +0 -0
  100. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/ruff.toml +0 -0
  101. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/skills/av-false-positive/SKILL.md +0 -0
  102. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/skills/benchmark-update/SKILL.md +0 -0
  103. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/skills/brand-assets/SKILL.md +0 -0
  104. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/skills/file-bug-report/SKILL.md +0 -0
  105. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/skills/repomatic-changelog/SKILL.md +0 -0
  106. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/skills/repomatic-deps/SKILL.md +0 -0
  107. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/skills/repomatic-init/SKILL.md +0 -0
  108. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/skills/repomatic-topics/SKILL.md +0 -0
  109. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/skills/sphinx-docs-sync/SKILL.md +0 -0
  110. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/skills/translation-sync/SKILL.md +0 -0
  111. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/skills/upstream-audit/SKILL.md +0 -0
  112. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/typos.toml +0 -0
  113. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/uv.toml +0 -0
  114. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/yamllint.yaml +0 -0
  115. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/data/zizmor.yaml +0 -0
  116. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/frontmatter.py +0 -0
  117. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/github/__init__.py +0 -0
  118. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/github/pr_body.py +0 -0
  119. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/github/status.py +0 -0
  120. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/gitignore.py +0 -0
  121. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/http.py +0 -0
  122. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/humanize.py +0 -0
  123. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/py.typed +0 -0
  124. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/__init__.py +0 -0
  125. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/available-admonition.md +0 -0
  126. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/broken-links-issue.md +0 -0
  127. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/bump-version.md +0 -0
  128. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/detect-squash-merge.md +0 -0
  129. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/development-warning.md +0 -0
  130. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/fix-changelog.md +0 -0
  131. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/fix-typos.md +0 -0
  132. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/fix-vulnerable-deps.md +0 -0
  133. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/format-images.md +0 -0
  134. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/format-json.md +0 -0
  135. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/format-markdown.md +0 -0
  136. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/format-pyproject.md +0 -0
  137. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/format-python.md +0 -0
  138. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/format-shell.md +0 -0
  139. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/generated-footer.md +0 -0
  140. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/github-releases.md +0 -0
  141. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/immutable-releases.md +0 -0
  142. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/pr-metadata.md.noformat +0 -0
  143. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/prepare-release.md.noformat +0 -0
  144. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/refresh-tip.md +0 -0
  145. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/release-notes.md +0 -0
  146. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/release-sync-report.md +0 -0
  147. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/setup-guide-branch-ruleset.md +0 -0
  148. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/setup-guide-dependabot.md +0 -0
  149. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/setup-guide-fork-pr-approval.md +0 -0
  150. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/setup-guide-notifications-pat.md +0 -0
  151. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/setup-guide-pages-source.md +0 -0
  152. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/setup-guide-pypi-trusted-publisher.md +0 -0
  153. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/setup-guide-sha-pinning-required.md +0 -0
  154. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/setup-guide-token.md +0 -0
  155. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/setup-guide-verify.md +0 -0
  156. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/setup-guide-virustotal.md +0 -0
  157. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/setup-guide.md +0 -0
  158. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/sync-action-pins.md +0 -0
  159. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/sync-bumpversion.md +0 -0
  160. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/sync-dep-sources.md +0 -0
  161. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/sync-gitignore.md +0 -0
  162. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/sync-mailmap.md +0 -0
  163. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/sync-repomatic.md +0 -0
  164. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/sync-tool-versions.md +0 -0
  165. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/sync-uv-lock.md +0 -0
  166. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/sync-workflow-pins.md +0 -0
  167. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/unavailable-admonition.md +0 -0
  168. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/unsubscribe-phase1.md +0 -0
  169. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/unsubscribe-phase2.md +0 -0
  170. {repomatic-7.6.0 → repomatic-7.7.0}/repomatic/templates/update-dep-graph.md +0 -0
  171. {repomatic-7.6.0 → repomatic-7.7.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.7.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.7.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
@@ -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.7.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.7.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.7.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.7.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]
@@ -248,7 +249,7 @@ source = ["repomatic"]
248
249
  precision = 2
249
250
 
250
251
  [tool.bumpversion]
251
- current_version = "7.6.0"
252
+ current_version = "7.7.0"
252
253
  parse = '(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(\.dev(?P<dev>\d+))?'
253
254
  serialize = [
254
255
  "{major}.{minor}.{patch}.dev{dev}",
@@ -333,10 +334,13 @@ pypi-package-history = [
333
334
  "gha-utils",
334
335
  "repokit",
335
336
  ]
336
- release-assets = ["repomatic-plugin.zip"]
337
+ release-assets = ["repomatic-claude-plugin.zip"]
337
338
 
338
339
  [tool.repomatic.changelog]
339
340
  archive-location = "./docs/changelog-archive.md"
340
341
 
341
342
  [tool.repomatic.manpages]
342
343
  script = "repomatic.cli:repomatic"
344
+
345
+ [tool.actionlint.paths."**/workflows/*"]
346
+ 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.7.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.7.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.7.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.7.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 -->",
@@ -395,7 +398,7 @@ run.source = [ "repomatic" ]
395
398
  report.precision = 2
396
399
 
397
400
  [tool.bumpversion]
398
- current_version = "7.6.0"
401
+ current_version = "7.7.0"
399
402
  # Parse versions with an optional .devN suffix (PEP 440).
400
403
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(\\.dev(?P<dev>\\d+))?"
401
404
  serialize = [
@@ -522,4 +525,21 @@ pypi-package-history = [ "gha-utils", "repokit" ]
522
525
  # attached to every GitHub release by the engine's extra-assets job. The
523
526
  # marketplace at .claude-plugin/marketplace.json installs the plugin straight
524
527
  # from this asset, so a release missing it breaks `/plugin install`.
525
- release-assets = [ "repomatic-plugin.zip" ]
528
+ release-assets = [ "repomatic-claude-plugin.zip" ]
529
+
530
+ [tool.actionlint]
531
+ # actionlint reads no pyproject section of its own: repomatic serializes this one
532
+ # to a temporary YAML config and passes it via `--config-file`. `paths` keys are
533
+ # globs relative to the repository root, and each `ignore` entry is a regular
534
+ # expression matched against the error message.
535
+ #
536
+ # `github.job_workflow_sha` is a real context property, holding the commit SHA of
537
+ # the reusable workflow file itself, which the tool-cache keys read so they do not
538
+ # rotate on every caller commit. GitHub documents it only as an OIDC token claim,
539
+ # never on the `github` context reference page, so actionlint's schema lacks it.
540
+ # Drop this entry once rhysd/actionlint#671 ships in a release.
541
+ #
542
+ # The glob carries no dot on purpose: click-extra expands every dotted key of
543
+ # `pyproject.toml`, and one starting with a dot makes it warn on each repomatic
544
+ # invocation. `**` matches dot-directories, so `.github/workflows/` is covered.
545
+ paths."**/workflows/*".ignore = [ 'property "job_workflow_sha" is not defined' ]
@@ -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.7.0"
21
+ __git_tag_sha__ = "2c6e58c0d2ec44e627868095eb4c22b98683c463"
@@ -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
 
@@ -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
 
@@ -26,6 +26,8 @@ from __future__ import annotations
26
26
 
27
27
  import hashlib
28
28
  import logging
29
+ import re
30
+ import shutil
29
31
  import struct
30
32
  from pathlib import Path
31
33
 
@@ -38,6 +40,25 @@ if TYPE_CHECKING:
38
40
  from typing import Final
39
41
 
40
42
 
43
+ BINARY_ASSET_SUFFIXES = (".bin", ".exe")
44
+ """File extensions identifying compiled binaries among release assets.
45
+
46
+ The one definition of "a compiled release asset": `scan-virustotal` uploads
47
+ this set, the release workflow downloads it (`--pattern` flags in
48
+ `_release-engine.yaml`), `docs/binaries.md` lists it, and the dev-release
49
+ asset globs derive from it.
50
+ """
51
+
52
+ PYTHON_DIST_SUFFIXES = (".tar.gz", ".whl")
53
+ """File extensions identifying Python distributions among release assets.
54
+
55
+ The counterpart of {data}`BINARY_ASSET_SUFFIXES`, and the same kind of single
56
+ definition: {func}`pack_binary_assets` excludes this set from the binary
57
+ upload list (`create-release` already attached those), and the dev-release
58
+ asset globs add it on top of the compiled binaries.
59
+ """
60
+
61
+
41
62
  def compute_file_sha256(path: Path) -> str:
42
63
  """Compute the SHA-256 hex digest of a file.
43
64
 
@@ -112,19 +133,20 @@ NUITKA_BUILD_TARGETS = {
112
133
  "min_os": "10",
113
134
  },
114
135
  }
115
- """List of GitHub-hosted runners used for Nuitka builds.
136
+ """GitHub-hosted runner matrix for Nuitka builds, keyed by target name.
116
137
 
117
- The key of the dictionary is the target name, which is used as a short name for
118
- user-friendlyness. As such, it is used to name the compiled binary.
138
+ The key doubles as the compiled binary's short target identifier: it names the
139
+ published release asset, so it is chosen for user-friendliness and must stay
140
+ stable (download URLs and `docs/binaries.md` match on it).
119
141
 
120
142
  Values are dictionaries with the following keys:
121
143
 
122
144
  - `os`: Operating system name, as used in [GitHub-hosted runners](https://docs.github.com/en/actions/writing-workflows/choosing-where-your-workflow-runs/choosing-the-runner-for-a-job#standard-github-hosted-runners-for-public-repositories).
123
145
 
124
146
  ```{hint}
125
- We choose to run the compilation only on the latest supported version of each
126
- OS, for each architecture. Note that macOS and Windows do not have the latest
127
- version available for each architecture.
147
+ Compilation only runs on the latest supported version of each OS, for each
148
+ architecture. macOS and Windows do not offer their latest version on every
149
+ architecture.
128
150
  ```
129
151
 
130
152
  - `platform_id`: Platform identifier, as defined by [Extra Platform](https://github.com/kdeldycke/extra-platforms).
@@ -168,6 +190,86 @@ FLAT_BUILD_TARGETS = [
168
190
  """List of build targets in a flat format, suitable for matrix inclusion."""
169
191
 
170
192
 
193
+ def binary_name(package: str, target: str, version: str | None = None) -> str:
194
+ """Compose a compiled binary's release-asset filename.
195
+
196
+ The one definition of the naming convention:
197
+ ``{package}-{version}-{target}.{ext}`` for the versioned upload, and with
198
+ no *version* the stable alias (``{package}-{target}.{ext}``) backing the
199
+ `releases/latest/download` URLs. The extension comes from
200
+ {data}`NUITKA_BUILD_TARGETS`.
201
+ """
202
+ extension = NUITKA_BUILD_TARGETS[target]["extension"]
203
+ middle = f"-{version}" if version else ""
204
+ return f"{package}{middle}-{target}.{extension}"
205
+
206
+
207
+ def versionless_alias(filename: str, version: str) -> str | None:
208
+ """Map a versioned binary filename to its stable alias, or `None`.
209
+
210
+ Strips the `-{version}-` segment (`papaya-1.2.3-linux-arm64.bin` becomes
211
+ `papaya-linux-arm64.bin`). Returns `None` for filenames that carry no such
212
+ segment or are not compiled binaries, so callers can filter and map in one
213
+ pass.
214
+ """
215
+ marked = f"-{version}-"
216
+ if marked not in filename or not filename.endswith(BINARY_ASSET_SUFFIXES):
217
+ return None
218
+ return filename.replace(marked, "-", 1)
219
+
220
+
221
+ def binary_filename_re(package: str) -> re.Pattern[str]:
222
+ """Match a *package* binary filename, versioned or versionless.
223
+
224
+ Captures `target` and `ext`, both alternations derived from
225
+ {data}`NUITKA_BUILD_TARGETS` so a new build target extends the pattern
226
+ without anyone editing a regex. The release freeze rewrites both spellings
227
+ onto the versioned form through this; `tests/test_platform_keys.py` pins
228
+ the pattern against every target.
229
+ """
230
+ targets = "|".join(sorted(NUITKA_BUILD_TARGETS))
231
+ extensions = "|".join(
232
+ sorted({data["extension"] for data in NUITKA_BUILD_TARGETS.values()})
233
+ )
234
+ return re.compile(
235
+ rf"{re.escape(package)}(?:-[\d.]+)?-"
236
+ rf"(?P<target>{targets})\.(?P<ext>{extensions})"
237
+ )
238
+
239
+
240
+ def pack_binary_assets(dist_dir: Path, version: str) -> list[Path]:
241
+ """Pack a release's upload list, materializing the versionless aliases.
242
+
243
+ Mirrors what the release engine's upload step needs: every file in
244
+ *dist_dir* except the Python distributions (`create-release` already
245
+ uploaded those), plus a byte-identical versionless alias copied beside
246
+ each versioned binary so the stable `releases/latest/download` URLs always
247
+ resolve. Aliases share their sibling's digest, which is what lets artifact
248
+ attestations verify them unchanged and the binaries catalog collapse them
249
+ (see `binaries_page._binary_assets`).
250
+
251
+ Idempotent: re-running overwrites the same aliases with the same bytes.
252
+
253
+ :param dist_dir: Directory holding the compiled binaries and attestation
254
+ bundles downloaded from the build jobs.
255
+ :param version: The release version whose binaries earn aliases.
256
+ :return: Sorted paths to upload, aliases included.
257
+ """
258
+ uploads = {
259
+ path
260
+ for path in dist_dir.iterdir()
261
+ if path.is_file() and not path.name.endswith(PYTHON_DIST_SUFFIXES)
262
+ }
263
+ for path in sorted(uploads):
264
+ alias = versionless_alias(path.name, version)
265
+ if alias is None:
266
+ continue
267
+ alias_path = dist_dir / alias
268
+ shutil.copy2(path, alias_path)
269
+ uploads.add(alias_path)
270
+ return sorted(uploads)
271
+
272
+
171
273
  BINARY_AFFECTING_PATHS: Final[tuple[str, ...]] = (
172
274
  ".github/workflows/_release-engine.yaml",
173
275
  ".github/workflows/release.yaml",
@@ -248,8 +350,18 @@ MACHO_FAT_MAGICS: Final[frozenset[int]] = frozenset((0xCAFEBABE, 0xCAFEBABF))
248
350
  """Big-endian magics of universal (fat) Mach-O containers, 32- and 64-bit."""
249
351
 
250
352
  LC_VERSION_MIN_MACOSX: Final[int] = 0x24
353
+ """Mach-O load command carrying the minimum macOS version (pre-10.14 SDKs)."""
354
+
251
355
  LC_BUILD_VERSION: Final[int] = 0x32
356
+ """Mach-O load command carrying the platform and minimum OS (10.14+ SDKs)."""
357
+
252
358
  MACHO_PLATFORM_MACOS: Final[int] = 1
359
+ """`platform` field value naming macOS inside an `LC_BUILD_VERSION` command."""
360
+
361
+ _PE_PROBE_BYTES: Final[int] = 65536
362
+ """Upper bound read when probing for a PE header: enough to cover any
363
+ real-world DOS-stub offset to the COFF header without reading the whole
364
+ binary."""
253
365
 
254
366
 
255
367
  def _version_key(version: str) -> tuple[int, ...]:
@@ -352,7 +464,7 @@ def _macho_info(path: Path) -> tuple[set[int], str | None]:
352
464
  def _pe_machine(path: Path) -> int | None:
353
465
  """Return the COFF machine type of a PE executable, `None` if not PE."""
354
466
  with path.open("rb") as stream:
355
- head = stream.read(65536)
467
+ head = stream.read(_PE_PROBE_BYTES)
356
468
  if head[:2] != b"MZ" or len(head) < 0x40:
357
469
  return None
358
470
  pe_offset = struct.unpack_from("<I", head, 0x3C)[0]
@@ -47,6 +47,15 @@ if TYPE_CHECKING:
47
47
  ISSUE_TITLE = "Broken links"
48
48
  """Issue title used for the combined broken links report."""
49
49
 
50
+ LYCHEE_BROKEN_LINKS_EXIT = 2
51
+ """The one lychee exit code that reports on the links rather than on the run.
52
+
53
+ Lychee exits 0 on success, 1 on an unexpected failure, 2 when it found broken
54
+ links, and 3 on a config error. Only 2 is a verdict about the links; 1 and 3 say
55
+ the run itself did not complete, so neither "broken links found" nor "no broken
56
+ links" can be claimed from them.
57
+ """
58
+
50
59
  LYCHEE_DEFAULT_BODY = Path("./lychee/out.md")
51
60
  """Default output path used by the lychee-action GitHub Action."""
52
61
 
@@ -55,7 +64,7 @@ SPHINX_DEFAULT_OUTPUT = Path("./docs/_linkcheck/output.json")
55
64
 
56
65
 
57
66
  # ---------------------------------------------------------------------------
58
- # Sphinx linkcheck parsing, filtering, and report generation
67
+ # Sphinx linkcheck parsing, filtering, and report generation.
59
68
  # ---------------------------------------------------------------------------
60
69
 
61
70
 
@@ -170,7 +179,7 @@ def generate_markdown_report(
170
179
 
171
180
 
172
181
  # ---------------------------------------------------------------------------
173
- # Label selection
182
+ # Label selection.
174
183
  # ---------------------------------------------------------------------------
175
184
 
176
185
 
@@ -186,7 +195,7 @@ def get_label(repo_name: str) -> str:
186
195
 
187
196
 
188
197
  # ---------------------------------------------------------------------------
189
- # Combined broken links issue (Lychee + Sphinx linkcheck)
198
+ # Combined broken links issue (Lychee + Sphinx linkcheck).
190
199
  # ---------------------------------------------------------------------------
191
200
 
192
201
 
@@ -230,7 +239,6 @@ def manage_combined_broken_links_issue(
230
239
  in the Sphinx report. Auto-composed from {attr}`Metadata.repo_url
231
240
  <repomatic.metadata.Metadata.repo_url>` and {attr}`Metadata.sha
232
241
  <repomatic.metadata.Metadata.sha>`.
233
- :raises ValueError: If lychee exit code is not 0, 2, or `None`.
234
242
  :raises ValueError: If `repo_name` cannot be determined.
235
243
  """
236
244
  # Shared Metadata instance for all CI context lookups.
@@ -270,20 +278,27 @@ def manage_combined_broken_links_issue(
270
278
  sphinx_source_url = f"{md.repo_url}/blob/{md.sha}/docs"
271
279
  logging.info(f"Auto-composed source URL: {sphinx_source_url}")
272
280
 
273
- # Interpret lychee exit code.
274
- # Exit codes: 0 = success, 1 = unexpected failure, 2 = broken links,
275
- # 3 = config error. Only treat as "broken links found" when lychee
276
- # produced an output file with actual content. A non-zero exit code
277
- # without output (e.g., config error, transient failure) should not
278
- # create an issue that misleadingly says "No broken links found."
281
+ # Interpret lychee's exit code. Only LYCHEE_BROKEN_LINKS_EXIT reports on the
282
+ # links themselves; every other non-zero code reports that the run failed,
283
+ # and says nothing either way about the links.
279
284
  lychee_has_broken = False
285
+ lychee_failed = False
280
286
  if lychee_exit_code is not None:
281
- if lychee_exit_code != 0 and lychee_body_file is not None:
282
- lychee_has_broken = True
287
+ if lychee_exit_code == LYCHEE_BROKEN_LINKS_EXIT:
288
+ # An output file is required: a broken-links verdict with nothing to
289
+ # show would file an issue whose body claims the links are fine.
290
+ if lychee_body_file is not None:
291
+ lychee_has_broken = True
292
+ else:
293
+ logging.warning(
294
+ f"Lychee exit code {lychee_exit_code} but no output file found. "
295
+ "Skipping broken links report."
296
+ )
283
297
  elif lychee_exit_code != 0:
298
+ lychee_failed = True
284
299
  logging.warning(
285
- f"Lychee exit code {lychee_exit_code} but no output file found. "
286
- "Skipping broken links report."
300
+ f"Lychee failed with exit code {lychee_exit_code}, which reports a"
301
+ " run failure rather than broken links."
287
302
  )
288
303
  logging.info(
289
304
  f"Lychee exit code {lychee_exit_code}: "
@@ -314,6 +329,11 @@ def manage_combined_broken_links_issue(
314
329
  lychee_content = sanitize_markdown_mentions(
315
330
  lychee_body_file.read_text(encoding="UTF-8").strip(),
316
331
  )
332
+ elif lychee_failed:
333
+ lychee_content = (
334
+ f"Lychee did not complete (exit code {lychee_exit_code}), so the"
335
+ " links it covers were not checked this run."
336
+ )
317
337
  else:
318
338
  lychee_content = "No broken links found."
319
339
  lychee_section = f"## Lychee\n\n{lychee_content}"