repomatic 7.6.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.
- {repomatic-7.6.0 → repomatic-7.8.0}/PKG-INFO +8 -6
- {repomatic-7.6.0 → repomatic-7.8.0}/pyproject.toml +11 -6
- {repomatic-7.6.0 → repomatic-7.8.0}/pyproject.toml.orig +37 -8
- {repomatic-7.6.0 → repomatic-7.8.0}/readme.md +6 -4
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/__init__.py +2 -2
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/__main__.py +9 -8
- repomatic-7.8.0/repomatic/attestation.py +189 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/awesome_toc.py +3 -3
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/binaries_page.py +36 -68
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/binary.py +119 -7
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/broken_links.py +34 -14
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/cache.py +51 -7
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/changelog.py +424 -153
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/checksums.py +11 -5
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/cli.py +179 -292
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/config.py +6 -3
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/_release-engine.yaml +79 -39
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/autofix.yaml +62 -49
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/cancel-runs.yaml +1 -1
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/changelog.yaml +14 -14
- repomatic-7.8.0/repomatic/data/coverage.toml +24 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/debug.yaml +2 -2
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/docs.yaml +6 -4
- repomatic-7.8.0/repomatic/data/labeller-content-based.yaml +31 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/labels.yaml +12 -10
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/lint.yaml +18 -14
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/pytest.toml +4 -5
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/release.yaml +15 -5
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/skills/awesome-triage/SKILL.md +10 -10
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/skills/babysit-ci/SKILL.md +14 -4
- repomatic-7.8.0/repomatic/data/skills/github-housekeeping/SKILL.md +129 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/skills/repomatic-audit/SKILL.md +5 -5
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/skills/repomatic-ship/SKILL.md +23 -5
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/tests.yaml +19 -59
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/unsubscribe.yaml +1 -1
- repomatic-7.8.0/repomatic/data/vt-trend-chart.js +52 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/dep_graph.py +14 -10
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/dep_report.py +139 -64
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/dep_sources.py +45 -57
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/docs.py +28 -16
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/git_ops.py +12 -5
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/github/actions.py +24 -1
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/github/dev_release.py +30 -7
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/github/gh.py +24 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/github/issue.py +14 -5
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/github/matrix.py +35 -6
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/github/release_sync.py +8 -27
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/github/releases.py +17 -24
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/github/sponsor.py +31 -24
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/github/token.py +26 -13
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/github/unsubscribe.py +9 -27
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/github/workflow_sync.py +126 -226
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/http.py +11 -1
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/images.py +68 -60
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/init_project.py +189 -149
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/labels.py +39 -20
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/lint_repo.py +386 -70
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/mailmap.py +24 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/matrix_axes.py +0 -7
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/metadata.py +194 -159
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/npm.py +5 -1
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/plugin.py +16 -6
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/prepare_release.py +101 -81
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/pypi.py +110 -49
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/pyproject.py +40 -1
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/registry.py +84 -47
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/setup_guide.py +19 -8
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/sync_ops.py +205 -15
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/yanked-admonition.md +2 -2
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/tool_registry.py +85 -3
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/tool_runner.py +182 -127
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/tool_runner_page.py +7 -19
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/uv.py +199 -106
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/version_sync.py +74 -1
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/virustotal.py +9 -2
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/vulnerable_deps.py +29 -12
- repomatic-7.6.0/repomatic/data/codecov.yaml +0 -6
- repomatic-7.6.0/repomatic/data/labeller-content-based.yaml +0 -37
- {repomatic-7.6.0 → repomatic-7.8.0}/license +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/bundle.py +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/compat.py +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/__init__.py +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/action-publish-pypi.yaml +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/agent-grunt-qa.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/agent-qa-engineer.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/agent-sphinx-docs.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/autolock.yaml +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/awesome_template/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/awesome_template/.github/ISSUE_TEMPLATE/new-link.yaml +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/awesome_template/.github/code-of-conduct.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/awesome_template/.github/contributing.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/awesome_template/.github/contributing.zh.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/awesome_template/.github/funding.yml +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/awesome_template/.github/pull_request_template.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/awesome_template/__init__.py +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/awesome_template/license +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/bumpversion.toml +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/labeller-file-based.yaml +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/labels.toml +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/lychee.toml +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/mdformat.toml +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/mypy.toml +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/ruff.toml +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/skills/av-false-positive/SKILL.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/skills/benchmark-update/SKILL.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/skills/brand-assets/SKILL.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/skills/file-bug-report/SKILL.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/skills/repomatic-changelog/SKILL.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/skills/repomatic-deps/SKILL.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/skills/repomatic-init/SKILL.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/skills/repomatic-topics/SKILL.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/skills/sphinx-docs-sync/SKILL.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/skills/translation-sync/SKILL.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/skills/upstream-audit/SKILL.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/typos.toml +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/uv.toml +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/yamllint.yaml +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/data/zizmor.yaml +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/frontmatter.py +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/github/__init__.py +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/github/pr_body.py +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/github/status.py +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/gitignore.py +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/humanize.py +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/py.typed +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/__init__.py +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/available-admonition.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/broken-links-issue.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/bump-version.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/detect-squash-merge.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/development-warning.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/fix-changelog.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/fix-typos.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/fix-vulnerable-deps.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/format-images.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/format-json.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/format-markdown.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/format-pyproject.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/format-python.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/format-shell.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/generated-footer.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/github-releases.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/immutable-releases.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/pr-metadata.md.noformat +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/prepare-release.md.noformat +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/refresh-tip.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/release-notes.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/release-sync-report.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/setup-guide-branch-ruleset.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/setup-guide-dependabot.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/setup-guide-fork-pr-approval.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/setup-guide-notifications-pat.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/setup-guide-pages-source.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/setup-guide-pypi-trusted-publisher.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/setup-guide-sha-pinning-required.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/setup-guide-token.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/setup-guide-verify.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/setup-guide-virustotal.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/setup-guide.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/sync-action-pins.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/sync-bumpversion.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/sync-dep-sources.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/sync-gitignore.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/sync-mailmap.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/sync-repomatic.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/sync-tool-versions.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/sync-uv-lock.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/sync-workflow-pins.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/unavailable-admonition.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/unsubscribe-phase1.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/unsubscribe-phase2.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/update-dep-graph.md +0 -0
- {repomatic-7.6.0 → repomatic-7.8.0}/repomatic/templates/update-docs.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: repomatic
|
|
3
|
-
Version: 7.
|
|
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.
|
|
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
|
[](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.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.
|
|
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.
|
|
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.
|
|
159
|
+
product-version = "7.8.0"
|
|
160
160
|
windows-icon-from-ico = "docs/assets/icon.ico"
|
|
161
161
|
|
|
162
162
|
[tool.ruff]
|
|
@@ -208,6 +208,7 @@ astroid = "astroid"
|
|
|
208
208
|
Contribuer = "Contribuer"
|
|
209
209
|
oject = "oject"
|
|
210
210
|
PNGs = "PNGs"
|
|
211
|
+
quater = "quater"
|
|
211
212
|
sur = "sur"
|
|
212
213
|
|
|
213
214
|
[tool.mypy]
|
|
@@ -245,10 +246,11 @@ branch = true
|
|
|
245
246
|
source = ["repomatic"]
|
|
246
247
|
|
|
247
248
|
[tool.coverage.report]
|
|
249
|
+
fail_under = 80
|
|
248
250
|
precision = 2
|
|
249
251
|
|
|
250
252
|
[tool.bumpversion]
|
|
251
|
-
current_version = "7.
|
|
253
|
+
current_version = "7.8.0"
|
|
252
254
|
parse = '(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(\.dev(?P<dev>\d+))?'
|
|
253
255
|
serialize = [
|
|
254
256
|
"{major}.{minor}.{patch}.dev{dev}",
|
|
@@ -333,10 +335,13 @@ pypi-package-history = [
|
|
|
333
335
|
"gha-utils",
|
|
334
336
|
"repokit",
|
|
335
337
|
]
|
|
336
|
-
release-assets = ["repomatic-plugin.zip"]
|
|
338
|
+
release-assets = ["repomatic-claude-plugin.zip"]
|
|
337
339
|
|
|
338
340
|
[tool.repomatic.changelog]
|
|
339
341
|
archive-location = "./docs/changelog-archive.md"
|
|
340
342
|
|
|
341
343
|
[tool.repomatic.manpages]
|
|
342
344
|
script = "repomatic.cli:repomatic"
|
|
345
|
+
|
|
346
|
+
[tool.actionlint.paths."**/workflows/*"]
|
|
347
|
+
ignore = ['property "job_workflow_sha" is not defined']
|
|
@@ -12,7 +12,7 @@ requires = [ "uv-build>=0.8" ]
|
|
|
12
12
|
[project]
|
|
13
13
|
# Docs: https://packaging.python.org/en/latest/guides/writing-pyproject-toml/
|
|
14
14
|
name = "repomatic"
|
|
15
|
-
version = "7.
|
|
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.
|
|
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.
|
|
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.
|
|
272
|
+
product-version = "7.8.0"
|
|
273
273
|
windows-icon-from-ico = "docs/assets/icon.ico"
|
|
274
274
|
|
|
275
275
|
[tool.ruff]
|
|
@@ -310,8 +310,11 @@ output-prefer-rule-codes = true
|
|
|
310
310
|
[tool.typos]
|
|
311
311
|
default.extend-identifiers = { Github = "GitHub", IOS = "iOS", Javascript = "JavaScript", MacOS = "macOS", PNGs = "PNGs", PyPi = "PyPI", Typescript = "TypeScript" }
|
|
312
312
|
# False positives. "Contribuer" is the French for "Contributing", part of the
|
|
313
|
-
# translated-readme fixture in tests/test_awesome_toc.py.
|
|
314
|
-
|
|
313
|
+
# translated-readme fixture in tests/test_awesome_toc.py. "quater" is the fourth
|
|
314
|
+
# term of the Latin ordinal series numbering the repo-lint checks (bis, ter,
|
|
315
|
+
# quater), not a misspelling of "quarter": the unattended fix-typos job rewrote
|
|
316
|
+
# it once already and had to be reverted by hand.
|
|
317
|
+
default.extend-words = { astroid = "astroid", Contribuer = "Contribuer", oject = "oject", PNGs = "PNGs", quater = "quater", sur = "sur" }
|
|
315
318
|
# Skip content between <!-- typos:off --> and <!-- typos:on --> markers.
|
|
316
319
|
default.extend-ignore-re = [
|
|
317
320
|
"(?s)<!-- typos:off -->.*?<!-- typos:on -->",
|
|
@@ -392,10 +395,19 @@ xfail_strict = true
|
|
|
392
395
|
# https://coverage.readthedocs.io/en/latest/config.html
|
|
393
396
|
run.branch = true
|
|
394
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
|
|
395
407
|
report.precision = 2
|
|
396
408
|
|
|
397
409
|
[tool.bumpversion]
|
|
398
|
-
current_version = "7.
|
|
410
|
+
current_version = "7.8.0"
|
|
399
411
|
# Parse versions with an optional .devN suffix (PEP 440).
|
|
400
412
|
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(\\.dev(?P<dev>\\d+))?"
|
|
401
413
|
serialize = [
|
|
@@ -522,4 +534,21 @@ pypi-package-history = [ "gha-utils", "repokit" ]
|
|
|
522
534
|
# attached to every GitHub release by the engine's extra-assets job. The
|
|
523
535
|
# marketplace at .claude-plugin/marketplace.json installs the plugin straight
|
|
524
536
|
# from this asset, so a release missing it breaks `/plugin install`.
|
|
525
|
-
release-assets = [ "repomatic-plugin.zip" ]
|
|
537
|
+
release-assets = [ "repomatic-claude-plugin.zip" ]
|
|
538
|
+
|
|
539
|
+
[tool.actionlint]
|
|
540
|
+
# actionlint reads no pyproject section of its own: repomatic serializes this one
|
|
541
|
+
# to a temporary YAML config and passes it via `--config-file`. `paths` keys are
|
|
542
|
+
# globs relative to the repository root, and each `ignore` entry is a regular
|
|
543
|
+
# expression matched against the error message.
|
|
544
|
+
#
|
|
545
|
+
# `github.job_workflow_sha` is a real context property, holding the commit SHA of
|
|
546
|
+
# the reusable workflow file itself, which the tool-cache keys read so they do not
|
|
547
|
+
# rotate on every caller commit. GitHub documents it only as an OIDC token claim,
|
|
548
|
+
# never on the `github` context reference page, so actionlint's schema lacks it.
|
|
549
|
+
# Drop this entry once rhysd/actionlint#671 ships in a release.
|
|
550
|
+
#
|
|
551
|
+
# The glob carries no dot on purpose: click-extra expands every dotted key of
|
|
552
|
+
# `pyproject.toml`, and one starting with a dot makes it warn on each repomatic
|
|
553
|
+
# invocation. `**` matches dot-directories, so `.github/workflows/` is covered.
|
|
554
|
+
paths."**/workflows/*".ignore = [ 'property "job_workflow_sha" is not defined' ]
|
|
@@ -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.
|
|
@@ -35,21 +35,22 @@ from repomatic.cli import repomatic
|
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
def main():
|
|
38
|
-
"""Execute the CLI
|
|
38
|
+
"""Execute the CLI under its canonical name, whatever the entry point.
|
|
39
|
+
|
|
40
|
+
Without `prog_name`, Click derives the displayed name from the invocation:
|
|
39
41
|
|
|
40
42
|
```{code-block} shell-session
|
|
41
43
|
$ python -m repomatic --version
|
|
42
44
|
python -m repomatic, version 4.0.0
|
|
43
45
|
```
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
- plain inline package call: `python -m repomatic`,
|
|
48
|
-
- Poetry's script entry point: `repomatic = 'repomatic.__main__:main`,
|
|
49
|
-
- Nuitka's main module invocation requirement:
|
|
50
|
-
`python -m nuitka (...) repomatic/__main__.py`
|
|
47
|
+
The `main()` indirection lets three invocation styles share one entry
|
|
48
|
+
point rendering the same name:
|
|
51
49
|
|
|
52
|
-
|
|
50
|
+
- the plain module call: `python -m repomatic`,
|
|
51
|
+
- the `[project.scripts]` console script: `repomatic = "repomatic.__main__:main"`,
|
|
52
|
+
- Nuitka's main-module compilation requirement:
|
|
53
|
+
`python -m nuitka (...) repomatic/__main__.py`.
|
|
53
54
|
"""
|
|
54
55
|
repomatic(prog_name=repomatic.name)
|
|
55
56
|
|
|
@@ -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})
|
|
@@ -235,7 +235,7 @@ def fix_awesome_toc(root: Path | None = None) -> dict[Path, list[str]]:
|
|
|
235
235
|
reference = root / REFERENCE_README
|
|
236
236
|
reference_headings: list[str] | None = None
|
|
237
237
|
if reference.is_file():
|
|
238
|
-
reference_headings = headings(reference.read_text(encoding="
|
|
238
|
+
reference_headings = headings(reference.read_text(encoding="UTF-8"))
|
|
239
239
|
else:
|
|
240
240
|
logging.warning(
|
|
241
241
|
f"No {REFERENCE_README} in {root}: translations can only be matched "
|
|
@@ -244,13 +244,13 @@ def fix_awesome_toc(root: Path | None = None) -> dict[Path, list[str]]:
|
|
|
244
244
|
|
|
245
245
|
report: dict[Path, list[str]] = {}
|
|
246
246
|
for readme in _readmes(root):
|
|
247
|
-
content = readme.read_text(encoding="
|
|
247
|
+
content = readme.read_text(encoding="UTF-8")
|
|
248
248
|
forbidden = forbidden_headings_for(content, reference_headings)
|
|
249
249
|
updated, removed = strip_toc_entries(content, forbidden)
|
|
250
250
|
if not removed:
|
|
251
251
|
logging.debug(f"No forbidden ToC entry in {readme}.")
|
|
252
252
|
continue
|
|
253
|
-
readme.write_text(updated, encoding="
|
|
253
|
+
readme.write_text(updated, encoding="UTF-8")
|
|
254
254
|
logging.info(f"Removed {len(removed)} ToC entries from {readme}.")
|
|
255
255
|
report[readme] = removed
|
|
256
256
|
return report
|
|
@@ -57,7 +57,8 @@ from pathlib import Path
|
|
|
57
57
|
|
|
58
58
|
from click_extra.blocks import replace_region
|
|
59
59
|
|
|
60
|
-
from .binary import NUITKA_BUILD_TARGETS
|
|
60
|
+
from .binary import BINARY_ASSET_SUFFIXES, NUITKA_BUILD_TARGETS
|
|
61
|
+
from .bundle import get_data_content
|
|
61
62
|
from .github.releases import parse_release_version
|
|
62
63
|
from .virustotal import VIRUSTOTAL_GUI_URL
|
|
63
64
|
|
|
@@ -68,11 +69,21 @@ if TYPE_CHECKING:
|
|
|
68
69
|
from .github.releases import ReleaseAsset, ReleaseWithAssets
|
|
69
70
|
from .virustotal import ScanRecord
|
|
70
71
|
|
|
71
|
-
|
|
72
|
-
"""
|
|
72
|
+
CHART_JS_URL = "https://cdn.jsdelivr.net/npm/chart.js@4.5.0/dist/chart.umd.min.js"
|
|
73
|
+
"""Pinned CDN artifact drawing the detections trend chart.
|
|
73
74
|
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
The one external artifact this module publishes into every downstream
|
|
76
|
+
repository's docs, so it carries a checksum beside the pin: bump the version
|
|
77
|
+
by hand together with {data}`CHART_JS_SRI`.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
CHART_JS_SRI = "sha384-XcdcwHqIPULERb2yDEM4R0XaQKU3YnDsrTmjACBZyfdVVqjh6xQ4/DCMd7XLcA6Y"
|
|
81
|
+
"""Subresource Integrity digest of {data}`CHART_JS_URL`.
|
|
82
|
+
|
|
83
|
+
The browser refuses the script if the CDN bytes stop matching. Recompute on
|
|
84
|
+
every version bump as the sha384 of the exact artifact, verified against the
|
|
85
|
+
same file inside the npm registry tarball before trusting the CDN copy:
|
|
86
|
+
`hashlib.sha384(artifact_bytes)` then base64.
|
|
76
87
|
"""
|
|
77
88
|
|
|
78
89
|
CSV_HEADERS = (
|
|
@@ -186,13 +197,14 @@ def _binary_assets(release: ReleaseWithAssets) -> list[ReleaseAsset]:
|
|
|
186
197
|
"""Return the release's compiled binaries, sorted by filename.
|
|
187
198
|
|
|
188
199
|
Releases also carry versionless alias copies of each binary, backing the
|
|
189
|
-
stable ``releases/latest/download`` URLs (
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
200
|
+
stable ``releases/latest/download`` URLs (created by
|
|
201
|
+
{func}`~repomatic.binary.pack_binary_assets`, which
|
|
202
|
+
`release-assets` runs in the release engine). An alias shares its digest
|
|
203
|
+
with its versioned sibling: collapse each digest group onto its longest
|
|
204
|
+
name (the versioned one, since {func}`~repomatic.binary.versionless_alias`
|
|
205
|
+
only ever strips the version segment) so the catalog lists each binary
|
|
206
|
+
once. Assets without a recorded digest (uploaded before GitHub started
|
|
207
|
+
recording them, mid-2025) predate the aliases and are kept as-is.
|
|
196
208
|
"""
|
|
197
209
|
binaries = [a for a in release.assets if a.name.endswith(BINARY_ASSET_SUFFIXES)]
|
|
198
210
|
digest_groups: dict[str, list[ReleaseAsset]] = {}
|
|
@@ -275,6 +287,15 @@ def render_chart_section(records: Sequence[ScanRecord]) -> str:
|
|
|
275
287
|
],
|
|
276
288
|
sort_keys=True,
|
|
277
289
|
)
|
|
290
|
+
# The chart script ships as a bundled data file so it reads (and lints) as
|
|
291
|
+
# JavaScript instead of sixty quoted Python strings. Placeholder tokens are
|
|
292
|
+
# substituted with str.replace, never str.format: the script is full of
|
|
293
|
+
# literal braces.
|
|
294
|
+
script = (
|
|
295
|
+
get_data_content("vt-trend-chart.js")
|
|
296
|
+
.replace("__VT_TREND_PAYLOAD__", payload)
|
|
297
|
+
.replace("__VT_DANGER_PCT__", str(FLAGGED_DANGER_PCT))
|
|
298
|
+
)
|
|
278
299
|
return (
|
|
279
300
|
"## VirusTotal detections\n\n"
|
|
280
301
|
"Share of antivirus engine verdicts flagging the binaries of each "
|
|
@@ -283,62 +304,9 @@ def render_chart_section(records: Sequence[ScanRecord]) -> str:
|
|
|
283
304
|
"there up.\n\n"
|
|
284
305
|
"```{raw} html\n"
|
|
285
306
|
'<div style="height: 320px;"><canvas id="vt-trend"></canvas></div>\n'
|
|
286
|
-
'<script src="
|
|
287
|
-
'
|
|
288
|
-
"<script>\n"
|
|
289
|
-
f"const VT_TREND = {payload};\n"
|
|
290
|
-
f"const VT_DANGER_PCT = {FLAGGED_DANGER_PCT};\n"
|
|
291
|
-
"const vtCss = getComputedStyle(document.documentElement);\n"
|
|
292
|
-
"const vtColor = (name, fallback) =>\n"
|
|
293
|
-
" vtCss.getPropertyValue(name).trim() || fallback;\n"
|
|
294
|
-
"const vtTint = (p) => {\n"
|
|
295
|
-
' if (p.pct === 0) { return vtColor("--sd-color-success", "#28a745"); }\n'
|
|
296
|
-
" return p.pct >= VT_DANGER_PCT\n"
|
|
297
|
-
' ? vtColor("--sd-color-danger", "#dc3545")\n'
|
|
298
|
-
' : vtColor("--sd-color-warning", "#f0b37e");\n'
|
|
299
|
-
"};\n"
|
|
300
|
-
'new Chart(document.getElementById("vt-trend"), {\n'
|
|
301
|
-
' type: "line",\n'
|
|
302
|
-
" data: {\n"
|
|
303
|
-
" datasets: [{\n"
|
|
304
|
-
" data: VT_TREND.map((p) => ({x: Date.parse(p.date), y: p.pct})),\n"
|
|
305
|
-
' borderColor: "#88888866",\n'
|
|
306
|
-
" pointBackgroundColor: VT_TREND.map(vtTint),\n"
|
|
307
|
-
" pointBorderColor: VT_TREND.map(vtTint),\n"
|
|
308
|
-
" pointRadius: 4,\n"
|
|
309
|
-
" tension: 0.2,\n"
|
|
310
|
-
" }],\n"
|
|
311
|
-
" },\n"
|
|
312
|
-
" options: {\n"
|
|
313
|
-
" maintainAspectRatio: false,\n"
|
|
314
|
-
" plugins: {\n"
|
|
315
|
-
" legend: {display: false},\n"
|
|
316
|
-
" tooltip: {callbacks: {\n"
|
|
317
|
-
" title: (items) => VT_TREND[items[0].dataIndex].tag,\n"
|
|
318
|
-
" label: (item) => {\n"
|
|
319
|
-
" const p = VT_TREND[item.dataIndex];\n"
|
|
320
|
-
' return p.flagged + " / " + p.total\n'
|
|
321
|
-
' + " verdicts flagged (" + p.pct + "%)";\n'
|
|
322
|
-
" },\n"
|
|
323
|
-
" }},\n"
|
|
324
|
-
" },\n"
|
|
325
|
-
" scales: {\n"
|
|
326
|
-
" x: {\n"
|
|
327
|
-
' type: "linear",\n'
|
|
328
|
-
" ticks: {\n"
|
|
329
|
-
" maxTicksLimit: 8,\n"
|
|
330
|
-
" callback: (value) =>\n"
|
|
331
|
-
" new Date(value).toISOString().slice(0, 10),\n"
|
|
332
|
-
" },\n"
|
|
333
|
-
" },\n"
|
|
334
|
-
" y: {\n"
|
|
335
|
-
" beginAtZero: true,\n"
|
|
336
|
-
' title: {display: true, text: "Flagged verdicts (%)"},\n'
|
|
337
|
-
" },\n"
|
|
338
|
-
" },\n"
|
|
339
|
-
" },\n"
|
|
340
|
-
"});\n"
|
|
341
|
-
"</script>\n"
|
|
307
|
+
f'<script src="{CHART_JS_URL}" integrity="{CHART_JS_SRI}" '
|
|
308
|
+
'crossorigin="anonymous"></script>\n'
|
|
309
|
+
f"<script>\n{script}</script>\n"
|
|
342
310
|
"```"
|
|
343
311
|
)
|
|
344
312
|
|