repomatic.init_project module

Bundled data files, configuration templates, and repository initialization.

Provides a unified interface for accessing bundled data files from repomatic/data/ and orchestrates repository bootstrapping via repomatic init.

Every component repomatic init accepts is declared in COMPONENTS, which carries each one’s description, default scope and target paths; repomatic init --help lists them. That tuple is the only roster: a list repeated here would silently fall behind it.

Selectors use the same component[/file] syntax as the exclude config option in [tool.repomatic]. Qualified entries like skills/repomatic-topics select a single file within a component.

repomatic.init_project.BREAKING_BULLET_RE = re.compile('^[ \\t]*[-*][ \\t]+(?P<bullet>\\*\\*(?:Breaking|Deprecated):\\*\\*.*)$', re.MULTILINE)

A release-notes bullet announcing a breaking change or a deprecation.

The changelog convention opens these bullets with a bold **Breaking:** or **Deprecated:** label, and each GitHub release body repeats its changelog section verbatim. The upgrade report lifts them above the full notes, because they are the entries a repository may have to act on.

repomatic.init_project.INIT_WARNING_RE = re.compile('^[ \\t]*warning:.*$', re.IGNORECASE|re.MULTILINE)

A warning line in the output of repomatic init.

Matches the warning: lines the configuration loader logs, like an unknown [tool.repomatic] key, and the Warning: lines of the closing summary.

repomatic.init_project.PYPROJECT_FMT_INDENT = 2

Spaces pyproject-fmt puts before each row of a multi-line array, its default.

_graft_local_additions lays out a rebuilt multi-line array at this indent. tomlrt’s own fallback is four spaces, which the format-pyproject job rewrites in a pull request of its own.

repomatic.init_project.RUNTIME_FRAGMENTS: tuple[str, ...] = ('release.yaml', 'vt-trend-chart.js')

Bundled files loaded by repomatic at runtime, not deployed verbatim.

These files live in repomatic/data/ so they ship in the wheel and are discoverable via get_data_content(), but repomatic init never copies them as-is. release.yaml is the canonical caller repomatic.github.workflow_sync reads to assemble each downstream release.yaml, copying its jobs and rewriting the local uses: refs (see _generate_release_caller); the deployed release.yaml is generated, not this bundled copy. vt-trend-chart.js is the detections-chart script repomatic.release.binaries_page.render_chart_section splices into docs/binaries.md with its payload placeholders filled. New entries must be added explicitly so the data-file registry tests stay authoritative.

repomatic.init_project.EXPORTABLE_FILES: dict[str, str | None] = {'_release-engine.yaml': '.github/workflows/release.yaml', 'action-publish-pypi.yaml': '.github/actions/publish-pypi/action.yaml', 'actionlint.yaml': None, 'agent-grunt-qa.md': '.claude/agents/grunt-qa.md', 'agent-qa-engineer.md': '.claude/agents/qa-engineer.md', 'agent-sphinx-docs.md': '.claude/agents/sphinx-docs.md', 'autofix.yaml': '.github/workflows/autofix.yaml', 'autolock.yaml': '.github/workflows/autolock.yaml', 'bumpversion.toml': None, 'cancel-runs.yaml': '.github/workflows/cancel-runs.yaml', 'changelog.yaml': '.github/workflows/changelog.yaml', 'coverage.toml': None, 'debug.yaml': '.github/workflows/debug.yaml', 'docs.yaml': '.github/workflows/docs.yaml', 'labels.toml': 'labels.toml', 'labels.yaml': '.github/workflows/labels.yaml', 'lint.yaml': '.github/workflows/lint.yaml', 'lychee.toml': None, 'mdformat.toml': None, 'metrics.yaml': '.github/workflows/metrics.yaml', 'mypy.toml': None, 'pytest.toml': None, 'release.yaml': None, 'ruff.toml': None, 'tests.yaml': '.github/workflows/tests.yaml', 'typos.toml': None, 'unsubscribe.yaml': '.github/workflows/unsubscribe.yaml', 'uv.toml': None, 'vt-trend-chart.js': None, 'yamllint.yaml': None, 'zizmor.yaml': None}

