Skip to content

rats.ci

Commands for building and validating components.

The rats-ci application provides a set of sub-commands that represent common operations done in the typical CI pipeline. Wrapping the CI logic into a cli application allows us to run these same checks locally during development, and reproduce any possible issues that fail the CI pipeline, avoiding a long development loop. The rats-ci command can be installed repo-wide in a monorepo, but the commands should be run from within a component, where the pyproject.toml file is located.

When using the rats-ci application within a monorepo, we can give developers a common API while adapting to the different technology choices within any given component.

CLI

The rats-ci command is broken up into build-image, check, fix, install, and test groups, which map to one or more default commands that can be customized by users. Any number of the provided commands can be run in sequence, so rats-ci fix check test is often used as a quick way to fix any linting errors automatically, when possible; running the linting and typing checks, as configured by the component being checked; and any unit tests, using pytest by default.

Usage: rats-ci [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]...

  commands used during ci/cd

Options:
  --help  Show this message and exit.

Commands:
  build-image  Build a container image of the component.
  check        Run any configured linting & typing checks for the component.
  config       Show information about the configured command groups for active
               component.
  fix          Run any configured auto-formatters for the component.
  install      Install the development environment for the component.
  test         Run any configured tests for the component.

Configuration

Info

The default configuration reflects the tools and options we use when developing rats. However, we want the entire development team to use a common set of commands, like rats-ci check to run linting and other type checking rules, regardless of any differences in language or configuration across components. Separating the implementation details from the CI/CD concepts allows the team to contribute across a growing code base without needing to memorize a new set of development commands; and without needing to force all components to follow a very rigid set of commands.

Apart from configuring each group with the settings below, we can configure all groups in one operation using the rats.ci.AppConfigs.COMMAND_GROUPS service.

You can run rats-ci config to see the current configuration for the ci commands within your component. If you haven't modified the defaults, you should see the configuration below.

$ cd rats-apps
$ rats-ci config
component: rats-apps
  install
    poetry install
  fix
    ruff check --fix --unsafe-fixes
    ruff format
  check
    ruff format --check
    ruff check
    pyright
  test
    pytest

install

We're currently using Poetry for dependency management, so the rats-ci install command maps simply to poetry install by default. In simple cases, this command might not be immediately necessary on most projects; but if installing all the development dependencies in a component requires more than one command, or you have components other than python packages, you can update the rats.ci.AppConfigs.INSTALL service to define your installation steps.

fix

The rats-ci fix command tries to make any linting or style fixes possible. Any kind of automated formatting tools that can remove tedious process for maintaining a common style across the project, and can avoid failing CI builds for uninteresting reasons that would create a long, annoying feedback loop. By default, we run ruff check --fix --unsafe-fixes and ruff format, and expect the component being fixed to have ruff installed and configured, as usual. The default behavior can be updated by setting rats.ci.AppConfigs.FIX.

check

Any linting and typing errors that can't be automatically fixed with rats-ci fix, can be detected with rats-ci check. We run ruff format --check, ruff check, and pyright, but these defaults can be changed with the rats.ci.AppConfigs.CHECK service.

test

We run our test suite with pytest, but occasionally register additional commands to run integration and end-to-end tests. The rats.ci.AppConfigs.TEST service can be provided to change the configured test suite.

build-container

The rats-ci build-container command builds and pushes a container image based on the value of the DEVTOOLS_IMAGE_REGISTRY and DEVTOOLS_IMAGE_TAG environment variables. If DEVTOOLS_IMAGE_TAG is not defined, we use the rats.projects module to calculate a tag based on the hash of all the files available to the build context. The image push can be disabled by setting the DEVTOOLS_IMAGE_PUSH_ON_BUILD=0 environment variable.

$ cd rats-apps
$ rats-ci build-image
building docker image: example.azurecr.io/rats-apps:8b79a4344354…
[+] Building 2.5s (14/14) FINISHED
 => [internal] load build definition from Containerfile
 => => transferring dockerfile: 1.41kB
 => [1/9] FROM mcr.microsoft.com/mirror/docker/library/ubuntu:24.04
 => [9/9] COPY . /opt/rats
 => exporting to image
 => => exporting layers
 => => writing image sha256:2150f859e…
 => => naming to example.azurecr.io/rats-apps:8b79a434…
5f70bf18a086: Layer already exists
ef2fe6e1db4c: Pushed
b02f35d52f65: Pushed
8b79a4…aa69d: digest: sha256:a2c3356…4c9ca size: 2199

Info

For legacy reasons, the rats-ci build-container command is not yet configurable in the same way the others are, but we hope to address this in a future release.

__all__ = ['AppConfigs', 'AppServices', 'Application', 'CiCommandGroups', 'main'] module-attribute

AppConfigs

