repomatic 7.7.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 (172) hide show
  1. {repomatic-7.7.0 → repomatic-7.8.0}/PKG-INFO +8 -6
  2. {repomatic-7.7.0 → repomatic-7.8.0}/pyproject.toml +6 -5
  3. {repomatic-7.7.0 → repomatic-7.8.0}/pyproject.toml.orig +14 -5
  4. {repomatic-7.7.0 → repomatic-7.8.0}/readme.md +6 -4
  5. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/__init__.py +2 -2
  6. repomatic-7.8.0/repomatic/attestation.py +189 -0
  7. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/changelog.py +167 -36
  8. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/cli.py +55 -0
  9. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/_release-engine.yaml +59 -41
  10. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/autofix.yaml +47 -44
  11. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/cancel-runs.yaml +1 -1
  12. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/changelog.yaml +14 -14
  13. repomatic-7.8.0/repomatic/data/coverage.toml +24 -0
  14. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/debug.yaml +1 -1
  15. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/docs.yaml +3 -3
  16. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/labels.yaml +5 -5
  17. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/lint.yaml +8 -8
  18. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/pytest.toml +4 -5
  19. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/release.yaml +1 -1
  20. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/skills/babysit-ci/SKILL.md +4 -0
  21. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/skills/repomatic-audit/SKILL.md +4 -4
  22. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/skills/repomatic-ship/SKILL.md +5 -3
  23. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/tests.yaml +12 -55
  24. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/unsubscribe.yaml +1 -1
  25. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/github/releases.py +11 -2
  26. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/http.py +11 -1
  27. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/lint_repo.py +83 -1
  28. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/metadata.py +0 -23
  29. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/prepare_release.py +30 -6
  30. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/pypi.py +17 -3
  31. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/registry.py +35 -34
  32. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/version_sync.py +1 -1
  33. repomatic-7.7.0/repomatic/data/codecov.yaml +0 -6
  34. {repomatic-7.7.0 → repomatic-7.8.0}/license +0 -0
  35. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/__main__.py +0 -0
  36. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/awesome_toc.py +0 -0
  37. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/binaries_page.py +0 -0
  38. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/binary.py +0 -0
  39. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/broken_links.py +0 -0
  40. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/bundle.py +0 -0
  41. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/cache.py +0 -0
  42. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/checksums.py +0 -0
  43. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/compat.py +0 -0
  44. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/config.py +0 -0
  45. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/__init__.py +0 -0
  46. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/action-publish-pypi.yaml +0 -0
  47. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/agent-grunt-qa.md +0 -0
  48. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/agent-qa-engineer.md +0 -0
  49. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/agent-sphinx-docs.md +0 -0
  50. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/autolock.yaml +0 -0
  51. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/awesome_template/.github/ISSUE_TEMPLATE/config.yml +0 -0
  52. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/awesome_template/.github/ISSUE_TEMPLATE/new-link.yaml +0 -0
  53. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/awesome_template/.github/code-of-conduct.md +0 -0
  54. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/awesome_template/.github/contributing.md +0 -0
  55. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/awesome_template/.github/contributing.zh.md +0 -0
  56. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/awesome_template/.github/funding.yml +0 -0
  57. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/awesome_template/.github/pull_request_template.md +0 -0
  58. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/awesome_template/__init__.py +0 -0
  59. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/awesome_template/license +0 -0
  60. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/bumpversion.toml +0 -0
  61. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/labeller-content-based.yaml +0 -0
  62. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/labeller-file-based.yaml +0 -0
  63. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/labels.toml +0 -0
  64. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/lychee.toml +0 -0
  65. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/mdformat.toml +0 -0
  66. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/mypy.toml +0 -0
  67. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/ruff.toml +0 -0
  68. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/skills/av-false-positive/SKILL.md +0 -0
  69. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/skills/awesome-triage/SKILL.md +0 -0
  70. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/skills/benchmark-update/SKILL.md +0 -0
  71. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/skills/brand-assets/SKILL.md +0 -0
  72. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/skills/file-bug-report/SKILL.md +0 -0
  73. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/skills/github-housekeeping/SKILL.md +0 -0
  74. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/skills/repomatic-changelog/SKILL.md +0 -0
  75. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/skills/repomatic-deps/SKILL.md +0 -0
  76. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/skills/repomatic-init/SKILL.md +0 -0
  77. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/skills/repomatic-topics/SKILL.md +0 -0
  78. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/skills/sphinx-docs-sync/SKILL.md +0 -0
  79. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/skills/translation-sync/SKILL.md +0 -0
  80. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/skills/upstream-audit/SKILL.md +0 -0
  81. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/typos.toml +0 -0
  82. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/uv.toml +0 -0
  83. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/vt-trend-chart.js +0 -0
  84. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/yamllint.yaml +0 -0
  85. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/data/zizmor.yaml +0 -0
  86. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/dep_graph.py +0 -0
  87. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/dep_report.py +0 -0
  88. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/dep_sources.py +0 -0
  89. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/docs.py +0 -0
  90. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/frontmatter.py +0 -0
  91. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/git_ops.py +0 -0
  92. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/github/__init__.py +0 -0
  93. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/github/actions.py +0 -0
  94. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/github/dev_release.py +0 -0
  95. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/github/gh.py +0 -0
  96. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/github/issue.py +0 -0
  97. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/github/matrix.py +0 -0
  98. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/github/pr_body.py +0 -0
  99. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/github/release_sync.py +0 -0
  100. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/github/sponsor.py +0 -0
  101. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/github/status.py +0 -0
  102. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/github/token.py +0 -0
  103. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/github/unsubscribe.py +0 -0
  104. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/github/workflow_sync.py +0 -0
  105. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/gitignore.py +0 -0
  106. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/humanize.py +0 -0
  107. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/images.py +0 -0
  108. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/init_project.py +0 -0
  109. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/labels.py +0 -0
  110. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/mailmap.py +0 -0
  111. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/matrix_axes.py +0 -0
  112. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/npm.py +0 -0
  113. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/plugin.py +0 -0
  114. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/py.typed +0 -0
  115. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/pyproject.py +0 -0
  116. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/setup_guide.py +0 -0
  117. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/sync_ops.py +0 -0
  118. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/__init__.py +0 -0
  119. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/available-admonition.md +0 -0
  120. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/broken-links-issue.md +0 -0
  121. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/bump-version.md +0 -0
  122. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/detect-squash-merge.md +0 -0
  123. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/development-warning.md +0 -0
  124. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/fix-changelog.md +0 -0
  125. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/fix-typos.md +0 -0
  126. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/fix-vulnerable-deps.md +0 -0
  127. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/format-images.md +0 -0
  128. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/format-json.md +0 -0
  129. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/format-markdown.md +0 -0
  130. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/format-pyproject.md +0 -0
  131. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/format-python.md +0 -0
  132. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/format-shell.md +0 -0
  133. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/generated-footer.md +0 -0
  134. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/github-releases.md +0 -0
  135. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/immutable-releases.md +0 -0
  136. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/pr-metadata.md.noformat +0 -0
  137. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/prepare-release.md.noformat +0 -0
  138. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/refresh-tip.md +0 -0
  139. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/release-notes.md +0 -0
  140. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/release-sync-report.md +0 -0
  141. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/setup-guide-branch-ruleset.md +0 -0
  142. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/setup-guide-dependabot.md +0 -0
  143. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/setup-guide-fork-pr-approval.md +0 -0
  144. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/setup-guide-notifications-pat.md +0 -0
  145. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/setup-guide-pages-source.md +0 -0
  146. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/setup-guide-pypi-trusted-publisher.md +0 -0
  147. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/setup-guide-sha-pinning-required.md +0 -0
  148. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/setup-guide-token.md +0 -0
  149. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/setup-guide-verify.md +0 -0
  150. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/setup-guide-virustotal.md +0 -0
  151. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/setup-guide.md +0 -0
  152. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/sync-action-pins.md +0 -0
  153. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/sync-bumpversion.md +0 -0
  154. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/sync-dep-sources.md +0 -0
  155. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/sync-gitignore.md +0 -0
  156. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/sync-mailmap.md +0 -0
  157. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/sync-repomatic.md +0 -0
  158. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/sync-tool-versions.md +0 -0
  159. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/sync-uv-lock.md +0 -0
  160. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/sync-workflow-pins.md +0 -0
  161. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/unavailable-admonition.md +0 -0
  162. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/unsubscribe-phase1.md +0 -0
  163. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/unsubscribe-phase2.md +0 -0
  164. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/update-dep-graph.md +0 -0
  165. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/update-docs.md +0 -0
  166. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/templates/yanked-admonition.md +0 -0
  167. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/tool_registry.py +0 -0
  168. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/tool_runner.py +0 -0
  169. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/tool_runner_page.py +0 -0
  170. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/uv.py +0 -0
  171. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/virustotal.py +0 -0
  172. {repomatic-7.7.0 → repomatic-7.8.0}/repomatic/vulnerable_deps.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: repomatic
