# Copyright Kevin Deldycke <[email protected]> and contributors.
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
"""Declarative registry of the external tools `repomatic run` manages.
Each `ToolSpec` entry pins a tool's version and, for binary-distributed tools,
its per-platform download URLs and SHA-256 digests in `CHECKSUMS`; the paired
`VERSIONS` map records the version each checksum set was computed for. The
`ArchiveFormat`, `NativeFormat`, `BinarySpec`, and `NpmSpec` types describe how
each tool is fetched and how its `[tool.X]` section is translated to the tool's
native config format. The `repomatic run` engine in `tool_runner.py` consumes
this data to install and invoke each tool.
```{note}
`sync-tool-versions` and `update-checksums` rewrite this module's `version=`,
`VERSIONS`, and `CHECKSUMS` literals in place by string substitution, so their
formatting must stay stable. The lint, autofix, and docs workflows key their
tool caches on a hash of this file, so only a genuine version or checksum bump
invalidates a cached tool download.
```
"""
from __future__ import annotations
import json
import logging
import re
import sys
from dataclasses import dataclass
from datetime import datetime, timezone
from enum import Enum
from inspect import cleandoc
from pathlib import Path
import tomlrt
import yaml
from click_extra import TableFormat, render_table
from extra_platforms import (
AARCH64,
ALL_PLATFORMS,
LINUX,
MACOS,
UNKNOWN_PLATFORM,
WINDOWS,
X86_64,
Architecture,
Group,
Platform,
current_architecture,
current_platform,
)
from packaging.requirements import Requirement
from . import __version__
from .npm import NPM_PACKAGE_URL
from .pypi import PYPI_PACKAGE_URL
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Callable, Sequence
from typing import Any, Literal
from .metadata import Metadata
GENERATED_HEADER_TEMPLATE = (
"Generated by {command} v{version} - https://github.com/kdeldycke/repomatic"
)
"""Template for the first line of generated-file headers.
Used by both CLI commands (e.g. `sync-mailmap`) and the tool runner
(e.g. `run shfmt`) to stamp files with provenance. Format fields:
`command` (full command path) and `version` (package version).
"""
def _reroot_section(section: tomlrt.Table) -> tomlrt.Document:
"""Reparent a `[tool.X]` section to a standalone TOML document.
A native config file (`.gitleaks.toml`) drops the `[tool.X]` prefix the
section carries inside `pyproject.toml`: top-level keys move to the document
root and every `[tool.X.sub]` header becomes `[sub]`. Assigning each value
into a fresh `Document` routes sub-tables and arrays-of-tables through
tomlrt's trivia-preserving clone path (dimbleby/tomlrt#171), so their
comments and nested headers survive. Direct scalar and array keys lose their
key-level trivia on that assignment (the trivia lives on the parent slot,
not the value), so the leading block and end-of-line comment are copied back
explicitly.
```{note}
End-of-line comments are re-emitted with a single space before `#`,
normalising any wider padding from the source. A comment on the `[tool.X]`
header line itself has no home at the document root and is dropped.
```
:param section: A live `[tool.X]` table parsed from `pyproject.toml`.
:return: A standalone document with the section's body at the root.
"""
# An inline `tool.x = {...}` table holds no standalone comments, and the
# comment API is unavailable on it; expand to a plain document, matching the
# behaviour before trivia preservation.
if section.is_inline:
return tomlrt.Document(section.to_dict())
doc = tomlrt.Document()
for key in section:
doc[key] = section[key]
# Sub-tables and AoTs carry their own trivia through the assignment above;
# only direct scalar and array KV trivia needs restoring.
for key, value in section.items():
if isinstance(value, (tomlrt.Table, tomlrt.AoT)):
continue
if leading := section.leading_block.get(key):
doc.leading_block[key] = leading
if eol := section.comments.get(key):
doc.comments[key] = eol
return doc
PlatformKey = tuple[Platform | Group, Architecture]
"""A `(platform_or_group, architecture)` pair used as binary lookup key.
The platform element can be a single {class}`~extra_platforms.Platform` (like
`MACOS`) or a {class}`~extra_platforms.Group` (like `LINUX`, which matches
any Linux distribution). The architecture is always a concrete
{class}`~extra_platforms.Architecture`.
Resolution order in {meth}`BinarySpec.resolve_platform`:
1. Exact Platform match (`current_platform() == key_platform`).
2. Group membership (`current_platform() in key_group`), preferring the
group with fewest members (most specific).
3. The `LINUX` family, only when `current_platform()` is `UNKNOWN_PLATFORM`,
so a distribution extra-platforms cannot name still reaches a family-wide
key.
"""
[docs]
@dataclass(frozen=True)
class BinarySpec:
"""Platform-specific binary download specification.
Keys are {data}`PlatformKey` tuples pairing an extra-platforms
{class}`~extra_platforms.Platform` or {class}`~extra_platforms.Group`
with an {class}`~extra_platforms.Architecture`. This lets callers use
broad groups (`LINUX` matches any distro) or specific platforms
(`DEBIAN`) with full detection heuristics from extra-platforms.
```{hint}
Structural integrity checks (key types, checksum format, URL
placeholders, strip_components consistency) are enforced in
`test_tool_spec_integrity`. If the registry becomes
user-configurable in the future, move these checks to
`__post_init__`.
```
"""
urls: dict[PlatformKey, str]
"""Platform key to URL template mapping. URLs use ``{version}`` placeholders."""
checksums: dict[PlatformKey, str]
"""Platform key to SHA-256 hex digest mapping."""
archive_format: ArchiveFormat | dict[PlatformKey | Platform | Group, ArchiveFormat]
"""Archive format of the downloaded file.
A single {class}`ArchiveFormat` applies to every platform. A dict maps
platform specifiers to formats, allowing mixed archives in one spec::
archive_format={ALL_PLATFORMS: ArchiveFormat.TAR_GZ, WINDOWS: ArchiveFormat.ZIP}
Dict keys follow the same resolution as {meth}`resolve_platform`:
exact {data}`PlatformKey` tuple first, then bare
{class}`~extra_platforms.Platform` equality, then
{class}`~extra_platforms.Group` membership (smallest group wins).
"""
archive_executable: str | None = None
"""Path of the executable inside the archive. `None` defaults to the
tool name. For `RAW` format, used as the final filename.
"""
strip_components: int | dict[PlatformKey | Platform | Group, int] = 0
"""Number of leading path components to strip when extracting.
A single `int` applies to every platform. A dict maps platform specifiers to
counts, using the same resolution as {meth}`get_archive_format`, for a
project whose archives are not laid out identically across platforms::
strip_components={ALL_PLATFORMS: 1, WINDOWS: 0}
`gh` is the motivating case: its Linux and macOS archives nest everything
under a `gh_{version}_{platform}_{arch}/` directory, while the Windows zip
puts `bin/gh.exe` at the root. The nesting cannot be absorbed by
{attr}`archive_executable` instead, since that is one string for all
platforms and the directory name carries the version and platform.
"""
@staticmethod
def _resolve_per_platform(
mapping: dict[PlatformKey | Platform | Group, Any],
key: PlatformKey,
what: str,
) -> Any:
"""Pick a per-platform mapping's value for the given platform key.
Resolves in order: exact {data}`PlatformKey` tuple, bare Platform
equality, then Group membership with the smallest group winning, so a
narrow `WINDOWS` entry overrides a broad `ALL_PLATFORMS` one.
:param mapping: Platform specifier to value mapping.
:param key: The platform key to resolve for.
:param what: Field name, for the error message.
:return: The matching value.
:raises ValueError: If no entry matches the platform key.
"""
# Exact tuple match.
if key in mapping:
return mapping[key]
# Bare Platform or Group match.
key_plat = key[0]
group_hits: list[tuple[Group, Any]] = []
for map_key, value in mapping.items():
if isinstance(map_key, tuple):
continue
if map_key == key_plat:
return value
if isinstance(map_key, Group) and (
isinstance(key_plat, Platform)
and key_plat in map_key
or isinstance(key_plat, Group)
and (key_plat & map_key)
):
group_hits.append((map_key, value))
if group_hits:
# Most-specific group: fewest members.
return min(group_hits, key=lambda h: len(h[0]))[1]
msg = f"No {what} for {key[0].name} {key[1].name}"
raise ValueError(msg)
[docs]
def get_strip_components(self, key: PlatformKey) -> int:
"""Return the leading path components to strip for a platform key.
When {attr}`strip_components` is a plain `int`, returns it directly.
When it is a dict, resolves through {meth}`_resolve_per_platform`.
"""
if isinstance(self.strip_components, int):
return self.strip_components
count: int = self._resolve_per_platform(
self.strip_components, key, "strip_components"
)
return count
MYPY_VERSION_MIN = (3, 8)
"""Earliest Python dialect Mypy's `--python-version 3.x` parameter accepts.
Floors the value {attr}`repomatic.metadata.Metadata.mypy_params` derives from
the project's `requires-python`, which the `mypy` entry in
{data}`TOOL_REGISTRY` passes through `computed_params`. A project declaring an
older floor would otherwise hand mypy a version it rejects outright.
[Sourced from Mypy's own defaults](https://github.com/python/mypy/blob/master/mypy/defaults.py).
"""
TOOL_LIST_HEADER_DEFS: tuple[tuple[str, str], ...] = (
("Tool", "tool"),
("Version", "version"),
("Config source", "config-source"),
)
"""Column definitions for the `repomatic run --list` table.
Lives beside the registry it renders; the CLI derives its `--sort-by`
choices from it.
"""
NPM_MIN_VERSION_FOR_COOLDOWN = "11.10.0"
"""First npm release honoring `min-release-age`, the cooldown gate for npm tools.
Older npm silently ignores the `--min-release-age` flag, so {func}`_install_npm`
warns when it cannot enforce the cooldown. This is a fixed floor (the release that
introduced the option), distinct from the auto-bumped `npm@X` bootstrap pin in
`lint.yaml`, which tracks the latest npm.
"""
[docs]
@dataclass(frozen=True)
class NpmSpec:
"""npm-registry backend marker for a {class}`ToolSpec`.
Presence (`ToolSpec.npm is not None`) selects the npm backend, the way a
{class}`BinarySpec` selects the download backend. The package name,
executable, and version all derive from the `ToolSpec` fields, so no per-tool
npm config is needed today; the class exists as a typed discriminator and a
home for future npm-specific options.
```{note}
npm tools need Node.js and npm on `PATH` at run time: the one backend that
depends on a runtime repomatic neither bundles nor provisions (binary tools
are self-contained; the uv backends use uv). Integrity is npm's own
per-tarball verification on install, so unlike {class}`BinarySpec` there is
no repomatic-pinned checksum; the `minimum-release-age` cooldown (npm's
`min-release-age`, npm 11.10.0+) gates the transitive tree instead. Older npm
ignores the gate, so the runner warns rather than silently skipping it.
```
"""
# ---------------------------------------------------------------------------
# Post-process callbacks
# ---------------------------------------------------------------------------
_DIRECTIVE_YAML_OPTIONS_RE = re.compile(
r"^((?:`{3,}|:{3,})\{[^}]+\}[^\n]*\n)"
r"---\n"
r"((?:[^\n]+\n)+?)"
r"---\n",
re.MULTILINE,
)
"""Match YAML-block directive options immediately after a MyST fence opening.
```{note}
Workaround for [executablebooks/mdformat-myst#21](https://github.com/executablebooks/mdformat-myst/issues/21) where
`mdformat-myst` unconditionally converts `:key: value` directive
options to YAML blocks (`---` / `key: value` / `---`). Remove when
upstream merges [executablebooks/mdformat-myst#49](https://github.com/executablebooks/mdformat-myst/pull/49).
```
"""
def _yaml_block_to_field_list(match: re.Match[str]) -> str:
"""Convert a single YAML-block directive option to field-list syntax."""
directive_line = match.group(1)
yaml_lines = match.group(2)
# Prepend ":" to each non-empty line: `key: value` β `:key: value`.
field_lines = re.sub(r"^(?=\S)", ":", yaml_lines, flags=re.MULTILINE)
return directive_line + field_lines
_ESCAPED_COLON_FENCE_RE = re.compile(
r"^\\(:{3,})\\\{[^}]+\}.*" # Opener: escaped ::: run, escaped {name}, title.
r"(?:\n(?:.*\n)*?)?" # Inner option and body lines.
r"^\\\1[ \t]*$", # Closer: the matching escaped ::: run.
re.MULTILINE,
)
r"""Match a colon-fence directive whose delimiters mdformat has escaped.
```{note}
Workaround for
[executablebooks/mdformat-myst#13](https://github.com/executablebooks/mdformat-myst/issues/13):
`mdformat-myst` does not treat ``:::{name}`` colon fences as directives, so
`mdformat-deflist` escapes their leading colons and `mdformat-myst` escapes the
opening brace, leaving an uneditable ``\:::\{name}`` / ``\:option:`` / ``\:::`` block.
Remove when upstream ships colon-fence support, via either
[executablebooks/mdformat-myst#36](https://github.com/executablebooks/mdformat-myst/pull/36)
or
[executablebooks/mdformat-myst#48](https://github.com/executablebooks/mdformat-myst/pull/48).
```
"""
def _unescape_colon_fence(match: re.Match[str]) -> str:
"""Strip mdformat's backslash escaping from a colon-fence directive block.
Handles nested fences: every fence and option line in the matched block is
un-escaped, not just the outermost opener.
"""
block = match.group(0)
# Drop the leading-colon escape on every fence and option line.
block = re.sub(r"^\\:", ":", block, flags=re.MULTILINE)
# Drop the brace escape on directive openers: :::\{name} -> :::{name}.
return re.sub(r"^(:{3,})\\\{", r"\1{", block, flags=re.MULTILINE)
def _fix_myst_directives(extra_args: Sequence[str]) -> None:
"""Undo mdformat's MyST-hostile directive rewrites, in place.
Two fixups run on every file in *extra_args* that exists on disk: YAML-block
directive options are restored to field-list syntax (see
{data}`_DIRECTIVE_YAML_OPTIONS_RE`), and escaped colon-fence directives are
un-escaped (see {data}`_ESCAPED_COLON_FENCE_RE`). Files without matching
patterns are left untouched.
"""
for arg in extra_args:
path = Path(arg)
if not path.is_file():
continue
content = path.read_text(encoding="UTF-8")
fixed = _DIRECTIVE_YAML_OPTIONS_RE.sub(_yaml_block_to_field_list, content)
fixed = _ESCAPED_COLON_FENCE_RE.sub(_unescape_colon_fence, fixed)
if fixed != content:
path.write_text(fixed, encoding="UTF-8")
logging.debug("Fixed MyST directives in %s", path)
# ---------------------------------------------------------------------------
# Binary checksums
# ---------------------------------------------------------------------------
CHECKSUMS: dict[str, dict[PlatformKey, str]] = {
"actionlint": {
(
LINUX,
AARCH64,
): "325e971b6ba9bfa504672e29be93c24981eeb1c07576d730e9f7c8805afff0c6",
(
LINUX,
X86_64,
): "8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8",
(
MACOS,
AARCH64,
): "aba9ced2dee8d27fecca3dc7feb1a7f9a52caefa1eb46f3271ea66b6e0e6953f",
(
MACOS,
X86_64,
): "5b44c3bc2255115c9b69e30efc0fecdf498fdb63c5d58e17084fd5f16324c644",
(
WINDOWS,
AARCH64,
): "cadcf7ea4efe3a68728893813643cebe1185e5b1d4be5b96245f65c9a4d5ea41",
(
WINDOWS,
X86_64,
): "6e7241b51e6817ea6a047693d8e6fed13b31819c9a0dd6c5a726e1592d22f6e9",
},
"biome": {
(
LINUX,
AARCH64,
): "27490d47af66420788b634afb48db23b588f272c8a284ba3daf706a5faa640ab",
(
LINUX,
X86_64,
): "7b5045d6d34f055df8ffe1bf3077164e6f6a24c45a41497d628a5e86d0e12fe7",
(
MACOS,
AARCH64,
): "f71fe80909d2f70f1e051320f5ba9dfd553bc5ef3bacef5cdee1b00ee96a285c",
(
MACOS,
X86_64,
): "887431b79e45758e05d94a89111af72b28e5d6545c92480ecac9247d8bacb321",
(
WINDOWS,
AARCH64,
): "655cc1f2ecf3719f79c9def7f2d824bb2a451fcd1d738d43468b12dd66620fd5",
(
WINDOWS,
X86_64,
): "62adea0ea523f04cc5c074b2bb00e748b97252023aede03196e1bf4aacf80a9c",
},
"gh": {
(
LINUX,
AARCH64,
): "73ea440ecad9c9e284429997ee6f93577bc6f7bc6fba357ef62c53ad8fb641a5",
(
LINUX,
X86_64,
): "a2c9b8497e1f85b1ad0dfcb78b5a622e098801b8e461e459e88e1ee12f018112",
(
MACOS,
AARCH64,
): "a58b8fd77b417a38f47a0b54d1370c59b0fcdb324ccc9ca002b0998f7c4c999e",
(
MACOS,
X86_64,
): "63298c998cc2a924c9e254c6af6a1caad6ece281122687a91f079bc0a462700e",
(
WINDOWS,
AARCH64,
): "3e2d4a166da4ee5020c592737b65eec0e724946d5d5b962f5fe59d99116dc4bf",
(
WINDOWS,
X86_64,
): "35d7fe05c4dd1411ffda1e73dfc7c6f44b75c936ca51fa6595c657fdc0350cec",
},
"gitleaks": {
(
LINUX,
AARCH64,
): "e4a487ee7ccd7d3a7f7ec08657610aa3606637dab924210b3aee62570fb4b080",
(
LINUX,
X86_64,
): "551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb",
(
MACOS,
AARCH64,
): "b40ab0ae55c505963e365f271a8d3846efbc170aa17f2607f13df610a9aeb6a5",
(
MACOS,
X86_64,
): "dfe101a4db2255fc85120ac7f3d25e4342c3c20cf749f2c20a18081af1952709",
(
WINDOWS,
AARCH64,
): "b95f5e4f5c425cedca7ee203d9afd29597e692c4924a12ed42f970537c72cc0f",
(
WINDOWS,
X86_64,
): "d29144deff3a68aa93ced33dddf84b7fdc26070add4aa0f4513094c8332afc4e",
},
"labelmaker": {
(
LINUX,
AARCH64,
): "4685e142da55150904d16624fe1052161de5dba1a859cddef19ab41833c37728",
(
LINUX,
X86_64,
): "d76f8e64f9671884dac1758fe54a28a6680c5d9bf0ffd593a2c68ba558cc49a2",
(
MACOS,
AARCH64,
): "a52a4e102f0760ce1632da5fdaee2b0debe0e6ddea577b88a94a60172fe85751",
(
MACOS,
X86_64,
): "dc8374d6a9bec4ebf143fb42e3024aeffabe8585bb9bd6f134cfaf0693be7688",
(
WINDOWS,
X86_64,
): "939195930f9d5fd2b15a5cf43497019a52083e6c6713807d3379de49395c2e10",
},
"lychee": {
(
LINUX,
AARCH64,
): "91a7bd65685da41b90ccb9bc867a3d649a7818042dae04ff405e55a25bddee4c",
(
LINUX,
X86_64,
): "1f4e0ef7f6554a6ed33dd7ac144fb2e1bbed98598e7af973042fc5cd43951c9a",
(
MACOS,
AARCH64,
): "c9d3740ea2d891854d37116c9fba840f37b6e7c89d330e7db84ac333631c4977",
(
WINDOWS,
X86_64,
): "32975d1493ee1a975d6bb41e4fb56fe419cb442ded628bb772ba2e614acfacad",
},
"oxipng": {
(
LINUX,
AARCH64,
): "97d168c6c0d1dbcb36e7438eb489804748a2ba40d94fe21aa7dab7372e9efe9b",
(
LINUX,
X86_64,
): "b33f84c73d42cb592bea5d84c431030b1e97784817693380dfcec7d9575f871e",
(
MACOS,
AARCH64,
): "9aad3927d095b6ade2aacb92b89ebaca442483c1f7cde5d7a2486b283c2ed5f9",
(
MACOS,
X86_64,
): "c45acf40a70cc02539c55555ac240bf5ef24544b7ea9959d22da19f606cec205",
(
WINDOWS,
X86_64,
): "a5ad52c9c288dc99c2eae90dcad73dee64e39bf3f5aa5303c0fb55ac9c5f069b",
},
"shfmt": {
(
LINUX,
AARCH64,
): "32d92acaa5cd8abb29fc49dac123dc412442d5713967819d8af2c29f1b3857c7",
(
LINUX,
X86_64,
): "fb096c5d1ac6beabbdbaa2874d025badb03ee07929f0c9ff67563ce8c75398b1",
(
MACOS,
AARCH64,
): "9680526be4a66ea1ffe988ed08af58e1400fe1e4f4aef5bd88b20bb9b3da33f8",
(
MACOS,
X86_64,
): "6feedafc72915794163114f512348e2437d080d0047ef8b8fa2ec63b575f12af",
(
WINDOWS,
X86_64,
): "60cd368533d0ad73fa86d93d5bbf95ef40587245ce684ed138c1b31557b5fe97",
},
"typos": {
(
LINUX,
AARCH64,
): "85c8b87b22a0fb1da130cd4d495e0beba7f1225eb580933184509e146ec4c509",
(
LINUX,
X86_64,
): "48bd2d58e02ce713b8c0f1aa239e68ee4f7d8c551013135806e6aed3938d9e10",
(
MACOS,
AARCH64,
): "8c0e7bd40b2b60c0b0cfe9f74dd814b4d4385c956ce86860f7da9e62d91fdc73",
(
MACOS,
X86_64,
): "4cecbf653a9fc45f023abf57f4e2e2f6b138c2d2387b09289beacdd3f0ea7bfd",
(
WINDOWS,
X86_64,
): "06d3a1b71c282e021671070696a72696d5c60ea485b47dc4f8f1fbcf90144d02",
},
}
"""Tool name to platform-keyed SHA-256 hex digest mapping.
Recomputed in place by `repomatic update-checksums` and `sync-tool-versions`.
Kept as a flat sidecar dict (rather than inline in each `BinarySpec`) so the
checksum recompute can replace a hash by exact string match without re-parsing
the registry, and so `VERSIONS` can anchor the offline staleness test.
"""
VERSIONS: dict[str, str] = {
"actionlint": "1.7.12",
"biome": "2.5.7",
"gh": "2.97.0",
"gitleaks": "8.30.1",
"labelmaker": "0.6.4",
"lychee": "0.24.2",
"oxipng": "10.2.0",
"shfmt": "3.13.1",
"typos": "1.49.0",
}
"""Tool name to the version each checksum set was computed for.
`test_tool_spec_integrity` asserts this equals the matching `ToolSpec.version`,
so a bump whose checksums were never refreshed (a stale `CHECKSUMS` entry) fails
CI offline, without downloading anything.
"""
# ---------------------------------------------------------------------------
# Tool registry
# ---------------------------------------------------------------------------
TOOL_REGISTRY: dict[str, ToolSpec] = {
"actionlint": ToolSpec(
name="actionlint",
version="1.7.12",
native_config_files=(".github/actionlint.yaml", ".github/actionlint.yml"),
config_flag="--config-file",
native_format=NativeFormat.YAML,
default_config="actionlint.yaml",
default_flags=("-color",),
source_url="https://github.com/rhysd/actionlint",
config_docs_url="https://github.com/rhysd/actionlint/blob/main/docs/config.md",
cli_docs_url="https://github.com/rhysd/actionlint/blob/main/docs/usage.md",
binary=BinarySpec(
urls={
(
LINUX,
AARCH64,
): "https://github.com/rhysd/actionlint/releases/download/v{version}/actionlint_{version}_linux_arm64.tar.gz",
(
LINUX,
X86_64,
): "https://github.com/rhysd/actionlint/releases/download/v{version}/actionlint_{version}_linux_amd64.tar.gz",
(
MACOS,
AARCH64,
): "https://github.com/rhysd/actionlint/releases/download/v{version}/actionlint_{version}_darwin_arm64.tar.gz",
(
MACOS,
X86_64,
): "https://github.com/rhysd/actionlint/releases/download/v{version}/actionlint_{version}_darwin_amd64.tar.gz",
(
WINDOWS,
AARCH64,
): "https://github.com/rhysd/actionlint/releases/download/v{version}/actionlint_{version}_windows_arm64.zip",
(
WINDOWS,
X86_64,
): "https://github.com/rhysd/actionlint/releases/download/v{version}/actionlint_{version}_windows_amd64.zip",
},
checksums=CHECKSUMS["actionlint"],
archive_format={
ALL_PLATFORMS: ArchiveFormat.TAR_GZ,
WINDOWS: ArchiveFormat.ZIP,
},
),
docs_notes=cleandoc(r"""
**Try it:**
```shell-session
$ repomatic run actionlint
```
**Minimal `[tool.actionlint]`:**
```toml
[tool.actionlint.self-hosted-runner]
labels = ["my-linux-runner"]
```
With no arguments actionlint lints every workflow under `.github/workflows`. The `[tool.actionlint]` section is bridged to a temporary YAML config: declaring self-hosted runner labels stops custom `runs-on:` values being flagged as unknown.
"""),
),
"autopep8": ToolSpec(
name="autopep8",
default_paths="python_files",
version="2.3.2",
source_url="https://github.com/hhatto/autopep8",
cli_docs_url="https://pypi.org/project/autopep8/",
reads_pyproject=True,
default_flags=(
"--recursive",
"--in-place",
"--max-line-length",
"88",
"--select",
"E501",
),
docs_notes=cleandoc(r"""
**Try it:**
```shell-session
$ repomatic run autopep8 -- .
```
autopep8 takes its configuration from CLI flags only. repomatic passes `--recursive --in-place --max-line-length 88 --select E501` by default; append more flags after `--`.
"""),
),
"awesome-lint": ToolSpec(
name="awesome-lint",
version="2.3.0",
npm=NpmSpec(),
cli_docs_url="https://github.com/sindresorhus/awesome-lint#usage",
),
"biome": ToolSpec(
name="biome",
default_args=(
"format",
"--write",
"--no-errors-on-unmatched",
# JSONC files: biome auto-detects the well-known ones, these
# cover the rest.
"--json-parse-allow-comments=true",
"--json-parse-allow-trailing-commas=true",
),
default_paths="json_files",
display_name="Biome",
version="2.5.7",
source_url="https://github.com/biomejs/biome",
tag_pattern=r"^@biomejs/biome@(?P<version>.+)$",
config_docs_url="https://biomejs.dev/reference/configuration/",
cli_docs_url="https://biomejs.dev/reference/cli/",
native_config_files=(
"biome.json",
"biome.jsonc",
".biome.json",
".biome.jsonc",
),
config_flag="--config-path",
config_after_subcommand=True,
native_format=NativeFormat.JSON,
binary=BinarySpec(
urls={
(
LINUX,
AARCH64,
): "https://github.com/biomejs/biome/releases/download/%40biomejs%2Fbiome%40{version}/biome-linux-arm64",
(
LINUX,
X86_64,
): "https://github.com/biomejs/biome/releases/download/%40biomejs%2Fbiome%40{version}/biome-linux-x64",
(
MACOS,
AARCH64,
): "https://github.com/biomejs/biome/releases/download/%40biomejs%2Fbiome%40{version}/biome-darwin-arm64",
(
MACOS,
X86_64,
): "https://github.com/biomejs/biome/releases/download/%40biomejs%2Fbiome%40{version}/biome-darwin-x64",
(
WINDOWS,
AARCH64,
): "https://github.com/biomejs/biome/releases/download/%40biomejs%2Fbiome%40{version}/biome-win32-arm64.exe",
(
WINDOWS,
X86_64,
): "https://github.com/biomejs/biome/releases/download/%40biomejs%2Fbiome%40{version}/biome-win32-x64.exe",
},
checksums=CHECKSUMS["biome"],
archive_format=ArchiveFormat.RAW,
),
docs_notes=cleandoc(r"""
**Try it:**
```shell-session
$ repomatic run biome -- check .
```
**Minimal `[tool.biome]`:**
```toml
[tool.biome.formatter]
indentStyle = "space"
```
`biome check` reports formatting and lint issues; add `--write` after `--` to apply fixes. The `[tool.biome]` section is bridged to a temporary `biome.json`, so keys keep Biome's camelCase spelling.
"""),
),
"bump-my-version": ToolSpec(
name="bump-my-version",
version="1.5.1",
reads_pyproject=True,
source_url="https://github.com/callowayproject/bump-my-version",
config_docs_url="https://callowayproject.github.io/bump-my-version/reference/configuration/",
cli_docs_url="https://callowayproject.github.io/bump-my-version/reference/cli/",
native_config_files=(".bumpversion.toml",),
config_flag="--config-file",
native_format=NativeFormat.TOML,
docs_notes=cleandoc(r"""
**Try it:**
```shell-session
$ repomatic run bump-my-version -- show-bump
```
**Minimal `[tool.bumpversion]`:**
```toml
[tool.bumpversion]
current_version = "1.2.3"
```
The configuration table is `[tool.bumpversion]`, not `[tool.bump-my-version]`: the section name predates the project's rename. `show-bump` previews the next versions without writing; `repomatic run bump-my-version -- bump minor` performs the bump.
"""),
),
"gh": ToolSpec(
name="gh",
display_name="GitHub CLI",
version="2.97.0",
source_url="https://github.com/cli/cli",
cli_docs_url="https://cli.github.com/manual/",
binary=BinarySpec(
urls={
(
LINUX,
AARCH64,
): "https://github.com/cli/cli/releases/download/v{version}/gh_{version}_linux_arm64.tar.gz",
(
LINUX,
X86_64,
): "https://github.com/cli/cli/releases/download/v{version}/gh_{version}_linux_amd64.tar.gz",
(
MACOS,
AARCH64,
): "https://github.com/cli/cli/releases/download/v{version}/gh_{version}_macOS_arm64.zip",
(
MACOS,
X86_64,
): "https://github.com/cli/cli/releases/download/v{version}/gh_{version}_macOS_amd64.zip",
(
WINDOWS,
AARCH64,
): "https://github.com/cli/cli/releases/download/v{version}/gh_{version}_windows_arm64.zip",
(
WINDOWS,
X86_64,
): "https://github.com/cli/cli/releases/download/v{version}/gh_{version}_windows_amd64.zip",
},
checksums=CHECKSUMS["gh"],
archive_format={
ALL_PLATFORMS: ArchiveFormat.TAR_GZ,
MACOS: ArchiveFormat.ZIP,
WINDOWS: ArchiveFormat.ZIP,
},
archive_executable="bin/gh",
# Linux and macOS archives nest everything under a
# `gh_{version}_{platform}_{arch}/` directory; the Windows zip puts
# `bin/gh.exe` at the root.
strip_components={ALL_PLATFORMS: 1, WINDOWS: 0},
),
docs_notes=cleandoc(r"""
**Try it:**
```shell-session
$ repomatic run gh -- --version
```
Pinned so the release lane gets the same `gh` everywhere. The manylinux container the Linux binaries compile in ships no `gh`, and the runner images that do ship one leave its version to the image. `gh` reads no project configuration: it authenticates from `GH_TOKEN` in the environment.
"""),
),
"gitleaks": ToolSpec(
name="gitleaks",
display_name="Gitleaks",
version="8.30.1",
source_url="https://github.com/gitleaks/gitleaks",
config_docs_url="https://github.com/gitleaks/gitleaks#configuration",
cli_docs_url="https://github.com/gitleaks/gitleaks#usage",
native_config_files=(".gitleaks.toml",),
config_flag="--config",
native_format=NativeFormat.TOML,
binary=BinarySpec(
urls={
(
LINUX,
AARCH64,
): "https://github.com/gitleaks/gitleaks/releases/download/v{version}/gitleaks_{version}_linux_arm64.tar.gz",
(
LINUX,
X86_64,
): "https://github.com/gitleaks/gitleaks/releases/download/v{version}/gitleaks_{version}_linux_x64.tar.gz",
(
MACOS,
AARCH64,
): "https://github.com/gitleaks/gitleaks/releases/download/v{version}/gitleaks_{version}_darwin_arm64.tar.gz",
(
MACOS,
X86_64,
): "https://github.com/gitleaks/gitleaks/releases/download/v{version}/gitleaks_{version}_darwin_x64.tar.gz",
(
WINDOWS,
AARCH64,
): "https://github.com/gitleaks/gitleaks/releases/download/v{version}/gitleaks_{version}_windows_arm64.zip",
(
WINDOWS,
X86_64,
): "https://github.com/gitleaks/gitleaks/releases/download/v{version}/gitleaks_{version}_windows_x64.zip",
},
checksums=CHECKSUMS["gitleaks"],
archive_format={
ALL_PLATFORMS: ArchiveFormat.TAR_GZ,
WINDOWS: ArchiveFormat.ZIP,
},
),
docs_notes=cleandoc(r"""
**Try it:**
```shell-session
$ repomatic run gitleaks -- dir .
```
**Minimal `[tool.gitleaks]`:**
```toml
[tool.gitleaks.extend]
useDefault = true
[tool.gitleaks.allowlist]
paths = ['''\.env\.sample$''']
```
`gitleaks dir .` scans the working tree; `gitleaks git` scans history instead. The `[tool.gitleaks]` section is bridged to a temporary `.gitleaks.toml`: keep `extend.useDefault = true`, or a custom config silently replaces the built-in rule set.
"""),
),
"labelmaker": ToolSpec(
name="labelmaker",
version="0.6.4",
source_url="https://github.com/jwodder/labelmaker",
cli_docs_url="https://github.com/jwodder/labelmaker",
binary=BinarySpec(
urls={
(
LINUX,
AARCH64,
): "https://github.com/jwodder/labelmaker/releases/download/v{version}/labelmaker-aarch64-unknown-linux-gnu.tar.xz",
(
LINUX,
X86_64,
): "https://github.com/jwodder/labelmaker/releases/download/v{version}/labelmaker-x86_64-unknown-linux-gnu.tar.xz",
(
MACOS,
AARCH64,
): "https://github.com/jwodder/labelmaker/releases/download/v{version}/labelmaker-aarch64-apple-darwin.tar.xz",
(
MACOS,
X86_64,
): "https://github.com/jwodder/labelmaker/releases/download/v{version}/labelmaker-x86_64-apple-darwin.tar.xz",
(
WINDOWS,
X86_64,
): "https://github.com/jwodder/labelmaker/releases/download/v{version}/labelmaker-x86_64-pc-windows-msvc.zip",
},
checksums=CHECKSUMS["labelmaker"],
archive_format={
ALL_PLATFORMS: ArchiveFormat.TAR_XZ,
WINDOWS: ArchiveFormat.ZIP,
},
strip_components=1,
),
docs_notes=cleandoc(r"""
labelmaker syncs a repository's issue and PR labels from a label-definition file, so unlike the linters it needs a target repository and a `GITHUB_TOKEN`, not a path in the working tree. There is no `[tool.labelmaker]` section: the label file is the configuration. See the [upstream usage docs](https://github.com/jwodder/labelmaker) for its flags and file schema.
"""),
),
"lychee": ToolSpec(
name="lychee",
display_name="Lychee",
version="0.24.2",
source_url="https://github.com/lycheeverse/lychee",
tag_pattern=r"^lychee-v(?P<version>.+)$",
config_docs_url="https://lychee.cli.rs/guides/config/",
cli_docs_url="https://lychee.cli.rs/guides/cli/",
native_config_files=("lychee.toml",),
config_flag="--config",
output_flag="--output",
native_format=NativeFormat.TOML,
# Since v0.24.0 (https://github.com/lycheeverse/lychee/issues/1930,
# https://github.com/lycheeverse/lychee/pull/2104), lychee natively reads
# [tool.lychee] from pyproject.toml, so repomatic skips the translation bridge.
reads_pyproject=True,
binary=BinarySpec(
urls={
(
LINUX,
AARCH64,
): "https://github.com/lycheeverse/lychee/releases/download/lychee-v{version}/lychee-aarch64-unknown-linux-gnu.tar.gz",
(
LINUX,
X86_64,
): "https://github.com/lycheeverse/lychee/releases/download/lychee-v{version}/lychee-x86_64-unknown-linux-gnu.tar.gz",
(
MACOS,
AARCH64,
): "https://github.com/lycheeverse/lychee/releases/download/lychee-v{version}/lychee-aarch64-apple-darwin.tar.gz",
(
WINDOWS,
X86_64,
): "https://github.com/lycheeverse/lychee/releases/download/lychee-v{version}/lychee-x86_64-pc-windows-msvc.zip",
},
checksums=CHECKSUMS["lychee"],
archive_format={
ALL_PLATFORMS: ArchiveFormat.TAR_GZ,
WINDOWS: ArchiveFormat.ZIP,
},
strip_components=1,
),
docs_notes=cleandoc(r"""
**Try it:**
```shell-session
$ repomatic run lychee -- .
```
**Minimal `[tool.lychee]`:**
```toml
[tool.lychee]
max_redirects = 5
```
lychee checks links found in the given path. Since v0.24 it reads `[tool.lychee]` from `pyproject.toml` natively, so repomatic does not translate it.
"""),
),
# TODO: add config_flag="--config" once upstream adds --config support.
# See https://github.com/hukkin/mdformat/issues/432
# and https://github.com/hukkin/mdformat/issues/562
"mdformat": ToolSpec(
name="mdformat",
default_paths="markdown_files",
per_file=True,
version="1.0.0",
source_url="https://github.com/hukkin/mdformat",
config_docs_url="https://mdformat.readthedocs.io/en/stable/users/configuration_file.html",
cli_docs_url="https://mdformat.readthedocs.io/en/stable/users/installation_and_usage.html",
native_config_files=(".mdformat.toml",),
native_format=NativeFormat.TOML,
default_config="mdformat.toml",
reads_pyproject=True,
default_flags=("--strict-front-matter",),
# No Python-formatting plugin here: ruff formats fenced Python blocks in
# Markdown natively, and the `format-python` job already runs it over doc
# files. A `mdformat-ruff` plugin would only cover the `python` info
# string (ruff also handles `py`, `py3`, `python3`, `pyi` and `pycon`) and
# would pin a second, independently-drifting ruff version in this
# environment, letting the two jobs fight over the same code blocks.
with_packages=(
"mdformat_admon==2.1.1",
"mdformat-config==0.2.1",
"mdformat_deflist==0.1.4",
"mdformat_footnote==0.1.3",
"mdformat-front-matters==2.0.0",
"mdformat-gfm==1.0.0",
"mdformat_gfm_alerts==2.1.0",
"mdformat_myst==0.3.0",
"mdformat-pelican==1.0.0",
"mdformat_pyproject==0.1.1",
"mdformat-recover-urls==0.0.2",
"mdformat-shfmt==0.2.0",
"mdformat_simple_breaks==0.1.0",
"mdformat-toc==0.5.0",
"mdformat-web==0.2.0",
),
# mdformat-shfmt shells out to `shfmt` rather than importing it, so the
# binary has to be on PATH. Routed through the registry to get the same
# pinned, checksum-verified build `repomatic run shfmt` uses.
path_tools=("shfmt",),
post_process=_fix_myst_directives,
check_flags=("--check",),
docs_notes=cleandoc(r"""
**Try it:**
```shell-session
$ repomatic run mdformat -- .
```
**Minimal `[tool.mdformat]`:**
```toml
[tool.mdformat]
wrap = "no"
```
mdformat rewrites Markdown in place. repomatic bundles a plugin set (GFM, MyST, front-matter, and others) and a baseline `mdformat.toml`; `[tool.mdformat]` in your `pyproject.toml` overrides it.
"""),
),
"mypy": ToolSpec(
name="mypy",
default_paths="python_files",
version="2.3.0",
source_url="https://github.com/python/mypy",
config_docs_url="https://mypy.readthedocs.io/en/stable/config_file.html",
cli_docs_url="https://mypy.readthedocs.io/en/stable/command_line.html",
# mypy also auto-discovers standalone `mypy.ini`/`.mypy.ini`, but those use
# INI syntax, which `NativeFormat` cannot represent β so `native_config_files`
# is intentionally left empty. `reads_pyproject=True` (for `[tool.mypy]`) plus
# `config_flag` cover how repomatic resolves mypy's configuration.
config_flag="--config-file",
reads_pyproject=True,
needs_venv=True,
default_flags=("--color-output",),
computed_params=lambda m: m.mypy_params or [],
docs_notes=cleandoc(r"""
**Try it:**
```shell-session
$ repomatic run mypy -- .
```
**Minimal `[tool.mypy]`:**
```toml
[tool.mypy]
strict = true
```
mypy runs inside the project virtualenv (via `uv run`) so it can import your dependencies. repomatic derives `--python-version` from `requires-python`, so the check matches your lowest supported interpreter. In a repository without a `uv.lock` there is no project virtualenv to freeze, so mypy runs in an isolated environment instead and only resolves the standard library: fine for standalone scripts, but dependency imports then report `import-not-found`.
`uv run` provisions only the default dependency groups, so a module that imports a dep declared solely in a non-default group (`docs`, `typing`, β¦) sees it as missing and mypy reports `import-not-found`. Either move the stub/dependency somewhere mypy resolves, or silence it with an override:
```toml
[[tool.mypy.overrides]]
module = "the_docs_only_package.*"
ignore_missing_imports = true
```
"""),
),
"nuitka": ToolSpec(
name="nuitka",
display_name="Nuitka",
version="4.1.3",
package="nuitka[onefile]",
source_url="https://github.com/Nuitka/Nuitka",
config_docs_url="https://nuitka.net/doc/user-manual.html",
cli_docs_url="https://nuitka.net/doc/user-manual.html",
needs_venv=True,
module="nuitka",
native_format=NativeFormat.FLAGS,
default_flags=("--mode=onefile", "--assume-yes-for-downloads"),
# Modules listed in [tool.repomatic] nuitka.nofollow-imports are kept
# out of the binary ("tkinter" by default, else boltons.ecoutils'
# guarded probe drags the whole Tcl/Tk stack into every CLI).
computed_params=lambda metadata: [
f"--nofollow-import-to={module}"
for module in metadata.config.nuitka_nofollow_imports
],
docs_notes=cleandoc(r"""
**Try it:**
```shell-session
$ repomatic run nuitka -- my_app/__main__.py
```
**Minimal `[tool.nuitka]`:**
```toml
[tool.nuitka]
onefile = true
output-dir = "build"
```
repomatic reads every key from `[tool.nuitka]` and forwards it as a CLI flag: `true` becomes a bare `--flag`, a string or number becomes `--key=value`, and a list repeats the flag once per item. Nuitka does not read `[tool.nuitka]` natively yet ([Nuitka#3909](https://github.com/Nuitka/Nuitka/issues/3909)); repomatic's bridge fills the gap until it does.
Binaries skip `tkinter` by default, via the `nuitka.nofollow-imports` setting of [`[tool.repomatic]`](configuration.md): set it to `[]` to bundle Tcl/Tk in a GUI project.
"""),
),
"oxipng": ToolSpec(
name="oxipng",
display_name="Oxipng",
version="10.2.0",
source_url="https://github.com/shssoichiro/oxipng",
cli_docs_url="https://github.com/shssoichiro/oxipng#usage",
binary=BinarySpec(
urls={
(
LINUX,
AARCH64,
): "https://github.com/shssoichiro/oxipng/releases/download/v{version}/oxipng-{version}-aarch64-unknown-linux-gnu.tar.gz",
(
LINUX,
X86_64,
): "https://github.com/shssoichiro/oxipng/releases/download/v{version}/oxipng-{version}-x86_64-unknown-linux-gnu.tar.gz",
(
MACOS,
AARCH64,
): "https://github.com/shssoichiro/oxipng/releases/download/v{version}/oxipng-{version}-aarch64-apple-darwin.tar.gz",
(
MACOS,
X86_64,
): "https://github.com/shssoichiro/oxipng/releases/download/v{version}/oxipng-{version}-x86_64-apple-darwin.tar.gz",
(
WINDOWS,
X86_64,
): "https://github.com/shssoichiro/oxipng/releases/download/v{version}/oxipng-{version}-x86_64-pc-windows-msvc.zip",
},
checksums=CHECKSUMS["oxipng"],
archive_format={
ALL_PLATFORMS: ArchiveFormat.TAR_GZ,
WINDOWS: ArchiveFormat.ZIP,
},
# Every archive nests its payload under `oxipng-{version}-{triple}/`,
# with the executable directly inside rather than in a `bin/`.
strip_components=1,
),
docs_notes=cleandoc(r"""
**Try it:**
```shell-session
$ repomatic run oxipng -- --opt 4 --strip safe image.png
```
Lossless PNG optimizer. `repomatic format-images` reaches it through {func}`repomatic.tool_runner.ensure_binary`, so the pinned, checksum-verified build is used instead of whatever the runner image or the distro archive supplies.
"""),
),
"pyproject-fmt": ToolSpec(
name="pyproject-fmt",
default_paths="pyproject_files",
version="2.27.0",
source_url="https://github.com/tox-dev/pyproject-fmt",
config_docs_url="https://pyproject-fmt.readthedocs.io/en/latest/",
cli_docs_url="https://pyproject-fmt.readthedocs.io/en/latest/",
native_config_files=("pyproject-fmt.toml",),
config_flag="--config",
native_format=NativeFormat.TOML,
reads_pyproject=True,
rewrite_exit_code=1,
docs_notes=cleandoc(r"""
**Try it:**
```shell-session
$ repomatic run pyproject-fmt -- pyproject.toml
```
**Minimal `[tool.pyproject-fmt]`:**
```toml
[tool.pyproject-fmt]
indent = 4
```
pyproject-fmt normalizes and reorders `pyproject.toml` in place. It reads its own `[tool.pyproject-fmt]` section natively.
"""),
),
"ruff": ToolSpec(
name="ruff",
display_name="Ruff",
version="0.16.2",
source_url="https://github.com/astral-sh/ruff",
config_docs_url="https://docs.astral.sh/ruff/configuration/",
cli_docs_url="https://docs.astral.sh/ruff/configuration/#command-line-interface",
native_config_files=(".ruff.toml", "ruff.toml"),
config_flag="--config",
native_format=NativeFormat.TOML,
default_config="ruff.toml",
reads_pyproject=True,
docs_notes=cleandoc(r"""
**Try it:**
```shell-session
$ repomatic run ruff -- check .
```
**Minimal `[tool.ruff]`:**
```toml
[tool.ruff]
line-length = 100
```
`ruff check .` lints; `ruff format .` reformats. Both read `[tool.ruff]` natively. With no project config, repomatic falls back to its bundled `ruff.toml` baseline.
"""),
),
"shfmt": ToolSpec(
name="shfmt",
default_paths="shfmt_files",
version="3.13.1",
source_url="https://github.com/mvdan/sh",
config_docs_url="https://github.com/mvdan/sh/blob/master/cmd/shfmt/shfmt.1.scd",
cli_docs_url="https://github.com/mvdan/sh#shfmt",
native_config_files=(".editorconfig",),
native_format=NativeFormat.EDITORCONFIG,
default_flags=("--write",),
binary=BinarySpec(
urls={
(
LINUX,
AARCH64,
): "https://github.com/mvdan/sh/releases/download/v{version}/shfmt_v{version}_linux_arm64",
(
LINUX,
X86_64,
): "https://github.com/mvdan/sh/releases/download/v{version}/shfmt_v{version}_linux_amd64",
(
MACOS,
AARCH64,
): "https://github.com/mvdan/sh/releases/download/v{version}/shfmt_v{version}_darwin_arm64",
(
MACOS,
X86_64,
): "https://github.com/mvdan/sh/releases/download/v{version}/shfmt_v{version}_darwin_amd64",
(
WINDOWS,
X86_64,
): "https://github.com/mvdan/sh/releases/download/v{version}/shfmt_v{version}_windows_amd64.exe",
},
checksums=CHECKSUMS["shfmt"],
archive_format=ArchiveFormat.RAW,
),
docs_notes=cleandoc(r"""
**Try it:**
```shell-session
$ repomatic run shfmt -- .
```
shfmt formats shell scripts in place. It has no `[tool.shfmt]` section: indentation and style come from `.editorconfig` (`indent_size`, `shell_variant`, and the `shfmt`-specific keys).
"""),
),
"typos": ToolSpec(
name="typos",
version="1.49.0",
source_url="https://github.com/crate-ci/typos",
config_docs_url="https://github.com/crate-ci/typos/blob/master/docs/reference.md",
cli_docs_url="https://github.com/crate-ci/typos/blob/master/docs/reference.md",
native_config_files=("typos.toml", "_typos.toml", ".typos.toml"),
config_flag="--config",
native_format=NativeFormat.TOML,
reads_pyproject=True,
default_flags=("--write-changes",),
binary=BinarySpec(
urls={
(
LINUX,
AARCH64,
): "https://github.com/crate-ci/typos/releases/download/v{version}/typos-v{version}-aarch64-unknown-linux-musl.tar.gz",
(
LINUX,
X86_64,
): "https://github.com/crate-ci/typos/releases/download/v{version}/typos-v{version}-x86_64-unknown-linux-musl.tar.gz",
(
MACOS,
AARCH64,
): "https://github.com/crate-ci/typos/releases/download/v{version}/typos-v{version}-aarch64-apple-darwin.tar.gz",
(
MACOS,
X86_64,
): "https://github.com/crate-ci/typos/releases/download/v{version}/typos-v{version}-x86_64-apple-darwin.tar.gz",
(
WINDOWS,
X86_64,
): "https://github.com/crate-ci/typos/releases/download/v{version}/typos-v{version}-x86_64-pc-windows-msvc.zip",
},
checksums=CHECKSUMS["typos"],
archive_format={
ALL_PLATFORMS: ArchiveFormat.TAR_GZ,
WINDOWS: ArchiveFormat.ZIP,
},
),
docs_notes=cleandoc(r"""
**Try it:**
```shell-session
$ repomatic run typos -- .
```
**Minimal `[tool.typos]`:**
```toml
[tool.typos.files]
extend-exclude = ["*.lock"]
```
typos scans the tree and, with repomatic's default `--write-changes`, fixes what it finds. It reads `[tool.typos]` natively; use `[tool.typos.default.extend-words]` to map project-specific terms to their intended spelling.
Because the `fix-typos` workflow job ships whatever typos rewrites as an unattended pull request, guard content where a "correction" is a corruption with `[tool.typos.default.extend-ignore-re]` patterns. The two known traps are encoded hashes, whose random letter runs typos happily respells (a Guix `(base32 "...")` source hash losing its value to an `an`-to-`and` fix), and intentional-typo examples that docs or tests exercise on purpose:
```toml
[tool.typos.default]
extend-ignore-re = [
'base32 "[0-9a-z]{52}"',
"\\{query\\}",
]
```
"""),
),
"yamllint": ToolSpec(
name="yamllint",
default_args=(".",),
version="1.38.0",
source_url="https://github.com/adrienverge/yamllint",
config_docs_url="https://yamllint.readthedocs.io/en/stable/configuration.html",
cli_docs_url="https://yamllint.readthedocs.io/en/stable/quickstart.html",
native_config_files=(
".yamllint",
".yamllint.yaml",
".yamllint.yml",
),
config_flag="--config-file",
default_config="yamllint.yaml",
default_flags=("--strict",),
ci_flags=("--format", "github"),
docs_notes=cleandoc(r"""
**Try it:**
```shell-session
$ repomatic run yamllint -- .
```
**Minimal `[tool.yamllint]`:**
```toml
[tool.yamllint.rules.line-length]
max = 120
```
yamllint has no native `pyproject.toml` support, so repomatic bridges `[tool.yamllint]` to a temporary YAML config passed via `--config-file`. With no project config it uses repomatic's strict bundled `yamllint.yaml`.
"""),
),
"zizmor": ToolSpec(
name="zizmor",
default_args=(".",),
version="1.29.0",
source_url="https://github.com/zizmorcore/zizmor",
config_docs_url="https://docs.zizmor.sh/configuration/",
cli_docs_url="https://docs.zizmor.sh/usage/",
native_config_files=(
".github/zizmor.yml",
".github/zizmor.yaml",
"zizmor.yml",
"zizmor.yaml",
),
config_flag="--config",
default_config="zizmor.yaml",
default_flags=("--offline",),
ci_flags=("--format", "github"),
docs_notes=cleandoc(r"""
**Try it:**
```shell-session
$ repomatic run zizmor -- .
```
zizmor audits GitHub Actions workflows for security issues, offline by default. repomatic bridges `[tool.zizmor]` to a temporary YAML config (passed via `--config`); with none, it uses the bundled `zizmor.yaml`. See the [configuration reference](https://docs.zizmor.sh/configuration/) for available keys.
"""),
),
}
# ---------------------------------------------------------------------------
# Documentation renderers
# ---------------------------------------------------------------------------
#
# Each renderer turns the registry above into a Markdown fragment consumed by a
# `{python:render}` block in `docs/tool-runner.md`, so the page documents the
# live registry on every Sphinx build. Nothing they emit is checked in: adding
# a tool to the registry is all it takes for the page to cover it. They live
# beside the registry rather than in a module of their own because they read
# nothing else, and a spec field gains its rendering in the same file it is
# declared in.
_PYPI_RELEASE_TOOLS = frozenset({"mdformat", "mypy"})
"""Tools that lack a usable GitHub "latest release" object.
mdformat has zero GitHub Releases; mypy only has pre-releases.
"""
def _github_repo(url: str | None) -> str | None:
"""Extract ``owner/repo`` from a GitHub URL."""
m = re.match(r"https?://github\.com/([^/]+/[^/]+)", url or "")
return m.group(1) if m else None
def _tool_badges(key: str, spec: ToolSpec) -> str:
"""Render the Stars and Last release badges of a tool's section.
Stars need a GitHub repository; the release badge picks the tool's
backend: GitHub release date when a repository exists, PyPI version for
the {data}`_PYPI_RELEASE_TOOLS` exceptions, npm version for npm tools.
Badges keep their default shields.io labels so they self-describe inline
(in the retired comparison table, column headers played that role).
"""
badges = []
repo = _github_repo(spec.source_url)
if repo:
badges.append(
f""
)
if key in _PYPI_RELEASE_TOOLS:
badges.append(
f""
)
elif spec.npm is not None:
badges.append(
f""
)
elif repo:
badges.append(
f""
)
return " ".join(badges)