repomatic 7.7.0__tar.gz → 7.9.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.
- {repomatic-7.7.0 → repomatic-7.9.0}/PKG-INFO +8 -6
- {repomatic-7.7.0 → repomatic-7.9.0}/pyproject.toml +6 -5
- {repomatic-7.7.0 → repomatic-7.9.0}/pyproject.toml.orig +14 -5
- {repomatic-7.7.0 → repomatic-7.9.0}/readme.md +6 -4
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/__init__.py +2 -2
- repomatic-7.9.0/repomatic/attestation.py +189 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/changelog.py +167 -36
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/cli.py +103 -3
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/config.py +15 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/_release-engine.yaml +192 -48
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/agent-sphinx-docs.md +10 -7
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/autofix.yaml +51 -47
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/autolock.yaml +4 -3
- repomatic-7.9.0/repomatic/data/awesome_template/.github/code-of-conduct.md +43 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/cancel-runs.yaml +5 -4
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/changelog.yaml +18 -17
- repomatic-7.9.0/repomatic/data/coverage.toml +24 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/debug.yaml +6 -5
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/docs.yaml +23 -7
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/labels.yaml +10 -9
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/lint.yaml +13 -12
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/pytest.toml +4 -5
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/release.yaml +23 -5
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/skills/babysit-ci/SKILL.md +7 -3
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/skills/repomatic-audit/SKILL.md +4 -4
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/skills/repomatic-deps/SKILL.md +1 -1
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/skills/repomatic-ship/SKILL.md +11 -5
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/tests.yaml +19 -61
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/unsubscribe.yaml +5 -4
- repomatic-7.9.0/repomatic/data/yamllint.yaml +12 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/github/gh.py +40 -21
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/github/releases.py +11 -2
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/github/token.py +16 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/github/workflow_sync.py +26 -7
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/gitignore.py +38 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/http.py +11 -1
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/init_project.py +65 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/lint_repo.py +119 -2
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/metadata.py +112 -50
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/prepare_release.py +43 -8
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/pypi.py +17 -3
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/registry.py +35 -34
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/setup_guide.py +41 -18
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/setup-guide-token.md +2 -1
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/tool_registry.py +4 -1
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/tool_runner.py +115 -6
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/version_sync.py +9 -2
- repomatic-7.7.0/repomatic/data/awesome_template/.github/code-of-conduct.md +0 -70
- repomatic-7.7.0/repomatic/data/codecov.yaml +0 -6
- repomatic-7.7.0/repomatic/data/yamllint.yaml +0 -6
- {repomatic-7.7.0 → repomatic-7.9.0}/license +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/__main__.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/awesome_toc.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/binaries_page.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/binary.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/broken_links.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/bundle.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/cache.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/checksums.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/compat.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/__init__.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/action-publish-pypi.yaml +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/agent-grunt-qa.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/agent-qa-engineer.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/awesome_template/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/awesome_template/.github/ISSUE_TEMPLATE/new-link.yaml +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/awesome_template/.github/contributing.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/awesome_template/.github/contributing.zh.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/awesome_template/.github/funding.yml +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/awesome_template/.github/pull_request_template.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/awesome_template/__init__.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/awesome_template/license +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/bumpversion.toml +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/labeller-content-based.yaml +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/labeller-file-based.yaml +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/labels.toml +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/lychee.toml +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/mdformat.toml +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/mypy.toml +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/ruff.toml +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/skills/av-false-positive/SKILL.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/skills/awesome-triage/SKILL.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/skills/benchmark-update/SKILL.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/skills/brand-assets/SKILL.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/skills/file-bug-report/SKILL.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/skills/github-housekeeping/SKILL.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/skills/repomatic-changelog/SKILL.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/skills/repomatic-init/SKILL.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/skills/repomatic-topics/SKILL.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/skills/sphinx-docs-sync/SKILL.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/skills/translation-sync/SKILL.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/skills/upstream-audit/SKILL.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/typos.toml +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/uv.toml +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/vt-trend-chart.js +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/data/zizmor.yaml +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/dep_graph.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/dep_report.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/dep_sources.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/docs.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/frontmatter.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/git_ops.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/github/__init__.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/github/actions.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/github/dev_release.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/github/issue.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/github/matrix.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/github/pr_body.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/github/release_sync.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/github/sponsor.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/github/status.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/github/unsubscribe.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/humanize.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/images.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/labels.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/mailmap.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/matrix_axes.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/npm.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/plugin.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/py.typed +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/pyproject.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/sync_ops.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/__init__.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/available-admonition.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/broken-links-issue.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/bump-version.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/detect-squash-merge.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/development-warning.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/fix-changelog.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/fix-typos.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/fix-vulnerable-deps.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/format-images.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/format-json.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/format-markdown.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/format-pyproject.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/format-python.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/format-shell.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/generated-footer.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/github-releases.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/immutable-releases.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/pr-metadata.md.noformat +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/prepare-release.md.noformat +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/refresh-tip.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/release-notes.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/release-sync-report.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/setup-guide-branch-ruleset.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/setup-guide-dependabot.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/setup-guide-fork-pr-approval.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/setup-guide-notifications-pat.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/setup-guide-pages-source.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/setup-guide-pypi-trusted-publisher.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/setup-guide-sha-pinning-required.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/setup-guide-verify.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/setup-guide-virustotal.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/setup-guide.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/sync-action-pins.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/sync-bumpversion.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/sync-dep-sources.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/sync-gitignore.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/sync-mailmap.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/sync-repomatic.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/sync-tool-versions.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/sync-uv-lock.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/sync-workflow-pins.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/unavailable-admonition.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/unsubscribe-phase1.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/unsubscribe-phase2.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/update-dep-graph.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/update-docs.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/templates/yanked-admonition.md +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/tool_runner_page.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/uv.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/virustotal.py +0 -0
- {repomatic-7.7.0 → repomatic-7.9.0}/repomatic/vulnerable_deps.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: repomatic
|
|
3
|
-
Version: 7.
|
|
3
|
+
Version: 7.9.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.
|
|
62
|
+
Project-URL: Download, https://github.com/kdeldycke/repomatic/releases/tag/v7.9.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
|
[](https://pypi.org/project/repomatic/)
|
|
74
74
|
[](https://pypi.org/project/repomatic/)
|
|
75
75
|
[](https://pepy.tech/projects/repomatic)
|
|
76
76
|
[](https://github.com/kdeldycke/repomatic/actions/workflows/tests.yaml?query=branch%3Amain)
|
|
77
|
-
[](https://app.codecov.io/gh/kdeldycke/repomatic)
|
|
78
77
|
[](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
|
-  [Meta Package Manager](https://github.com/kdeldycke/meta-package-manager) - A unifying CLI for multiple package managers.
|
|
161
160
|
-  [Mail Deduplicate](https://github.com/kdeldycke/mail-deduplicate) - A CLI to deduplicate similar emails.
|
|
162
161
|
-  [dotfiles](https://github.com/kdeldycke/dotfiles) - macOS dotfiles for Python developers.
|
|
163
|
-
-  [Click Extra](https://github.com/kdeldycke/click-extra) -
|
|
162
|
+
-  [Click Extra](https://github.com/kdeldycke/click-extra) - Drop-in replacement for Click to make user-friendly and colorful CLI.
|
|
164
163
|
-  [repomatic](https://github.com/kdeldycke/repomatic) - Itself. Eat your own dog-food.
|
|
165
|
-
-  [Plumage](https://github.com/kdeldycke/plumage) - Clean and tidy theme for Pelican, the static site generator.
|
|
165
|
+
-  [Kevin Deldycke's blog](https://github.com/kdeldycke/kevin-deldycke-blog) - My personal blog, based on Pelican.
|
|
166
|
+
-  [Extra Platforms](https://github.com/kdeldycke/extra-platforms) - Detect architectures, platforms, shells, terminals, CI systems and agents, grouped by family.
|
|
167
|
+
-  [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
|
+
version = "7.9.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.
|
|
95
|
+
Download = "https://github.com/kdeldycke/repomatic/releases/tag/v7.9.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.
|
|
149
|
+
file-version = "7.9.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.
|
|
159
|
+
product-version = "7.9.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.
|
|
253
|
+
current_version = "7.9.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.
|
|
15
|
+
version = "7.9.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.
|
|
148
|
+
urls.Download = "https://github.com/kdeldycke/repomatic/releases/tag/v7.9.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.
|
|
253
|
+
file-version = "7.9.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.
|
|
272
|
+
product-version = "7.9.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.
|
|
410
|
+
current_version = "7.9.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
|
[](https://pypi.org/project/repomatic/)
|
|
6
6
|
[](https://pypi.org/project/repomatic/)
|
|
7
7
|
[](https://pepy.tech/projects/repomatic)
|
|
8
8
|
[](https://github.com/kdeldycke/repomatic/actions/workflows/tests.yaml?query=branch%3Amain)
|
|
9
|
-
[](https://app.codecov.io/gh/kdeldycke/repomatic)
|
|
10
9
|
[](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
|
-  [Meta Package Manager](https://github.com/kdeldycke/meta-package-manager) - A unifying CLI for multiple package managers.
|
|
93
92
|
-  [Mail Deduplicate](https://github.com/kdeldycke/mail-deduplicate) - A CLI to deduplicate similar emails.
|
|
94
93
|
-  [dotfiles](https://github.com/kdeldycke/dotfiles) - macOS dotfiles for Python developers.
|
|
95
|
-
-  [Click Extra](https://github.com/kdeldycke/click-extra) -
|
|
94
|
+
-  [Click Extra](https://github.com/kdeldycke/click-extra) - Drop-in replacement for Click to make user-friendly and colorful CLI.
|
|
96
95
|
-  [repomatic](https://github.com/kdeldycke/repomatic) - Itself. Eat your own dog-food.
|
|
97
|
-
-  [Plumage](https://github.com/kdeldycke/plumage) - Clean and tidy theme for Pelican, the static site generator.
|
|
97
|
+
-  [Kevin Deldycke's blog](https://github.com/kdeldycke/kevin-deldycke-blog) - My personal blog, based on Pelican.
|
|
98
|
+
-  [Extra Platforms](https://github.com/kdeldycke/extra-platforms) - Detect architectures, platforms, shells, terminals, CI systems and agents, grouped by family.
|
|
99
|
+
-  [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.
|
|
@@ -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
|
-
|
|
990
|
-
|
|
991
|
-
|
|
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
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|