Registry of all exportable files: maps filename to default output path.

None means the file is bundled but not directly written to a target path by repomatic init (used for pyproject.toml templates that need merging, tool-runner default configs, and runtime fragments).

repomatic.init_project.export_content(filename)[source]

Get the content of any exportable bundled file.

Parameters:

filename (str) – The filename (like “ruff.toml” or “release.yaml”).

Return type:

str

Returns:

Content of the file as a string.

Raises:
repomatic.init_project.init_config(config_type, pyproject_path=None)[source]

Initialize a configuration by merging it into pyproject.toml.

Reads the pyproject.toml file, checks if the tool section already exists, and if not, inserts the bundled template at the appropriate location.

The template is stored in native format (without [tool.X] prefix) and is parsed by tomlrt and added under the [tool] table.

Parameters:
  • config_type (str) – The configuration type (like "ruff" or "bumpversion").

  • pyproject_path (Path | None) – Path to pyproject.toml. Defaults to ./pyproject.toml.

Return type:

str | None

Returns:

The modified pyproject.toml content, or None if no changes needed.

Raises:

ValueError – If the config type is not supported.

repomatic.init_project.default_version_pin()[source]

Derive the default version pin from __version__.

Strips any .dev0 suffix and prefixes with v. For example, "5.10.0.dev0" becomes "v5.10.0".

Return type:

str

repomatic.init_project.resolve_default_pin(config, *, repo='kdeldycke/repomatic', today=None, warnings=None, floor=None)[source]

Resolve the upstream pin, holding a fresh release back by cooldown.

Returns the (version, commit_sha) init stamps into thin-caller uses: refs. In the common case, and on any datasource failure, this is the running repomatic version paired with its build-time SHA. Only when adopting a release still inside the [tool.repomatic] minimum-release-age window does the pin step back to the newest cooldown-cleared release (see _select_cooldown_pin), resolving that tag’s SHA afresh.

Important

The cooldown may hold back an adoption. It may never rewrite a pin the repository already carries, which is what floor records.

init is the only writer of these refs: sync-action-pins skips every slug in UPSTREAM_REPO_SLUGS, and ACTION_PIN_RE does not even match a subpath-carrying reusable-workflow ref. So a downstream repository adopts a new repomatic release one way: a newer init runs. A maintainer runs one by hand, and the CI sync-repomatic job runs one through init --upgrade, which picks the release itself (see resolve_upgrade_target()) and bypasses this function. Two things follow.

A pin equal to the running version is not a decision to gate. Without a newer release to adopt, the CI sync-repomatic job runs init at the pinned version itself, so base equals floor; re-judging it there downgrades the repository once a week after each hand-bump.

A pin below the running version is a skew. init renders caller content from the running version, so a ref naming an older release ships that content against an older reusable-workflow surface, which GitHub rejects as soon as the two disagree (see test_thin_caller_workflow_call_inputs_stay_minimal). Returning such a pin is therefore only half a decision: run_init() reads it back and, when the repository already carries workflows, skips regenerating them so the tree stays coherent at the pin it keeps. A first-time adoption has no tree to keep, so there the skew stands as the only alternative to writing no workflows at all.

Parameters:
  • config (Config) – Repomatic config supplying the minimum-release-age window.

  • repo (str) – Upstream owner/repo whose releases gate the pin.

  • today (date | None) – Reference date for the cooldown; defaults to the current UTC date.

  • warnings (list[str] | None) – When provided, a cooldown note is appended here (in addition to being logged), so run_init can surface it in the final init summary rather than only mid-run.

  • floor (UpstreamRefPin | None) – The highest upstream pin already committed downstream, from _highest_upstream_pin. None for a repository carrying none, the one case the cooldown may step back freely.

Return type:

tuple[str, str | None]

Returns:

(version_pin, commit_sha). commit_sha is None when no SHA can be resolved, leaving a bare tag pin.

class repomatic.init_project.UpgradeTarget(version, released, previous=None, held_back=None)[source]

Bases: object

The release init --upgrade runs as, chosen before any file is written.

version: str

Bare version to run init as.

released: str

PyPI upload date of version, as YYYY-MM-DD. Empty when the datasource does not list the release pinned on disk.

previous: str | None = None

Bare version of the highest upstream pin on disk before the run, or None for a repository carrying none.

held_back: Candidate | None = None

The newest release above version that the minimum-release-age window still holds back: the one a later run adopts.

property in_process: bool

Whether the running release is the target, so no hand-off is needed.

repomatic.init_project.resolve_upgrade_target(config, output_dir, *, repo='kdeldycke/repomatic', today=None)[source]

Pick the release init --upgrade runs as.

That is the newest final, non-yanked release on PyPI older than the [tool.repomatic] minimum-release-age window, and never one below the upstream pin the repository already carries: a release pinned by hand inside the window stays. PyPI is the datasource because uvx installs from it, so a version whose publication failed, with a tag but no wheel, is never picked.

Parameters:
  • config (Config) – Repomatic config supplying the window and both opt-outs.

  • output_dir (Path) – Root of the target repository, read for its current pin.

  • repo (str) – Upstream owner/repo; its name is the PyPI package.

  • today (date | None) – Reference date for the cooldown; defaults to the current UTC date.

Return type:

UpgradeTarget | None

Returns:

The target, or None when the upgrade does not apply: turned off by upstream-pin.sync or workflow.sync, run from a build that is not a release, or no release has cleared the window.

repomatic.init_project.upgrade_command(version, init_args, *, config, repo='kdeldycke/repomatic', today=None)[source]

Build the command running init as another repomatic release.

--no-cooldown hands the child the decision resolve_upgrade_target() already took. Left to judge the window again, the child reads GitHub release dates where the parent read PyPI ones, and a split verdict near the limit writes the new configs but leaves the callers at the old release.

The window reaches the resolution as --exclude-newer, which gates the child’s dependency tree like any other install. The package itself carries the self-pin exemption, because it already cleared this repository’s window: with that window disabled, no cutoff is passed, and the workflow’s own UV_EXCLUDE_NEWER would refuse the release.

Parameters:
  • version (str) – Bare version to run.

  • init_args (Sequence[str]) – Arguments for the child init, after --no-cooldown.

  • config (Config) – Repomatic config supplying the minimum-release-age window.

  • repo (str) – Upstream owner/repo; its name is the package and the command.

  • today (date | None) – Reference date for the cutoff; defaults to the current UTC date.

Return type:

list[str]

Returns:

The command line.

repomatic.init_project.run_upgrade(command, output_dir)[source]

Run the init hand-off built by upgrade_command().

Runs from output_dir, where the child finds [tool.repomatic] as the parent did. Both output streams are merged in the order they were written, then captured, because upgrade_report() quotes the warnings the child printed: a config key the new release no longer knows shows up nowhere else.

Parameters:
Return type:

CompletedProcess[str]

Returns:

The finished process, its merged output in stdout.

repomatic.init_project.upgrade_invite(previous, adopted)[source]

Render the invitation to review a move of the upstream pin.

Shared by every pull request that moves the pin, the init --upgrade report and a sync-workflow-pins lockstep bump, so both hand over the same launcher.

Parameters:
  • previous (str) – Bare version the pin moved from.

  • adopted (str) – Bare version the pin moved to.

Return type:

str

Returns:

The upgrade-invite section, ending on the skill launcher.

repomatic.init_project.upgrade_report(target, output_dir, config, *, init_output='', repo='kdeldycke/repomatic', today=None)[source]

Describe the upgrade a run just applied, for its pull request body.