3
- Version: 7.7.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.7.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.7.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.7.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.7.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.7.0"
159
+ product-version = "7.8.0"
160
160
  windows-icon-from-ico = "docs/assets/icon.ico"
161
161
 
162
162
  [tool.ruff]
@@ -246,10 +246,11 @@ branch = true
246
246
  source = ["repomatic"]
247
247
 
248
248
  [tool.coverage.report]
249
+ fail_under = 80
249
250
  precision = 2
250
251
 
251
252
  [tool.bumpversion]
252
- current_version = "7.7.0"
253
+ current_version = "7.8.0"
253
254
  parse = '(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(\.dev(?P<dev>\d+))?'
254
255
  serialize = [
255
256
  "{major}.{minor}.{patch}.dev{dev}",
@@ -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.7.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.7.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.7.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.7.0"
272
+ product-version = "7.8.0"
273
273
  windows-icon-from-ico = "docs/assets/icon.ico"
274
274
 
275
275
  [tool.ruff]
@@ -395,10 +395,19 @@ xfail_strict = true
395
395
  # https://coverage.readthedocs.io/en/latest/config.html
396
396
  run.branch = true
397
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
398
407
  report.precision = 2
399
408
 
400
409
  [tool.bumpversion]
401
- current_version = "7.7.0"
410
+ current_version = "7.8.0"
402
411
  # Parse versions with an optional .devN suffix (PEP 440).
403
412
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(\\.dev(?P<dev>\\d+))?"
404
413
  serialize = [
@@ -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.7.0"
21
- __git_tag_sha__ = "2c6e58c0d2ec44e627868095eb4c22b98683c463"
20
+ __version__ = "7.8.0"
21
+ __git_tag_sha__ = "ab43527b67361553b1c8539e8f3d9e6e2013b98c"
@@ -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})
@@ -733,6 +733,64 @@ class Changelog:
733
733
  return True
