rcmt

rcmt (short for Repository Configuration Management Tool) helps aligning configuration files across many repositories. It creates, modifies or deletes files in multiple repositories, then creates a Pull Request that contains the changes for each repository.

Quick start

Let’s say you want to make flake8 compatible with black. To accomplish this, the file .flake8 can be placed into every repository.

The content of .flake8 looks like this:

[flake8]
max-line-length = 88
extend-ignore = E203

Having to place this file in every repository, commit the change, create a pull request, wait for a build job to succeed and then merge the pull request is no fun. This is where rcmt can help.

First, you create a Package. A package is a directory that contains the file manifest.yaml. The manifest describes which Actions to apply to a repository. The directory can contain additional files that some actions need to work.

The basic directory structure of a package looks like this:

.
|____packages
| |____flake8
| | |____.flake8
| | |____manifest.py

And the Manifest looks like this:

from rcmt.package import Manifest
from rcmt.package.action import Own

# This name identifies the package.
# rcmt errors if two packages share the same name.
with Manifest(name="flake8") as manifest:
    # Actions tell rcmt what to do and how.
    manifest.add_action(
        # The action to apply. The "own" action lets rcmt
        # take ownership of the file.
        # rcmt resets the file if somebody changes it in a repository.
        Own(
            # Path to the source file rcmt should put in a repository.
            # This path is relative to the manifest.py file.
            source=".flake8",
            # Path to the target where rcmt writes the content of source.
            # This is relative to the root path of a repository.
            target=".flake8",
        )
    )

Packages describe what to do, but not to which repositories they apply. rcmt expects a matcher to do this:

from rcmt import Run
from rcmt.matcher import RepoName

# rcmt uses the name when committing changes.
with Run(name="python-defaults") as run:
    # Match all repositories of MyOrg.
    run.add_matcher(RepoName("github.com/MyOrg/.+"))
    # The name of a package to apply to all matching repositories.
    # "flake8" is the name set in the manifest.py file of the
    # flake8 package.
    run.add_package("flake8")

Everything is ready. Run rcmt:

rcmt run --packages ./packages ./run.py

rcmt will find all matching repositories, check them out locally, apply the package and create a Pull Request for each repository.