Reads the pin back from disk rather than trusting target, since what the run wrote is what the pull request carries. The sections, in reading order:

  • The version move, with its compare link and release date.

  • Every **Breaking:** and **Deprecated:** bullet of the releases in between.

  • The warnings the run printed, then its whole output, collapsed.

  • The release notes, one collapsed block per release.

  • The invitation to run the repomatic-upgrade review.

  • The newer release the window still holds back, if any.

Parameters:
  • target (UpgradeTarget) – The target resolve_upgrade_target() picked.

  • output_dir (Path) – Root of the target repository, read for the new pin.

  • config (Config) – Repomatic config supplying the minimum-release-age window.

  • init_output (str) – The output of a hand-off run, empty for an in-process one.

  • repo (str) – Upstream owner/repo whose releases are described.

  • today (date | None) – Reference date for relative dates; defaults to the current UTC date.

Return type:

tuple[str, str] | None

Returns:

(adopted, markdown), or None when the run moved no pin forward.

class repomatic.init_project.InitResult(created=<factory>, updated=<factory>, skipped=<factory>, excluded=<factory>, excluded_existing=<factory>, unmodified_configs=<factory>, removed_prunable=<factory>, removed_review=<factory>, warnings=<factory>, previous_pin=None, adopted_pin=None)[source]

Bases: object

Result of a repository initialization run.

created: list[str]

Relative paths of newly created files.

updated: list[str]

Relative paths of existing files overwritten with new content.

skipped: list[str]

Relative paths of skipped (already existing) files.

excluded: list[str]

Exclude entries that were applied.

excluded_existing: list[str]

Relative paths of excluded files that still exist on disk.

unmodified_configs: list[str]

Relative paths of config files identical to bundled defaults.

removed_prunable: list[tuple[str, str]]

(relative_path, successor) for on-disk orphans of dropped assets whose content matches the last-shipped version (safe to auto-delete).

removed_review: list[tuple[str, str]]

(relative_path, successor) for on-disk orphans of dropped assets that differ from the last-shipped version (locally modified: reported for manual review, never auto-deleted).

warnings: list[str]

Warning messages emitted during initialization.

previous_pin: str | None = None

Bare version of the highest upstream uses: pin the repository carried before the run, read only when a workflow component was selected. None for a first adoption, or for a run generating no workflow.

adopted_pin: str | None = None

Bare version the run wrote its thin callers at, or None when the workflow sync was skipped.

upgraded_pin()[source]

(previous, adopted) when the run moved the upstream pin forward.

The pair init’s closing hint and the upgrade review are keyed on. A first adoption has no previous release to review against, a regeneration at the pinned version moves nothing, and a rollback is a deliberate step back no upgrade review should chase, so all three answer None.

Return type:

tuple[str, str] | None

repomatic.init_project.prune_paths(paths, output_dir, *, prune_parents=True)[source]

Delete every path of an InitResult report section.

The deleting half of init’s delete flags (--delete-excluded, --delete-unmodified, the removed-asset pruning), kept beside run_init(), which produced the paths: the CLI decides which sections get deleted, this module owns the filesystem mutation.

Parameters:
  • paths (Sequence[str] | Sequence[tuple[str, str]]) – Bare relative paths, or (path, successor) pairs for the removed-asset sections.

  • output_dir (Path) – Repository root the paths are relative to.

  • prune_parents (bool) – Also remove parent directories left empty. On for the removed-asset and excluded sections, whose targets sit in directories repomatic itself created (.claude/skills/<name>/). Off for unmodified tool configs, which share .github/ and the repository root with files repomatic does not own.

Return type:

None

repomatic.init_project.adopted_ongoing_configs(output_dir)[source]

Return the ongoing tool configs whose section pyproject.toml already carries.

EXPLICIT governs adoption, not upkeep: it keeps a bare init from pushing [tool.typos] onto a repository that never asked for one. Once the section is there the repository has asked, so an ONGOING component rejoins the bare-init set and resumes tracking the bundled template.

