repomatic 6.26.0__tar.gz → 6.28.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 (152) hide show
  1. {repomatic-6.26.0 → repomatic-6.28.0}/PKG-INFO +2 -2
  2. {repomatic-6.26.0 → repomatic-6.28.0}/pyproject.toml +47 -48
  3. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/__init__.py +2 -2
  4. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/cli.py +196 -47
  5. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/config.py +24 -0
  6. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/_release-engine.yaml +70 -65
  7. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/autofix.yaml +39 -35
  8. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/changelog.yaml +14 -14
  9. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/debug.yaml +1 -1
  10. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/docs.yaml +3 -3
  11. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/labels.yaml +5 -5
  12. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/lint.yaml +7 -7
  13. repomatic-6.28.0/repomatic/data/lychee.toml +15 -0
  14. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/renovate.json5 +2 -2
  15. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/renovate.yaml +3 -3
  16. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/skill-awesome-triage.md +2 -0
  17. repomatic-6.28.0/repomatic/data/skill-repomatic-changelog.md +61 -0
  18. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/skill-repomatic-ship.md +6 -4
  19. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/tests.yaml +6 -3
  20. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/unsubscribe.yaml +1 -1
  21. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/github/advisories.py +1 -1
  22. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/github/matrix.py +9 -6
  23. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/metadata.py +53 -10
  24. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/myst_docstrings.py +5 -1
  25. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/test_matrix.py +29 -4
  26. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/test_plan.py +59 -2
  27. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/tool_runner.py +101 -14
  28. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/uv.py +169 -10
  29. repomatic-6.26.0/repomatic/data/lychee.toml +0 -15
  30. repomatic-6.26.0/repomatic/data/skill-repomatic-changelog.md +0 -61
  31. {repomatic-6.26.0 → repomatic-6.28.0}/license +0 -0
  32. {repomatic-6.26.0 → repomatic-6.28.0}/readme.md +0 -0
  33. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/__main__.py +0 -0
  34. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/binary.py +0 -0
  35. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/broken_links.py +0 -0
  36. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/cache.py +0 -0
  37. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/changelog.py +0 -0
  38. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/checksums.py +0 -0
  39. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/__init__.py +0 -0
  40. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/action-publish-pypi.yaml +0 -0
  41. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/agent-grunt-qa.md +0 -0
  42. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/agent-qa-engineer.md +0 -0
  43. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/agent-sphinx-docs.md +0 -0
  44. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/autolock.yaml +0 -0
  45. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/awesome_template/.github/ISSUE_TEMPLATE/config.yml +0 -0
  46. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/awesome_template/.github/ISSUE_TEMPLATE/new-link.yaml +0 -0
  47. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/awesome_template/.github/code-of-conduct.md +0 -0
  48. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/awesome_template/.github/contributing.md +0 -0
  49. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/awesome_template/.github/contributing.zh.md +0 -0
  50. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/awesome_template/.github/funding.yml +0 -0
  51. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/awesome_template/.github/pull_request_template.md +0 -0
  52. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/awesome_template/__init__.py +0 -0
  53. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/awesome_template/license +0 -0
  54. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/bumpversion.toml +3 -3
  55. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/cancel-runs.yaml +0 -0
  56. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/codecov.yaml +0 -0
  57. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/labeller-content-based.yaml +0 -0
  58. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/labeller-file-based.yaml +0 -0
  59. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/labels.toml +0 -0
  60. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/mdformat.toml +0 -0
  61. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/mypy.toml +0 -0
  62. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/pytest.toml +0 -0
  63. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/release.yaml +0 -0
  64. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/ruff.toml +0 -0
  65. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/skill-av-false-positive.md +0 -0
  66. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/skill-babysit-ci.md +0 -0
  67. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/skill-benchmark-update.md +0 -0
  68. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/skill-brand-assets.md +0 -0
  69. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/skill-file-bug-report.md +0 -0
  70. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/skill-repomatic-audit.md +0 -0
  71. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/skill-repomatic-deps.md +0 -0
  72. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/skill-repomatic-init.md +0 -0
  73. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/skill-repomatic-topics.md +0 -0
  74. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/skill-sphinx-docs-sync.md +0 -0
  75. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/skill-translation-sync.md +0 -0
  76. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/skill-upstream-audit.md +0 -0
  77. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/typos.toml +0 -0
  78. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/yamllint.yaml +0 -0
  79. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/data/zizmor.yaml +0 -0
  80. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/deps_graph.py +0 -0
  81. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/git_ops.py +0 -0
  82. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/github/__init__.py +0 -0
  83. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/github/actions.py +0 -0
  84. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/github/dev_release.py +0 -0
  85. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/github/gh.py +0 -0
  86. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/github/issue.py +0 -0
  87. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/github/pr.py +0 -0
  88. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/github/pr_body.py +0 -0
  89. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/github/release_sync.py +0 -0
  90. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/github/releases.py +0 -0
  91. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/github/status.py +0 -0
  92. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/github/token.py +0 -0
  93. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/github/unsubscribe.py +0 -0
  94. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/github/workflow_sync.py +0 -0
  95. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/images.py +0 -0
  96. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/init_project.py +0 -0
  97. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/lint_repo.py +0 -0
  98. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/mailmap.py +0 -0
  99. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/myst_converter.py +0 -0
  100. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/py.typed +0 -0
  101. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/pypi.py +0 -0
  102. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/pyproject.py +0 -0
  103. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/registry.py +0 -0
  104. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/release_prep.py +0 -0
  105. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/renovate.py +0 -0
  106. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/rst_to_myst.py +0 -0
  107. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/sponsor.py +0 -0
  108. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/__init__.py +0 -0
  109. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/available-admonition.md +0 -0
  110. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/broken-links-issue.md +0 -0
  111. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/bump-version.md +0 -0
  112. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/detect-squash-merge.md +0 -0
  113. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/development-warning.md +0 -0
  114. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/fix-changelog.md +0 -0
  115. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/fix-typos.md +0 -0
  116. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/fix-vulnerable-deps.md +0 -0
  117. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/format-images.md +0 -0
  118. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/format-json.md +0 -0
  119. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/format-markdown.md +0 -0
  120. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/format-pyproject.md +0 -0
  121. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/format-python.md +0 -0
  122. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/format-shell.md +0 -0
  123. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/generated-footer.md +0 -0
  124. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/github-releases.md +0 -0
  125. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/immutable-releases.md +0 -0
  126. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/pr-metadata.md.noformat +0 -0
  127. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/prepare-release.md +0 -0
  128. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/refresh-tip.md +0 -0
  129. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/release-notes.md +0 -0
  130. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/release-sync-report.md +0 -0
  131. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/renovate-migration.md +0 -0
  132. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/setup-guide-branch-ruleset.md +0 -0
  133. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/setup-guide-dependabot.md +0 -0
  134. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/setup-guide-fork-pr-approval.md +0 -0
  135. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/setup-guide-pages-source.md +0 -0
  136. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/setup-guide-pypi-trusted-publisher.md +0 -0
  137. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/setup-guide-token.md +0 -0
  138. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/setup-guide-verify.md +0 -0
  139. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/setup-guide-virustotal.md +0 -0
  140. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/setup-guide.md +0 -0
  141. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/sync-bumpversion.md +0 -0
  142. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/sync-gitignore.md +0 -0
  143. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/sync-mailmap.md +0 -0
  144. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/sync-repomatic.md +0 -0
  145. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/sync-uv-lock.md +0 -0
  146. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/unavailable-admonition.md +0 -0
  147. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/unsubscribe-phase1.md +0 -0
  148. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/unsubscribe-phase2.md +0 -0
  149. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/update-deps-graph.md +0 -0
  150. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/update-docs.md +0 -0
  151. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/templates/yanked-admonition.md +0 -0
  152. {repomatic-6.26.0 → repomatic-6.28.0}/repomatic/virustotal.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: repomatic
