Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 20 additions & 8 deletions docs/CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
- - [Optional fields](#optional-fields)
- - - [before](#before-liststring)
- - - [after](#after-liststring)
- - - [docker_container](#docker_container-string)
- - - [cleanup](#cleanup-liststring)
- - - [default_docker_container](#default_docker_container-string)
- - - [docker_run_options](#docker_run_options-string)
- [Containers](#containers)
- - [Containers, based on images from repo](#containers-based-on-images-from-repo),
Expand Down Expand Up @@ -48,9 +49,10 @@ name = "test"
description = "runs unit tests"
before = ["echo This is just an example", "echo You should configure your tests here"]
execute = "pytest"
after =["echo done > /dev/null"]
after = ["echo tests completed"]
cleanup = ["rm -f .coverage"]
examples = ["tests --cov -vv", "tests --last-failed"]
docker_container = "alpine"
default_docker_container = "alpine"
docker_run_options = "-it -v .:/app -w /app"
```

Expand Down Expand Up @@ -105,18 +107,28 @@ then resulting command, which will be executed by `runo` will be: `pytest --cov
Before executing the main part, you may want to perform some setup
(activate `venv` in case of Python project). In this section you can
specify list of commands/scripts, which should be executed before
the main part. Example:
the main part. These commands are executed inside the container
(or natively if no container is used). Example:
- `["source /tmp/.venv/bin/activate", "echo Starting tests"]`


#### `after` (`list[string]`)

Similar to `before`, but executed after the main part. Example:
- `["rm -f /tmp/unneeded_files*"]`
Similar to `before`, but executed after the main part. These commands
are executed inside the container (or natively if no container is used),
which makes them useful for post-execution actions that need access
to the container environment. Example:
- `["echo tests completed", "echo generating report"]`


Please note that these commands are executed on host machine (not inside container).
#### `cleanup` (`list[string]`)

Cleanup commands to run on the host machine after the container exits
(or after native execution completes). Useful for removing temporary files
or performing other host-side cleanup. Example:
- `["rm -f /tmp/unneeded_files*"]`

#### `docker_container` (`string`)
#### `default_docker_container` (`string`)

Name of the container (defined in the same config), which should be used
by the command by default. It can be overwritten via CLI at moment of run
Expand Down
80 changes: 59 additions & 21 deletions runo
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ from typing import (
Union,
)

__version__ = "26.01.29-716a0e17"
__version__ = "26.02.03-1ec55b48"

# runo uses .toml format of config files, but parsers for this format
# might be not available in old python versions (only in 3.11 tomllib
Expand Down Expand Up @@ -83,20 +83,24 @@ name = "test"
description = "runs unit tests"
# OPTIONALLY, you can specify actions, which you want to perform before the main command.
# For python projects, you may want to activate venv here.
# These commands are executed inside the container (or natively if no container is used).
before = ["echo This is just an example", "echo You should configure your tests here"]
# Actual command to run (can be single command, or script).
# You can pass additional options to this executable via console (see 'examples' section)
execute = "echo ALL TESTS PASSED"
# OPTIONALLY, you can specify actions, which you want to perform after the main command
# to do cleanup
after = ["echo done > /dev/null"]
# OPTIONALLY, you can specify actions to perform after the main command, but still inside
# the container. Useful for generating coverage reports or other post-test actions.
after = ["echo generating coverage report"]
# OPTIONALLY, you can specify cleanup actions to perform on the host machine after
# the container exits. Useful for removing temporary files.
cleanup = ["echo done > /dev/null"]
# OPTIONALLY, you can specify examples of command usage.
# if missing, ./runo will auto generate single example.
examples = ["tests --cov -vv", "tests --last-failed"]
## OPTIONALLY you can specify container, in which command should be executed by default.
## OPTIONALLY you can specify default container, in which command should be executed.
## Container must be defined in the same file.
## It can be overwritten, or set from CLI as well.
# docker_container = "alpine"
## It can be overwritten, or set from CLI as well with '-c'/'--container' option.
# default_docker_container = "alpine"
## OPTIONALLY, you can specify 'docker run' options, which should be used at command execution.
## See official docker documentation: https://docs.docker.com/reference/cli/docker/container/run/
# docker_run_options = "-it -v .:/app -w /app"
Expand All @@ -105,32 +109,32 @@ examples = ["tests --cov -vv", "tests --last-failed"]
name = "build"
description = "builds the project"
execute = "echo Build is running"
after = ["echo done"]
#docker_container = "alpine"
cleanup = ["echo done"]
#default_docker_container = "alpine"
#docker_run_options = "-it -v .:/app -w /app"

[[commands]]
name = "shell"
description = "debug container by running shell in interactive mode (keep container running)"
execute = "/bin/sh"
docker_container = "alpine"
default_docker_container = "alpine"
docker_run_options = "-it -v .:/app -w /app"

[[commands]]
name = "pre-commit"
description = "quick checks/fixes of code formatting (ruff/mypy)"
execute = "echo Ruff is formatting the code"
after = ["echo Formatting completed"]
cleanup = ["echo Formatting completed"]
#execute = "scripts/pre_commit.sh"
#docker_container = "alpine"
#default_docker_container = "alpine"
#docker_run_options = "-v .:/app -w /app"

[[commands]]
name = "update-deps"
description = "updates dependencies, used in project, to the latest versions"
execute = "echo Often it is pain, but if you will script it and put here, it will be super easy"
#execute = "scripts/update-requirements.sh"
#docker_container = "alpine"
#default_docker_container = "alpine"
#docker_run_options = "-v .:/app -w /app"

#######################################################
Expand Down Expand Up @@ -627,7 +631,27 @@ def _generate_command_to_run(command_cfg: dict, command_options: List[str]) -> s

execute = " ".join([command_cfg["execute"]] + command_options)

return _generate_bin_sh_cmd(before + [execute])
after = command_cfg.get("after", [])

return _generate_bin_sh_cmd(before + [execute] + after)


def _get_default_container_from_config(command_cfg: dict) -> Optional[str]:
"""
Get the default container name from command config.
Supports both 'default_docker_container' (preferred) and 'docker_container' (deprecated).
If 'docker_container' is used, logs a deprecation warning.
"""
if "default_docker_container" in command_cfg:
return command_cfg["default_docker_container"]
if "docker_container" in command_cfg:
_logger.error(
"DEPRECATION WARNING: 'docker_container' section in config is deprecated "
"and will be removed after 2027-01-01. "
"Please rename it to 'default_docker_container'."
)
return command_cfg["docker_container"]
return None


def _containers_to_use(
Expand All @@ -643,9 +667,8 @@ def _containers_to_use(
This function returns resulting list of containers, which should be used
to handle the given command run.
"""
containers_from_command_config = (
[command_cfg["docker_container"]] if "docker_container" in command_cfg else []
)
default_container = _get_default_container_from_config(command_cfg)
containers_from_command_config = [default_container] if default_container else []
containers_to_use = containers_requested_via_cli or containers_from_command_config

if len(containers_to_use) == 1 and containers_to_use[0] == "*":
Expand All @@ -656,9 +679,9 @@ def _containers_to_use(


def _generate_cleanup_cmd(command_cfg: dict) -> Optional[str]:
after = command_cfg.get("after")
if after:
return _generate_bin_sh_cmd(after)
cleanup = command_cfg.get("cleanup")
if cleanup:
return _generate_bin_sh_cmd(cleanup)
return None


Expand Down Expand Up @@ -791,6 +814,12 @@ class Validator:
if present_conflicting:
field_errors.append(f"conflicting fields found: {present_conflicting}")

requires_one_of = self._schema[field].get("requires_one_of_fields", set())
if requires_one_of and not requires_one_of.intersection(set(data.keys())):
field_errors.append(
f"requires one of the following fields to be present: {sorted(requires_one_of)}"
)

if field_errors:
errors[field].extend(field_errors)

Expand Down Expand Up @@ -823,16 +852,25 @@ _command_validator = Validator(
"before": dict(value_type=list, required=False),
"execute": dict(value_type=str, required=True),
"after": dict(value_type=list, required=False),
"cleanup": dict(value_type=list, required=False),
"examples": dict(value_type=list, required=False),
# Docker section
# DEPRECATED: 'docker_container' will be removed on 2027-01-01.
# Use 'default_docker_container' instead.
"docker_container": dict(
value_type=str,
required=False,
excludes_fields={"default_docker_container"},
),
"default_docker_container": dict(
value_type=str,
required=False,
excludes_fields={"docker_container"},
),
"docker_run_options": dict(
value_type=str,
required=False,
requires_fields={"docker_container"},
requires_one_of_fields={"docker_container", "default_docker_container"},
),
}
)
Expand Down
14 changes: 7 additions & 7 deletions runo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,30 @@ before = ["source /tmp/runo_venv/.venv/bin/activate", "ln -s runo runo.py"]
execute = "pytest"
# clean all files, generated by test (__pycache__/.coverage),
# except of htmlcov and pytest_cache, which are useful for us
after = ["rm -f runo.py", "rm -rf .coverage", "rm -rf tests/unit/__pycache__", "rm -rf __pycache__"]
cleanup = ["rm -f runo.py", "rm -rf .coverage", "rm -rf tests/unit/__pycache__", "rm -rf __pycache__"]
examples = ["test --cov -vv", "test --last-failed"]
docker_container = "python39"
default_docker_container = "python39"
docker_run_options = "-it -v .:/app -w /app"

[[commands]]
name = "shell"
description = "debug container by running shell in interactive mode (keep container running)"
execute = "/bin/sh"
docker_container = "python39"
default_docker_container = "python39"
docker_run_options = "-it -v .:/app -w /app"

[[commands]]
name = "pre-commit"
description = "quick checks/fixes of code formatting (ruff/mypy)."
execute = "scripts/pre_commit.sh"
docker_container = "python39"
default_docker_container = "python39"
docker_run_options = "-v .:/app -w /app"

[[commands]]
name = "update-deps"
description = "updates dependencies, used in project, to the latest versions"
execute = "scripts/update_deps.sh"
docker_container = "python39"
default_docker_container = "python39"
docker_run_options = "-v .:/app -w /app"

[[commands]]
Expand All @@ -42,7 +42,7 @@ description = "runs 'mypy' static type checker"
before = ["source /tmp/runo_venv/.venv/bin/activate"]
execute = "mypy"
examples = ["mypy runo tests"]
docker_container = "python39"
default_docker_container = "python39"
docker_run_options = "-v .:/app -w /app"

[[commands]]
Expand All @@ -51,7 +51,7 @@ description = "runs 'ruff' (checker/formatter)"
before = ["source /tmp/runo_venv/.venv/bin/activate"]
execute = "ruff"
examples = ["ruff check runo tests", "ruff format runo tests --check"]
docker_container = "python39"
default_docker_container = "python39"
docker_run_options = "-v .:/app -w /app"

[[docker_containers]]
Expand Down
49 changes: 42 additions & 7 deletions tests/e2e/runo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,23 @@ name = "example_native_multi"
description = "example of command, executing 2 linux commands natively on your OS"
before = ["echo BE", "echo FOR"]
execute = "echo PASSED"
after = ["echo AFTER"]
# e2e_tests = ["example_native_multi|BE\nFOR\nPASSED\nAFTER\n|0"]
after = ["echo AFTER_IN_CMD"]
cleanup = ["echo CLEANUP"]
# e2e_tests = ["example_native_multi|BE\nFOR\nPASSED\nAFTER_IN_CMD\nCLEANUP\n|0"]

[[commands]]
name = "example_native_after_only"
description = "example of command with only 'after' (runs in same shell as execute)"
execute = "echo MAIN"
after = ["echo POST_EXECUTE"]
# e2e_tests = ["example_native_after_only|MAIN\nPOST_EXECUTE\n|0"]

[[commands]]
name = "example_native_cleanup_only"
description = "example of command with only 'cleanup' (runs on host after main command)"
execute = "echo MAIN"
cleanup = ["echo HOST_CLEANUP"]
# e2e_tests = ["example_native_cleanup_only|MAIN\nHOST_CLEANUP\n|0"]

[[commands]]
name = "example_native_script"
Expand All @@ -29,42 +44,62 @@ execute = "dev/say.sh"
[[commands]]
name = "example_docker_file_single"
description = "example of command, executing 1 linux command in docker container, defined with docker file"
docker_container = "container_defined_by_docker_file"
default_docker_container = "container_defined_by_docker_file"
docker_run_options = "-it -v .:/app -w /app --user 1000:1000"
execute = "grep 'PRETTY_NAME' /etc/os-release"
# e2e_tests = ["example_docker_file_single|PRETTY_NAME=\"Alpine Linux v3.20\"\r\n|0"]

[[commands]]
name = "example_docker_file_multi"
description = "example of command, executing 2 linux commands in docker container, defined with docker file"
docker_container = "container_defined_by_docker_file"
default_docker_container = "container_defined_by_docker_file"
docker_run_options = "-it -v .:/app -w /app --user 1000:1000"
before = ["echo Your OS:"]
execute = "grep 'PRETTY_NAME' /etc/os-release"
# e2e_tests = ["example_docker_file_multi|Your OS:\r\nPRETTY_NAME=\"Alpine Linux v3.20\"\r\n|0"]

[[commands]]
name = "example_docker_file_after"
description = "example of 'after' running inside docker container (after execute). Shows RUNO_CONTAINER_NAME is set in container but not on host."
default_docker_container = "container_defined_by_docker_file"
docker_run_options = "-it -v .:/app -w /app --user 1000:1000"
before = ["echo CONTAINER_NAME_IN_BEFORE: $RUNO_CONTAINER_NAME"]
execute = "echo CONTAINER_NAME_IN_EXECUTE: $RUNO_CONTAINER_NAME"
after = ["echo CONTAINER_NAME_IN_AFTER: $RUNO_CONTAINER_NAME"]
cleanup = ["echo CLEANUP_RUNO_CONTAINER_NAME_IS_EMPTY: ${RUNO_CONTAINER_NAME:-true}"]
# e2e_tests = ["example_docker_file_after|CONTAINER_NAME_IN_BEFORE: container_defined_by_docker_file\r\nCONTAINER_NAME_IN_EXECUTE: container_defined_by_docker_file\r\nCONTAINER_NAME_IN_AFTER: container_defined_by_docker_file\r\nCLEANUP_RUNO_CONTAINER_NAME_IS_EMPTY: true\n|0"]

[[commands]]
name = "example_docker_file_script"
description = "example of command, executing script in docker container, defined with docker file"
docker_container = "container_defined_by_docker_file"
default_docker_container = "container_defined_by_docker_file"
docker_run_options = "-it -v .:/app -w /app --user 1000:1000"
examples = ["example_docker_file_script say_hello"]
before = ["echo Your OS:", "grep 'PRETTY_NAME' /etc/os-release"]
execute = "dev/say.sh"
# e2e_tests = ["example_docker_file_script hello_from_docker|Your OS:\r\nPRETTY_NAME=\"Alpine Linux v3.20\"\r\nyou asked to print 'hello_from_docker'\r\n|0"]

# Test backward compatibility with deprecated 'docker_container' field
[[commands]]
name = "example_docker_file_deprecated"
description = "example using deprecated docker_container field (backward compatibility test)"
docker_container = "container_defined_by_docker_file"
docker_run_options = "-it -v .:/app -w /app --user 1000:1000"
execute = "grep 'PRETTY_NAME' /etc/os-release"
# e2e_tests = ["example_docker_file_deprecated|PRETTY_NAME=\"Alpine Linux v3.20\"\r\n|0"]

[[commands]]
name = "example_docker_compose_simple"
description = "example of command, exicuting simple command from docker compose service"
docker_container = "container_defined_by_docker_compose"
default_docker_container = "container_defined_by_docker_compose"
docker_run_options = "--quiet --build -i"
execute = "echo PASSED"
# e2e_tests = ["example_docker_compose_simple|PASSED\n|0"]

[[commands]]
name = "example_docker_compose_test_client"
description = "example of command, which depends on docker compose service"
docker_container = "container_defined_by_docker_compose"
default_docker_container = "container_defined_by_docker_compose"
docker_run_options = "--quiet --build -i"
execute = "./dev/client.py"
# e2e_tests = ["example_docker_compose_test_client|Server is alive\n|0"]
Expand Down
Loading