COMMAND_GROUPS = apps.ServiceId[CiCommandGroups]('command-groups') class-attribute instance-attribute

Brings together the individual commands into a single config object.

Defining this service allows the entire configuration to be done in a single provider method, in case none of the defaults are desired.

from rats import apps, ci


class PluginContainer(apps.Container, apps.PluginMixin):

    @apps.service(ci.AppConfigs.COMMAND_GROUPS)
    def _cmd_groups(self) -> CiCommandGroups:
        return CiCommandGroups(
            install=tuple(
                tuple(["uv", "sync"]),
            ),
            fix=tuple(
                tuple(["ruff", "check", "--fix"]),
            ),
            check=tuple(
                tuple(["ruff", "check"]),
            ),
            test=tuple(
                tuple(["pytest"]),
            ),
        )

INSTALL = apps.ServiceId[Collection[str]]('install') class-attribute instance-attribute

Service group to define commands run with rats-ci install.

from rats import apps, ci


class PluginContainer(apps.Container, apps.PluginMixin):

    @apps.group(ci.AppConfigs.INSTALL)
    def _install_cmds(self) -> Iterator[tuple[str, ...]]:
        yield tuple(["uv", "sync"])

FIX = apps.ServiceId[Collection[str]]('fix') class-attribute instance-attribute

Service group to define commands run with rats-ci fix.

from rats import apps, ci


class PluginContainer(apps.Container, apps.PluginMixin):

    @apps.group(ci.AppConfigs.FIX)
    def _fix_cmds(self) -> Iterator[tuple[str, ...]]:
        yield tuple(["ruff", "check", "--fix"])

CHECK = apps.ServiceId[Collection[str]]('check') class-attribute instance-attribute

Service group to define commands run with rats-ci check.

from rats import apps, ci