734
734
 
735
735
 
736
+ def _load_pypi_releases(
737
+ package: str | None, history: Sequence[str], *, force_refresh: bool = False
738
+ ) -> dict[str, PyPIRelease]:
739
+ """Load a package's PyPI releases, merged with its former names.
740
+
741
+ A renamed project keeps its old releases under the old name, so the
742
+ changelog's early versions resolve only through
743
+ `[tool.repomatic] pypi.package-history`. The current package wins on
744
+ version collisions.
745
+
746
+ :param package: Current PyPI package name; empty or `None` skips PyPI.
747
+ :param history: Former package names, oldest-first.
748
+ :param force_refresh: Ignore cached responses and re-fetch.
749
+ :return: Dict mapping version strings to {class}`PyPIRelease` tuples.
750
+ """
751
+ releases: dict[str, PyPIRelease] = {}
752
+ if package:
753
+ releases = get_pypi_release_dates(package, force_refresh=force_refresh)
754
+ for former_package in history:
755
+ former_data = get_pypi_release_dates(
756
+ former_package, force_refresh=force_refresh
757
+ )
758
+ if former_data:
759
+ logging.info(
760
+ f"Using PyPI history for {former_package!r}"
761
+ f" ({len(former_data)} releases found)."
762
+ )
763
+ for version, release in former_data.items():
764
+ releases.setdefault(version, release)
765
+ return releases
766
+
767
+
768
+ def _is_platform_gap(
769
+ parsed: Version,
770
+ on_platform: bool,
771
+ platform_known: bool,
772
+ first_version: Version | None,
773
+ ) -> bool:
774
+ """Whether *parsed* is missing from a platform it should appear on.
775
+
776
+ A gap is an absence *after* the platform's first release: versions
777
+ predating it were never expected there, so they are not warned about.
778
+
779
+ :param parsed: The version under test.
780
+ :param on_platform: Whether the lookup found it on the platform.
781
+ :param platform_known: Whether the platform is addressable at all (a
782
+ package name for PyPI, a repository URL for GitHub).
783
+ :param first_version: Earliest version seen on that platform.
784
+ :return: `True` when the version is a genuine gap.
785
+ """
786
+ return (
787
+ not on_platform
788
+ and platform_known
789
+ and first_version is not None
790
+ and parsed >= first_version
791
+ )
792
+
793
+
736
794
  def _platform_admonition(
737
795
  template: str, version: str, verb: str, platforms: Sequence[str]
738
796
  ) -> str:
@@ -985,33 +1043,20 @@ def lint_changelog_dates(
985
1043
  if package is None:
986
1044
  package = get_project_name()
987
1045
 
988
- # Fetch all PyPI release dates in a single API call.
989
- pypi_data: dict[str, PyPIRelease] = {}
990
- if package:
991
- pypi_data = get_pypi_release_dates(package)
992
- if pypi_data:
993
- logging.info(
994
- f"Using PyPI as reference for {package!r}"
995
- f" ({len(pypi_data)} releases found)."
996
- )
997
- else:
998
- logging.info(
999
- f"Package {package!r} not found on PyPI, falling back to git tags."
1000
- )
1001
- else:
1046
+ # Fetch all PyPI release dates in a single API call, merged with the
1047
+ # releases of any former package name (for renamed projects).
1048
+ pypi_data = _load_pypi_releases(package, pypi_package_history)
1049
+ if not package:
1002
1050
  logging.info("No package name detected, falling back to git tags.")
1003
-
1004
- # Merge releases from former package names (for renamed projects).
1005
- for former_package in pypi_package_history:
1006
- former_data = get_pypi_release_dates(former_package)
1007
- if former_data:
1008
- logging.info(
1009
- f"Using PyPI history for {former_package!r}"
1010
- f" ({len(former_data)} releases found)."
1011
- )
1012
- # Current package wins on version collisions.
1013
- for v, rel in former_data.items():
1014
- pypi_data.setdefault(v, rel)
1051
+ elif pypi_data:
1052
+ logging.info(
1053
+ f"Using PyPI as reference for {package!r}"
1054
+ f" ({len(pypi_data)} releases found)."
1055
+ )
1056
+ else:
1057
+ logging.info(
1058
+ f"Package {package!r} not found on PyPI, falling back to git tags."
1059
+ )
1015
1060
 
1016
1061
  use_pypi = bool(pypi_data)
1017
1062
  has_mismatch = False
@@ -1051,6 +1096,98 @@ def lint_changelog_dates(
1051
1096
  first_github_version = min(Version(v) for v in github_releases)
1052
1097
  logging.info(f"First GitHub version: {first_github_version}")
1053
1098
 
1099
+ def retracted_versions() -> set[str]:
1100
+ """Versions whose section claims availability the lookups now deny.
1101
+
1102
+ Reads the enclosing scope on every call, so re-running it after a
1103
+ forced refresh answers against the fresh data. An available platform
1104
+ is rendered as a markdown link and a missing one as a bare label, so
1105
+ the `[` prefix is what separates a claim of presence from one of
1106
+ absence.
1107
+ """
1108
+ found = set()
1109
+ for candidate, _candidate_date in releases:
1110
+ existing = changelog.decompose_version(candidate).availability_admonition
1111
+ parsed_candidate = Version(candidate)
1112
+ drops_pypi = (
1113
+ _is_platform_gap(
1114
+ parsed_candidate,
1115
+ candidate in pypi_data,
1116
+ bool(package),
1117
+ first_pypi_version,
1118
+ )
1119
+ and f"[{PYPI_LABEL}](" in existing
1120
+ )
1121
+ drops_github = (
1122
+ _is_platform_gap(
1123
+ parsed_candidate,
1124
+ candidate in github_releases,
1125
+ bool(repo_url),
1126
+ first_github_version,
1127
+ )
1128
+ and f"[{GITHUB_LABEL}](" in existing
1129
+ )
1130
+ if drops_pypi or drops_github:
1131
+ found.add(candidate)
1132
+ return found
1133
+
1134
+ # Retraction gate: never demote an existing "is available" claim to
1135
+ # "is **not available**" on cached data. Both lookups are TTL-cached for a
1136
+ # day, so a snapshot taken before a release landed reports that release as
1137
+ # missing, which is indistinguishable here from one that was never
1138
+ # published. The sanity gates further down catch only a *wholly* empty
1139
+ # result; a stale-but-populated snapshot walks past them, and the window
1140
+ # where it lies is exactly the hours after a release, when this command is
1141
+ # most likely to run. Adding availability stays cheap and cached; dropping
1142
+ # it has to be confirmed live. Running before the date loop means the
1143
+ # spurious "not found on PyPI" warning goes away with it.
1144
+ retracted = retracted_versions()
1145
+ if retracted:
1146
+ logging.warning(
1147
+ f"Cached lookups would retract availability for"
1148
+ f" {', '.join(sorted(retracted))}; re-confirming live."
1149
+ )
1150
+ pypi_data = _load_pypi_releases(
1151
+ package, pypi_package_history, force_refresh=True
1152
+ )
1153
+ first_pypi_version = min(Version(v) for v in pypi_data) if pypi_data else None
1154
+ use_pypi = bool(pypi_data)
1155
+ if repo_url:
1156
+ try:
1157
+ github_releases = get_github_releases(repo_url, force_refresh=True)
1158
+ except GitHubReleasesUnavailable as exc:
1159
+ logging.warning(f"Confirming GitHub lookup failed: {exc}")
1160
+ if fix:
1161
+ msg = (
1162
+ f"Refusing to rewrite changelog: cached data would drop"
1163
+ f" the availability of {', '.join(sorted(retracted))},"
1164
+ f" and the confirming GitHub lookup failed ({exc})."
1165
+ f" Re-run when the GitHub API is reachable."
1166
+ )
1167
+ logging.error(msg)
1168
+ emit_annotation(AnnotationLevel.ERROR, msg)
1169
+ return 2
1170
+ else:
1171
+ first_github_version = (
1172
+ min(Version(v) for v in github_releases)
1173
+ if github_releases
1174
+ else None
1175
+ )
1176
+ still_missing = retracted_versions()
1177
+ if still_missing:
1178
+ # Confirmed against the live APIs: the release really is gone (a
1179
+ # deleted GitHub release, a removed PyPI file). Retracting is then
1180
+ # the correct repair, not a stale-cache artifact.
1181
+ logging.warning(
1182
+ f"Confirmed live: {', '.join(sorted(still_missing))} no longer"
1183
+ " published where the changelog claims."
1184
+ )
1185
+ else:
1186
+ logging.info(
1187
+ "Live lookups confirm the existing availability claims;"
1188
+ " the cache was stale."
1189
+ )
1190
+
1054
1191
  # Detect orphaned versions: present in external sources but missing
1055
1192
  # from the changelog.
1056
1193
  tag_versions = get_all_version_tags()
@@ -1321,17 +1458,11 @@ def lint_changelog_dates(
1321
1458
  # Build the WARNING admonition for platforms where missing.
1322
1459
  # Only warn about gaps: versions that postdate the first
1323
1460
  # release on that platform but are absent from it.
1324
- pypi_gap = (
1325
- not on_pypi
1326
- and bool(package)
1327
- and first_pypi_version is not None
1328
- and parsed >= first_pypi_version
1461
+ pypi_gap = _is_platform_gap(
1462
+ parsed, on_pypi, bool(package), first_pypi_version
1329
1463
  )
1330
- github_gap = (
1331
- not on_github
1332
- and bool(repo_url)
1333
- and first_github_version is not None
1334
- and parsed >= first_github_version
1464
+ github_gap = _is_platform_gap(
1465
+ parsed, on_github, bool(repo_url), first_github_version
1335
1466
  )
1336
1467
  warning = build_unavailable_admonition(
1337
1468
  version,