Without this the two flags cancel out. The only sync that ever runs unattended is the bare init the sync-repomatic job calls, so an ONGOING section is otherwise re-derived only when a human types its component name, and a [tool.typos] written by hand sits indefinitely beside a bundled template it never adopts a single rule from.

BOOTSTRAP components stay out: their template is a starting point the repository owns outright after the first write, and re-selecting one would revert deliberate local edits.

Parameters:

output_dir (Path) – Repository root holding pyproject.toml.

Return type:

set[str]

Returns:

Component names to add to a bare init selection. Empty when the file is absent, unparsable, or carries no [tool] table.

repomatic.init_project.run_init(output_dir, components=(), version=None, cooldown=True, repo='kdeldycke/repomatic', repo_slug=None, config=None)[source]

Bootstrap a repository for use with kdeldycke/repomatic.

Creates thin-caller workflow files, exports configuration files, and generates a minimal changelog.md if missing. Managed files (workflows, configs, skills) are always overwritten. User-owned files (changelog.md, zizmor.yaml) are created once and never overwritten.

For awesome-* repositories, the awesome-template component is auto-included when no explicit component selection is made.

Note

Scope exclusions (RepoScope.AWESOME_ONLY, PYTHON_ONLY) and user-config exclusions ([tool.repomatic] exclude) only apply during bare repomatic init. When components are explicitly named on the CLI, scope is bypassed: the caller knows what they asked for. This allows workflows to materialize out-of-scope configs at runtime (like repomatic init publish-pypi-action in a non-Python repo).

Parameters:
  • output_dir (Path) – Root directory of the target repository.

  • components (Sequence[str]) – Components to initialize. Empty means all defaults. When non-empty, scope and user-config exclusions are bypassed.

  • version (str | None) – Version pin for upstream workflows (like v5.10.0). When None, derived from the running package version, gated by cooldown.

  • cooldown (bool) – When True (and version is unset), hold the derived pin back to the newest release past the [tool.repomatic] minimum-release-age window instead of pinning a fresh running version (see resolve_default_pin()). Ignored when version is explicit.

  • repo (str) – Upstream repository containing reusable workflows.

  • repo_slug (str | None) – Repository owner/name slug for awesome-template URL rewriting. Auto-detected via Metadata if not provided.

  • config (Config | None) – The resolved [tool.repomatic] configuration. Loaded from the current directory when omitted, so a caller working against another tree must pass the config it read from there.

Return type:

InitResult

Returns:

Summary of created, updated, skipped, and warned items.

repomatic.init_project.is_source_repo(output_dir)[source]

Detect whether output_dir is the repomatic source repository root.

Returns True when output_dir contains the repomatic Python package source tree (repomatic/__init__.py and repomatic/data/). Only the upstream source repo has these. This prevents auto-exclusion from deleting files that are the source of truth (skills, opt-in workflows, bundled configs).

Note

Detection is based on output_dir contents, not on __file__, because uvx --from . installs the package into a temp venv where __file__ no longer points to the source checkout.

Return type:

bool

repomatic.init_project.AWESOME_TEMPLATE_SLUG = 'kdeldycke/awesome-template'

Source slug embedded in bundled awesome-template files, rewritten at sync time.

repomatic.init_project.init_awesome_template(output_dir, repo_slug, result)[source]

Copy bundled awesome-template files and rewrite URLs.

Copies all files from the repomatic/data/awesome_template/ bundle into output_dir and rewrites kdeldycke/awesome-template URLs in .github/ markdown and YAML files to match repo_slug.

Every copied file is recorded on result by its own relative path, the way the skills and agents trees already are. The roll-up stays a log line: those lists are consumed as paths (--delete-excluded joins them against output_dir), so a "awesome-template (12 files)" summary sitting among them would be a path that resolves nowhere.

Parameters:
  • output_dir (Path) – Root directory of the target repository.

  • repo_slug (str) – Target owner/name slug for URL rewriting.

  • result (InitResult) – InitResult accumulator for created/updated files.

Return type:

None