class PluginContainer(apps.Container, apps.PluginMixin):

    @apps.group(ci.AppConfigs.CHECK)
    def _check_cmds(self) -> Iterator[tuple[str, ...]]:
        yield tuple(["ruff", "check")

TEST = apps.ServiceId[Collection[str]]('test') class-attribute instance-attribute

Service group to define commands run with rats-ci test.

from rats import apps, ci


class PluginContainer(apps.Container, apps.PluginMixin):

    @apps.group(ci.AppConfigs.TEST)
    def _test_cmds(self) -> Iterator[tuple[str, ...]]:
        yield tuple(["pytest")

Application(app)

Bases: apps.AppContainer, cli.Container, apps.PluginMixin

Main application for the rats-ci cli commands.

Not typically used directly, but can be invoked using rats.apps.AppBundle within tests or in advanced workflows.

from rats import apps, ci


ci_app = apps.AppBundle(app_plugin=ci.Application)
ci_app.install()
ci_app.fix()
ci_app.check()
ci_app.test()

Warning

Calling ci_app.execute() is unlikely to behave as expected, because sys.argv is parsed by the click library.

Source code in rats/apps/_app_containers.py
def __init__(self, app: Container) -> None:
    self._app = app

execute()

Parses sys.argv to run the rats-ci cli application.

Source code in src/rats/ci/_app.py
def execute(self) -> None:
    """Parses [sys.argv][] to run the `rats-ci` cli application."""
    cli.create_group(
        click.Group(
            "rats-ci",
            help="commands used during ci/cd",
            chain=True,  # allow us to run more than one ci subcommand at once
        ),
        self,
    ).main()

config()

Show information about the configured command groups for active component.

Refer to rats.ci for details on how to update these values.

Source code in src/rats/ci/_app.py
@cli.command()
def config(self) -> None:
    """
    Show information about the configured command groups for active component.

    Refer to [rats.ci][] for details on how to update these values.
    """
    selected_component = self._app.get(projects.PluginServices.CWD_COMPONENT_TOOLS)
    command_groups = self._app.get(AppConfigs.COMMAND_GROUPS)

    print(f"component: {selected_component.component_name()}")
    for group, cmds in command_groups._asdict().items():
        print(f"  {group}")
        for cmd in cmds:
            print(f"    {' '.join(cmd)}")

install()

Install the development environment for the component.

Refer to rats.ci.AppConfigs.INSTALL for details on how to update these values.

Source code in src/rats/ci/_app.py
@cli.command()
def install(self) -> None:
    """
    Install the development environment for the component.

    Refer to [rats.ci.AppConfigs.INSTALL][] for  details on how to update these values.
    """
    group = list(self._app.get_group(AppServices.INSTALL_EXES))
    for exe in group:
        exe.execute()

    if len(group) == 0:
        selected_component = self._app.get(projects.PluginServices.CWD_COMPONENT_TOOLS)
        command_groups = self._app.get(AppConfigs.COMMAND_GROUPS)

        for cmd in command_groups.install:
            selected_component.run(*cmd)

        print(f"ran {len(command_groups.install)} installation commands")

fix()

Run any configured auto-formatters for the component.

Refer to rats.ci.AppConfigs.FIX for details on how to update these values.

Source code in src/rats/ci/_app.py
@cli.command()
def fix(self) -> None:
    """
    Run any configured auto-formatters for the component.

    Refer to [rats.ci.AppConfigs.FIX][] for  details on how to update these values.
    """
    group = list(self._app.get_group(AppServices.FIX_EXES))
    for exe in group:
        exe.execute()

    if len(group) == 0:
        selected_component = self._app.get(projects.PluginServices.CWD_COMPONENT_TOOLS)
        command_groups = self._app.get(AppConfigs.COMMAND_GROUPS)
        for cmd in command_groups.fix:
            selected_component.run(*cmd)

        print(f"ran {len(command_groups.fix)} fix commands")

check()

Run any configured linting & typing checks for the component.

Refer to rats.ci.AppConfigs.CHECK for details on how to update these values.

Source code in src/rats/ci/_app.py
@cli.command()
def check(self) -> None:
    """
    Run any configured linting & typing checks for the component.

    Refer to [rats.ci.AppConfigs.CHECK][] for  details on how to update these values.
    """
    group = list(self._app.get_group(AppServices.CHECK_EXES))
    for exe in group:
        exe.execute()

    if len(group) == 0:
        selected_component = self._app.get(projects.PluginServices.CWD_COMPONENT_TOOLS)
        command_groups = self._app.get(AppConfigs.COMMAND_GROUPS)

        for cmd in command_groups.check:
            selected_component.run(*cmd)

        print(f"ran {len(command_groups.check)} check commands")

test()

Run any configured tests for the component.

Refer to rats.ci.AppConfigs.TEST for details on how to update these values.

Source code in src/rats/ci/_app.py
@cli.command()
def test(self) -> None:
    """
    Run any configured tests for the component.

    Refer to [rats.ci.AppConfigs.TEST][] for  details on how to update these values.
    """
    group = list(self._app.get_group(AppServices.TEST_EXES))
    for exe in group:
        exe.execute()

    if len(group) == 0:
        selected_component = self._app.get(projects.PluginServices.CWD_COMPONENT_TOOLS)
        command_groups = self._app.get(AppConfigs.COMMAND_GROUPS)

        for cmd in command_groups.test:
            selected_component.run(*cmd)

        print(f"ran {len(command_groups.test)} test commands")

build_image()

Build a container image of the component.

Source code in src/rats/ci/_app.py
@cli.command()
def build_image(self) -> None:
    """Build a container image of the component."""
    group = list(self._app.get_group(AppServices.BUILD_IMAGE_EXES))
    for exe in group:
        exe.execute()

    if len(group) == 0:
        project_tools = self._app.get(projects.PluginServices.PROJECT_TOOLS)
        selected_component = self._app.get(projects.PluginServices.CWD_COMPONENT_TOOLS)

        project_tools.build_component_image(selected_component.find_path(".").name)

AppServices

Service IDs used by the rats.ci.Application class.

The rats-ci application will move to using rats.apps.Executable classes to define the behavior of the various commands. You can replace the default behavior of any of the given commands by registering one or more executables to the appropriate service group. This approach gives you more control over the logic of the commands, and works well when the exposed configs in rats.ci.AppConfigs are not enough.

INSTALL_EXES = apps.ServiceId[apps.Executable]('install-exes') class-attribute instance-attribute

Service group of executables to run with rats-ci install.

FIX_EXES = apps.ServiceId[apps.Executable]('fix-exes') class-attribute instance-attribute

Service group of executables to run with rats-ci fix.

CHECK_EXES = apps.ServiceId[apps.Executable]('check-exes') class-attribute instance-attribute

Service group of executables to run with rats-ci check.

TEST_EXES = apps.ServiceId[apps.Executable]('test-exes') class-attribute instance-attribute

Service group of executables to run with rats-ci test.

BUILD_IMAGE_EXES = apps.ServiceId[apps.Executable]('build-image-exes') class-attribute instance-attribute

Service group of executables to run with rats-ci build-image.

CiCommandGroups

Bases: NamedTuple

Main configuration object for the rats-ci subcommands.

install instance-attribute

Set of commands meant to be run as part of rats-ci install.

fix instance-attribute

Set of commands meant to be run as part of rats-ci fix.

check instance-attribute

Set of commands meant to be run as part of rats-ci check.

test instance-attribute

Set of commands meant to be run as part of rats-ci test.

main()

The main entry-point for the application, used to define the python script for `rats-ci.

Source code in src/rats/ci/_app.py
def main() -> None:
    """The main entry-point for the application, used to define the python script for `rats-ci."""
    apps.run_plugin(logs.ConfigureApplication)
    apps.run_plugin(Application)