3
- Version: 6.26.0
3
+ Version: 6.28.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,renovate,repository-automation,repository-management,reusable-workflows,software-factory,sphinx-doc,typo,uv,workflow-reusable
6
6
  Author: Kevin Deldycke
@@ -58,7 +58,7 @@ Requires-Dist: wcmatch>=10
58
58
  Requires-Python: >=3.10
59
59
  Project-URL: Changelog, https://github.com/kdeldycke/repomatic/blob/main/changelog.md
60
60
  Project-URL: Documentation, https://kdeldycke.github.io/repomatic
61
- Project-URL: Download, https://github.com/kdeldycke/repomatic/releases/tag/v6.26.0
61
+ Project-URL: Download, https://github.com/kdeldycke/repomatic/releases/tag/v6.28.0
62
62
  Project-URL: Funding, https://github.com/sponsors/kdeldycke
63
63
  Project-URL: Homepage, https://github.com/kdeldycke/repomatic
64
64
  Project-URL: Issues, https://github.com/kdeldycke/repomatic/issues
@@ -5,7 +5,7 @@ requires = [ "uv-build>=0.9" ]
5
5
  [project]
6
6
  # Docs: https://packaging.python.org/en/latest/guides/writing-pyproject-toml/
7
7
  name = "repomatic"
8
- version = "6.26.0"
8
+ version = "6.28.0"
9
9
  description = "🏭 Automate repository maintenance, releases, and CI/CD workflows"
10
10
  readme = "readme.md"
11
11
  keywords = [
@@ -116,18 +116,14 @@ dependencies = [
116
116
  # globbing uses iglob() with FOLLOW for symlink-aware file discovery.
117
117
  "wcmatch>=10",
118
118
  ]
119
-
120
- [project.scripts]
121
- repomatic = "repomatic.__main__:main"
122
-
123
- [project.urls]
124
- "Changelog" = "https://github.com/kdeldycke/repomatic/blob/main/changelog.md"
125
- "Documentation" = "https://kdeldycke.github.io/repomatic"
126
- "Download" = "https://github.com/kdeldycke/repomatic/releases/tag/v6.26.0"
127
- "Funding" = "https://github.com/sponsors/kdeldycke"
128
- "Homepage" = "https://github.com/kdeldycke/repomatic"
129
- "Issues" = "https://github.com/kdeldycke/repomatic/issues"
130
- "Repository" = "https://github.com/kdeldycke/repomatic"
119
+ urls.Changelog = "https://github.com/kdeldycke/repomatic/blob/main/changelog.md"
120
+ urls.Documentation = "https://kdeldycke.github.io/repomatic"
121
+ urls.Download = "https://github.com/kdeldycke/repomatic/releases/tag/v6.28.0"
122
+ urls.Funding = "https://github.com/sponsors/kdeldycke"
123
+ urls.Homepage = "https://github.com/kdeldycke/repomatic"
124
+ urls.Issues = "https://github.com/kdeldycke/repomatic/issues"
125
+ urls.Repository = "https://github.com/kdeldycke/repomatic"
126
+ scripts.repomatic = "repomatic.__main__:main"
131
127
 
132
128
  # Non-runtime development dependency groups.
133
129
  [dependency-groups]
@@ -186,12 +182,15 @@ dependency-groups.docs = { requires-python = ">= 3.14" }
186
182
  # Relative dates are fine here: uv 0.11.8+ writes a sentinel timestamp to
187
183
  # uv.lock for relative spans, so re-resolving never produces churn.
188
184
  exclude-newer = "1 week"
189
- # repomatic is pinned to git main. click-extra 7.19.0 is bypassed so the release
190
- # engine's manpages job picks up the new `--output-dir` CLI flag without waiting
191
- # out the cooldown window; drop the override on the next routine bump. tomlrt
192
- # 1.7.5 ships the silent-empty-dump fix the project depends on; bypass the
193
- # cooldown until the release ages past `exclude-newer`.
194
- exclude-newer-package = { click-extra = "0 day", repomatic = "0 day", tomlrt = "0 day" }
185
+ # Per-package cooldown control. A fixed date holds a package at the version
186
+ # available then (the day after it shipped), so `uv lock --upgrade` keeps that
187
+ # version instead of tracking newer releases. `sync-uv-lock` freezes new
188
+ # bypasses at that date and prunes each entry once its held version ages past
189
+ # `exclude-newer`, returning the package to the normal cooldown. repomatic is
190
+ # pinned to git main (no PyPI release to freeze), so it keeps a permanent
191
+ # "0 day" span. click-extra is held at 7.19.0 (manpages `--output-dir` flag);
192
+ # tomlrt is held at 1.8.2.
193
+ exclude-newer-package = { repomatic = "0 day", tomlrt = "2026-06-16T00:00:00Z" }
195
194
  # Package is at root level, not in "./src/".
196
195
  build-backend.module-root = ""
197
196
 
@@ -204,7 +203,7 @@ copyright = "Kevin Deldycke <kevin@deldycke.com> and contributors. Distributed u
204
203
  file-description = "🏭 Automate repository maintenance, releases, and CI/CD workflows"
205
204
  # Numeric only: Nuitka rejects PEP 440 .devN suffixes. Kept in sync with the
206
205
  # package version by the dedicated [tool.bumpversion] rule that strips the suffix.
207
- file-version = "6.26.0"
206
+ file-version = "6.28.0"
208
207
  include-data-dir = [
209
208
  "repomatic/data/awesome_template=repomatic/data/awesome_template",
210
209
  ]
@@ -222,7 +221,7 @@ include-package-data = [ "click_extra" ]
222
221
  linux-icon = "docs/assets/icon.png"
223
222
  macos-app-icon = "docs/assets/icon.icns"
224
223
  product-name = "Repomatic"
225
- product-version = "6.26.0"
224
+ product-version = "6.28.0"
226
225
  windows-icon-from-ico = "docs/assets/icon.ico"
227
226
 
228
227
  [tool.ruff]
@@ -284,11 +283,26 @@ default.extend-ignore-identifiers-re = [
284
283
  "WARNIN",
285
284
  ]
286
285
 
287
- [tool.pytest]
288
- # https://docs.pytest.org/en/latest/customize.html#pyproject-toml
289
- markers = [
290
- "once: Tests that only need to run once, not across the full CI matrix.",
286
+ [tool.mypy]
287
+ mypy_path = "docs"
288
+ check_untyped_defs = true
289
+ warn_redundant_casts = true
290
+ warn_unused_ignores = true
291
+ warn_return_any = true
292
+ warn_unreachable = true
293
+ pretty = true
294
+ warn_unused_configs = true
295
+ overrides = [
296
+ { module = [
297
+ "bumpversion.*",
298
+ "gitdb.*",
299
+ "py_walk.*",
300
+ "pydriller.*",
301
+ "vt",
302
+ ], ignore_missing_imports = true },
291
303
  ]
304
+
305
+ [tool.pytest]
292
306
  addopts = [
293
307
  "--durations=10",
294
308
  "--cov",
@@ -296,6 +310,10 @@ addopts = [
296
310
  "--numprocesses=auto",
297
311
  "--dist=loadgroup",
298
312
  ]
313
+ # https://docs.pytest.org/en/latest/customize.html#pyproject-toml
314
+ markers = [
315
+ "once: Tests that only need to run once, not across the full CI matrix.",
316
+ ]
299
317
  # Make sure tests that are expected to fail do not resurrect and start working all of a sudden.
300
318
  xfail_strict = true
301
319
 
@@ -306,20 +324,20 @@ run.source = [ "repomatic" ]
306
324
  report.precision = 2
307
325
 
308
326
  [tool.bumpversion]
309
- current_version = "6.26.0"
310
- allow_dirty = true
311
- ignore_missing_files = true
327
+ current_version = "6.28.0"
312
328
  # Parse versions with an optional .devN suffix (PEP 440).
313
329
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(\\.dev(?P<dev>\\d+))?"
314
330
  serialize = [
315
331
  "{major}.{minor}.{patch}.dev{dev}",
316
332
  "{major}.{minor}.{patch}",
317
333
  ]
334
+ ignore_missing_files = true
335
+ allow_dirty = true
336
+ parts.dev.optional_value = "release"
318
337
  # When dev is "release" (its optional value), the .devN suffix is omitted.
319
338
  # Bumping patch/minor/major resets dev to "0", producing X.Y.Z.dev0.
320
339
  # Bumping dev goes from "0" to "release", stripping the suffix for release.
321
340
  parts.dev.values = [ "0", "release" ]
322
- parts.dev.optional_value = "release"
323
341
 
324
342
  [[tool.bumpversion.files]]
325
343
  # Update Python package version in any __init__.py file.
@@ -373,25 +391,6 @@ regex = true
373
391
  search = "date-released: \\d{{4}}-\\d{{2}}-\\d{{2}}"
374
392
  replace = "date-released: {utcnow:%Y-%m-%d}"
375
393
 
376
- [tool.mypy]
377
- check_untyped_defs = true
378
- mypy_path = "docs"
379
- warn_unused_configs = true
380
- warn_redundant_casts = true
381
- warn_unused_ignores = true
382
- warn_return_any = true
383
- warn_unreachable = true
384
- pretty = true
385
- overrides = [
386
- { ignore_missing_imports = true, module = [
387
- "bumpversion.*",
388
- "gitdb.*",
389
- "py_walk.*",
390
- "pydriller.*",
391
- "vt",
392
- ] },
393
- ]
394
-
395
394
  [tool.lychee]
396
395
  # https://lychee.cli.rs/guides/config/
397
396
  exclude = [
@@ -17,5 +17,5 @@
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
- __version__ = "6.26.0"
21
- __git_tag_sha__ = "9b9fa8a1d838c1f28bb9d0bb82fa67084b8a2ca7"
20
+ __version__ = "6.28.0"
21
+ __git_tag_sha__ = "a6458162963b57814a69421fed1f969cbbe1d2b6"
@@ -26,6 +26,7 @@ import sys
26
26
  import tempfile
27
27
  import time
28
28
  from collections import Counter
29
+ from concurrent.futures import ThreadPoolExecutor
29
30
  from pathlib import Path
30
31
  from urllib.request import Request, urlopen
31
32
 
@@ -39,14 +40,17 @@ from click_extra import (
39
40
  EnumChoice,
40
41
  FloatRange,
41
42
  IntRange,
43
+ ParameterSource,
42
44
  ParamType,
43
45
  Section,
44
46
  UsageError,
45
47
  argument,
48
+ context,
46
49
  dir_path,
47
50
  echo,
48
51
  file_path,
49
52
  group,
53
+ jobs_option,
50
54
  option,
51
55
  option_group,
52
56
  pass_context,
@@ -167,7 +171,12 @@ from .sponsor import (
167
171
  is_pull_request,
168
172
  is_sponsor,
169
173
  )
170
- from .test_plan import DEFAULT_TEST_PLAN, SkippedTest, parse_test_plan
174
+ from .test_plan import (
175
+ DEFAULT_TEST_PLAN,
176
+ CLITestCase,
177
+ SkippedTest,
178
+ parse_test_plan,
179
+ )
171
180
  from .tool_runner import (
172
181
  TOOL_REGISTRY,
173
182
  binary_tool_context,
@@ -179,10 +188,12 @@ from .uv import (
179
188
  AdvisorySource,
180
189
  _format_upload_date,
181
190
  build_comparison_urls,
191
+ collect_vulnerable_packages,
182
192
  fetch_release_notes,
183
193
  fix_vulnerable_deps as _fix_vulnerable_deps,
184
194
  format_diff_table,
185
195
  format_release_notes,
196
+ format_vulnerability_table,
186
197
  sync_uv_lock as _sync_uv_lock,
187
198
  )
188
199
  from .virustotal import (
@@ -227,9 +238,21 @@ def prep_path(filepath: Path) -> IO:
227
238
 
228
239
  For non-stdout paths, parent directories are created automatically if they don't
229
240
  exist. This absorbs the `mkdir -p` step that workflows previously had to do.
241
+
242
+ ```{note}
243
+ When stdout is a captured in-memory stream with no backing file descriptor
244
+ (Click's test runner, the Sphinx `{click:run}` directive that live-renders CLI
245
+ output in the docs), `fileno()` raises and we write to the stream directly. Such
246
+ streams are already Python text objects, so the Windows `cp1252` concern does not
247
+ apply: that only bites a real terminal, which always has a descriptor.
248
+ ```
230
249
  """
231
250
  if is_stdout(filepath):
232
- return open(sys.stdout.fileno(), "w", encoding="UTF-8", closefd=False)
251
+ try:
252
+ fd = sys.stdout.fileno()
253
+ except (OSError, ValueError):
254
+ return sys.stdout
255
+ return open(fd, "w", encoding="UTF-8", closefd=False)
233
256
  filepath.parent.mkdir(parents=True, exist_ok=True)
234
257
  return filepath.open("w", encoding="UTF-8")
235
258
 
@@ -749,7 +772,12 @@ def metadata(ctx, format, overwrite, output, list_keys, keys):
749
772
  )
750
773
 
751
774
  if is_stdout(output):
752
- if overwrite:
775
+ # The overwrite flag is moot for stdout. Warn only when the user set it
776
+ # explicitly: firing on the default value warns on every bare stdout run.
777
+ if (
778
+ overwrite
779
+ and ctx.get_parameter_source("overwrite") is not ParameterSource.DEFAULT
780
+ ):
753
781
  logging.warning("Ignore the --overwrite/--force/--replace option.")
754
782
  logging.info(f"Print metadata to {sys.stdout.name}")
755
783
  else:
@@ -766,7 +794,9 @@ def metadata(ctx, format, overwrite, output, list_keys, keys):
766
794
  meta = Metadata()
767
795
 
768
796
  # Output a warning in GitHub runners if metadata are not saved to $GITHUB_OUTPUT.
769
- if is_github_ci():
797
+ # Skip the warning for stdout: writing to stdout is a legitimate use case
798
+ # (human inspection, --format json piping, Sphinx docs rendering) even in CI.
799
+ if is_github_ci() and not is_stdout(output):
770
800
  env_file = os.getenv("GITHUB_OUTPUT")
771
801
  if env_file and Path(env_file) != output:
772
802
  logging.warning(
@@ -1449,6 +1479,7 @@ def sync_mailmap(ctx, source, create_if_missing, destination_mailmap):
1449
1479
  default=False,
1450
1480
  help="Exit instantly on first failed test.",
1451
1481
  )
1482
+ @jobs_option
1452
1483
  @option(
1453
1484
  "-T",
1454
1485
  "--timeout",
@@ -1490,7 +1521,15 @@ def test_plan(
1490
1521
  (--plan-envvar), [tool.repomatic] config, or a built-in default.
1491
1522
  Each test invokes the command with the specified arguments and validates
1492
1523
  the output against expected patterns.
1524
+
1525
+ Cases run in parallel by default (see --jobs): each is an independent
1526
+ process invocation, so they overlap well. Pass --jobs 1 for sequential
1527
+ execution, which lets --exit-on-error stop on the first failure.
1493
1528
  """
1529
+ # click-extra's --jobs option stores its clamped worker count on the
1530
+ # context; it does not drive concurrency itself, so we read and act on it.
1531
+ worker_count = context.get(ctx, context.JOBS, 1)
1532
+
1494
1533
  # Load [tool.repomatic] config for fallback values.
1495
1534
  config = get_tool_config(ctx)
1496
1535
 
@@ -1540,16 +1579,21 @@ def test_plan(
1540
1579
 
1541
1580
  counter = Counter(total=len(test_list), skipped=0, failed=0)
1542
1581
 
1582
+ # Select the cases to run (respecting --select-test), keeping their 1-based
1583
+ # numbers for stable reporting.
1584
+ pending: list[tuple[int, CLITestCase]] = []
1543
1585
  for index, test_case in enumerate(test_list):
1544
1586
  test_number = index + 1
1545
- test_name = f"#{test_number}"
1546
- logging.info(f"Run test {test_name}...")
1547
-
1548
1587
  if select_test and test_number not in select_test:
1549
- logging.warning(f"Test {test_name} skipped by user request.")
1588
+ logging.warning(f"Test #{test_number} skipped by user request.")
1550
1589
  counter["skipped"] += 1
1551
1590
  continue
1591
+ pending.append((test_number, test_case))
1552
1592
 
1593
+ def run_case(item: tuple[int, CLITestCase]) -> tuple[int, str, CLITestCase]:
1594
+ """Run one case, returning its number, outcome, and the case itself."""
1595
+ test_number, test_case = item
1596
+ logging.info(f"Run test #{test_number}...")
1553
1597
  try:
1554
1598
  logging.debug(f"Test case parameters: {test_case}")
1555
1599
  test_case.run_cli_test(
@@ -1558,16 +1602,44 @@ def test_plan(
1558
1602
  default_timeout=timeout,
1559
1603
  )
1560
1604
  except SkippedTest as ex:
1561
- counter["skipped"] += 1
1562
- logging.warning(f"Test {test_name} skipped: {ex}")
1605
+ logging.warning(f"Test #{test_number} skipped: {ex}")
1606
+ return test_number, "skipped", test_case
1563
1607
  except Exception as ex: # noqa: BLE001
1608
+ logging.error(f"Test #{test_number} failed: {ex}")
1609
+ return test_number, "failed", test_case
1610
+ return test_number, "passed", test_case
1611
+
1612
+ def tally(outcome: tuple[int, str, CLITestCase]) -> bool:
1613
+ """Record an outcome in the counters and echo a failure's trace.
1614
+
1615
+ :return: `True` if the case failed.
1616
+ """
1617
+ _, status, test_case = outcome
1618
+ if status == "skipped":
1619
+ counter["skipped"] += 1
1620
+ elif status == "failed":
1564
1621
  counter["failed"] += 1
1565
- logging.error(f"Test {test_name} failed: {ex}")
1566
1622
  if show_trace_on_error and test_case.execution_trace:
1567
1623
  echo(test_case.execution_trace)
1568
- if exit_on_error:
1624
+ return status == "failed"
1625
+
1626
+ if worker_count == 1 or len(pending) <= 1:
1627
+ # Sequential: --exit-on-error can short-circuit on the first failure.
1628
+ for item in pending:
1629
+ if tally(run_case(item)) and exit_on_error:
1569
1630
  logging.debug("Don't continue testing, a failed test was found.")
1570
1631
  ctx.exit(1)
1632
+ else:
1633
+ # Parallel: subprocess.run releases the GIL while the child runs, so
1634
+ # worker threads overlap each case's process spawn and execution. All
1635
+ # selected cases run (--exit-on-error has no effect); executor.map
1636
+ # yields in submission order, keeping traces and counters ordered.
1637
+ logging.info(
1638
+ f"Run {len(pending)} test cases across {worker_count} parallel workers."
1639
+ )
1640
+ with ThreadPoolExecutor(max_workers=worker_count) as executor:
1641
+ for outcome in executor.map(run_case, pending):
1642
+ tally(outcome)
1571
1643
 
1572
1644
  if stats:
1573
1645
  echo(
@@ -2622,9 +2694,22 @@ def verify_binary(target: str, binary_path: Path) -> None:
2622
2694
  echo(f"Binary architecture verified for {target}: {binary_path}")
2623
2695
 
2624
2696
 
2697
+ AUDIT_HEADER_DEFS: tuple[tuple[str, str], ...] = (
2698
+ ("Package", "package"),
2699
+ ("Version", "version"),
2700
+ ("Advisory", "advisory"),
2701
+ ("Fixed", "fixed"),
2702
+ ("Sources", "sources"),
2703
+ )
2704
+ """Column definitions for the `repomatic audit` table."""
2705
+
2706
+ _audit_sort = SortByOption(*AUDIT_HEADER_DEFS, default="package")
2707
+
2708
+
2625
2709
  @repomatic.command(
2626
- short_help="Upgrade packages with known vulnerabilities",
2627
- section=_section_sync,
2710
+ short_help="Report (and optionally fix) vulnerable dependencies",
2711
+ section=_section_lint,
2712
+ params=[_audit_sort],
2628
2713
  )
2629
2714
  @option(
2630
2715
  "--lockfile",
@@ -2638,58 +2723,81 @@ def verify_binary(target: str, binary_path: Path) -> None:
2638
2723
  type=str,
2639
2724
  default=lambda: os.environ.get("GITHUB_REPOSITORY", ""),
2640
2725
  help=(
2641
- "Repository in OWNER/NAME format. Required to query the GitHub"
2642
- " Advisory Database. Defaults to GITHUB_REPOSITORY when set."
2726
+ "Repository in OWNER/NAME format. Enables the GitHub Advisory"
2727
+ " Database source. Defaults to GITHUB_REPOSITORY when set."
2728
+ ),
2729
+ )
2730
+ @option(
2731
+ "--fix/--no-fix",
2732
+ default=False,
2733
+ help=(
2734
+ "Upgrade fixable packages and persist cooldown exemptions"
2735
+ " (mutates uv.lock and pyproject.toml). Default: report only."
2643
2736
  ),
2644
2737
  )
2738
+ @option(
2739
+ "--exit-zero/--no-exit-zero",
2740
+ default=False,
2741
+ help="In report mode, exit 0 even when vulnerabilities are found.",
2742
+ )
2645
2743
  @option(
2646
2744
  "--output",
2647
2745
  type=file_path(writable=True, resolve_path=True, allow_dash=True),
2648
2746
  default=None,
2649
- help="Write a markdown report (vulnerabilities + updates) to this file.",
2747
+ help="Write a markdown report to this file.",
2650
2748
  )
2651
2749
  @output_format_option
2652
2750
  @pass_context
2653
- def fix_vulnerable_deps_cmd(
2751
+ def audit(
2654
2752
  ctx: Context,
2655
2753
  lockfile: Path,
2656
2754
  repo: str,
2755
+ fix: bool,
2756
+ exit_zero: bool,
2657
2757
  output: Path | None,
2658
2758
  output_format: str,
2659
2759
  ) -> None:
2660
- """Detect and upgrade packages with known security vulnerabilities.
2760
+ """Scan locked dependencies for known security vulnerabilities.
2661
2761
 
2662
2762
  \b
2663
- Queries every advisory database enabled in
2763
+ Read-only by default: queries every advisory database enabled in
2664
2764
  [tool.repomatic] vulnerable-deps.sources (default: uv-audit and
2665
- github-advisories), unions and deduplicates the results, then upgrades
2666
- each fixable package with uv lock --upgrade-package and:
2765
+ github-advisories), unions and deduplicates the results, and prints
2766
+ them. The table respects the global --table-format option (github,
2767
+ json, csv, etc.), so --table-format json yields a machine-readable
2768
+ report. Report mode exits 1 when any vulnerability is found (use
2769
+ --exit-zero to override), so it can gate CI.
2770
+
2771
+ \b
2772
+ With --fix, upgrades each fixable package with uv lock --upgrade-package
2773
+ and:
2667
2774
  - bypasses the exclude-newer cooldown for security fixes
2668
2775
  - persists exclude-newer-package entries in pyproject.toml
2669
2776
  - prints a markdown report of vulnerabilities and version changes
2670
2777
 
2671
2778
  \b
2672
2779
  Examples:
2673
- # Scan and fix vulnerabilities (uses GITHUB_REPOSITORY when set)
2674
- repomatic fix-vulnerable-deps
2780
+ # Report known vulnerabilities (read-only)
2781
+ repomatic audit
2675
2782
 
2676
2783
  \b
2677
- # Force a specific repository for the GHSA query
2678
- repomatic fix-vulnerable-deps --repo owner/name
2784
+ # Machine-readable output
2785
+ repomatic --table-format json audit
2679
2786
 
2680
2787
  \b
2681
- # CI: write markdown report as a GitHub Actions step output
2682
- repomatic fix-vulnerable-deps \\
2788
+ # Upgrade fixable packages (mutates uv.lock and pyproject.toml)
2789
+ repomatic audit --fix
2790
+
2791
+ \b
2792
+ # CI autofix job: write the markdown report as a step output
2793
+ repomatic audit --fix --repo owner/name \\
2683
2794
  --output "$GITHUB_OUTPUT" --output-format github-actions
2684
2795
  """
2685
2796
  config = get_tool_config(ctx)
2686
- if not config.vulnerable_deps.sync:
2687
- logging.info(
2688
- "[tool.repomatic] vulnerable-deps.sync is disabled."
2689
- " Skipping vulnerability scan."
2690
- )
2691
- ctx.exit(0)
2692
2797
 
2798
+ # Parse the configured advisory sources, dropping the GitHub Advisory
2799
+ # Database when neither --repo nor GITHUB_REPOSITORY is available to
2800
+ # query it.
2693
2801
  sources: list[AdvisorySource] = []
2694
2802
  for raw in config.vulnerable_deps.sources:
2695
2803
  try:
@@ -2706,28 +2814,67 @@ def fix_vulnerable_deps_cmd(
2706
2814
  )
2707
2815
  sources = [s for s in sources if s is not AdvisorySource.GITHUB_ADVISORIES]
2708
2816
 
2709
- has_fixes, diff_table = _fix_vulnerable_deps(
2817
+ if fix:
2818
+ # vulnerable-deps.sync gates the autofix (mutation), not reporting.
2819
+ if not config.vulnerable_deps.sync:
2820
+ logging.info(
2821
+ "[tool.repomatic] vulnerable-deps.sync is disabled. Skipping --fix."
2822
+ )
2823
+ ctx.exit(0)
2824
+
2825
+ has_fixes, diff_table = _fix_vulnerable_deps(
2826
+ lockfile, repo=repo or None, sources=sources or None
2827
+ )
2828
+ if not has_fixes:
2829
+ echo("No fixable vulnerabilities found.")
2830
+ ctx.exit(0)
2831
+
2832
+ echo("Upgraded vulnerable packages.")
2833
+ if diff_table:
2834
+ echo(diff_table)
2835
+ if output and diff_table:
2836
+ # Keep the github-actions key as `diff_table`: the autofix
2837
+ # workflow's pr-metadata step reads steps.fix.outputs.diff_table.
2838
+ if output_format == "github-actions":
2839
+ content = format_multiline_output("diff_table", diff_table)
2840
+ else:
2841
+ content = diff_table
2842
+ echo(content, file=prep_path(output))
2843
+ ctx.exit(0)
2844
+
2845
+ # Report mode (default, read-only).
2846
+ vulns = collect_vulnerable_packages(
2710
2847
  lockfile, repo=repo or None, sources=sources or None
2711
2848
  )
2712
-
2713
- if not has_fixes:
2714
- echo("No fixable vulnerabilities found.")
2849
+ if not vulns:
2850
+ echo("No known vulnerabilities found.")
2715
2851
  ctx.exit(0)
2716
2852
 
2717
- echo("Upgraded vulnerable packages.")
2718
- if diff_table:
2719
- echo(diff_table)
2853
+ rows = [
2854
+ (
2855
+ v.name,
2856
+ v.current_version,
2857
+ v.advisory_id,
2858
+ v.fixed_version or "unknown",
2859
+ ", ".join(sorted(s.value for s in v.sources)) or "—",
2860
+ )
2861
+ for v in vulns
2862
+ ]
2863
+ ctx.print_table(AUDIT_HEADER_DEFS, rows) # type: ignore[attr-defined]
2720
2864
 
2721
- if output and diff_table:
2865
+ if output:
2866
+ markdown = format_vulnerability_table(vulns)
2722
2867
  if output_format == "github-actions":
2723
- content = format_multiline_output("diff_table", diff_table)
2868
+ content = format_multiline_output("vuln_table", markdown)
2724
2869
  else:
2725
- content = diff_table
2870
+ content = markdown
2726
2871
  echo(content, file=prep_path(output))
2727
2872
 
2873
+ ctx.exit(0 if exit_zero else 1)
2874
+
2728
2875
 
2729
2876
  @repomatic.command(
2730
- short_help="Re-lock dependencies and prune stale cooldown overrides",
2877
+ short_help="Re-lock dependencies and roll cooldown overrides forward",
2731
2878
  section=_section_sync,
2732
2879
  )
2733
2880
  @option(
@@ -2766,8 +2913,10 @@ def sync_uv_lock_cmd(
2766
2913
 
2767
2914
  \b
2768
2915
  Wraps uv lock --upgrade and:
2769
- - prunes stale exclude-newer-package entries from pyproject.toml
2770
- whose locked version has aged past the exclude-newer cutoff
2916
+ - prunes exclude-newer-package entries from pyproject.toml whose held
2917
+ version has aged past the exclude-newer cutoff, then freezes the
2918
+ survivors at their locked version (a fixed date) so the upgrade
2919
+ holds them instead of tracking newer releases
2771
2920
  - prints a table of updated packages with upload dates
2772
2921
  - optionally fetches release notes from GitHub (markdown)
2773
2922
 
@@ -284,6 +284,30 @@ class TestMatrixConfig:
284
284
  Additive to the upstream default excludes.
285
285
  """
286
286
 
287
+ full_include: list[dict[str, str]] = field(
288
+ default_factory=list,
289
+ metadata={"click_extra.config_path": "full-include"},
290
+ )
291
+ """Full-matrix-only job rows, added as standalone matrix combinations.
292
+
293
+ Each entry is a dict of GitHub Actions matrix keys fully describing one job
294
+ (like `{"os": "ubuntu-24.04-arm", "python-version": "3.10",
295
+ "click-version": "8.3.1"}`). Unlike `include`, these are appended as
296
+ independent rows of the full matrix, never merged into the base
297
+ cross-product, so a cell can't overwrite a shipped-config job that shares
298
+ its `os` and `python-version`. Keys left out inherit the matrix defaults
299
+ (the single-key `include` entries, plus `state: stable`), so a cell lists
300
+ only what differs from the shipped configuration.
301
+
302
+ Use this for heterogeneous coverage, like pinning each release of a
303
+ dependency to its own runner and Python, where carving the same shape from
304
+ the base cross-product with `exclude` would take many rules. Like
305
+ `variations` and `unstable`, it touches the full matrix only; the PR matrix
306
+ stays a curated reduced set. Adding any entry makes the full matrix emit as
307
+ a flat job list (`{"include": [...]}`), which GitHub runs verbatim with no
308
+ cross-product expansion.
309
+ """
310
+
287
311
  include: list[dict[str, str]] = field(default_factory=list)
288
312
  """Extra include directives applied to both full and PR test matrices.
289
313