Tasks¶
A task is a named automation workflow, run from the CLI or from a CI job:
dothat run test-task
A task contains no logic of its own. It declares which actions to run and which parameters they receive, so the same definition serves local runs, CI jobs, and generated variants.
Defining a task¶
class CheckVersion(TaskBuilderImpl):
_basename = "check-version"
def apply(self):
return self.with_actions((PrintVersion,)).with_params((version(),))
_basename is the identifier used by the CLI and must be unique. It accepts an enum
value as well, which keeps the name in sync between the task and the CI job that runs
it:
class Tasks(Enum):
CHECK_VERSION = "check-version"
class CheckVersion(TaskBuilderImpl):
_basename = Tasks.CHECK_VERSION
Actions¶
An action implements a single operation and is reusable across tasks. Two base types are provided.
InteractivePythonAction for Python logic:
class PrintVersion(InteractivePythonAction):
def impl(self, *, version: str, **_):
LOGGER.info(version)
InteractiveShlex for shell commands, returned as argument lists so nothing is
re-parsed by a shell:
class CheckCommand(InteractiveShlex):
def impl(self, *, command: str, **_) -> list[list[str]]:
return [["echo", command]]
Task parameters arrive as keyword arguments; an action declares only what it needs and
absorbs the rest with **_. Actions also inherit self._config and
self._secret_handler from their base class without defining a constructor — see
Dependency injection.
Actions run sequentially, in declaration order:
self.with_actions((PrepareAction, BuildAction, PublishAction))
Parameters¶
A parameter defines a name, a CLI flag, a default, and optionally a type and an environment variable:
def version():
return Param(name="version", long="version", default="latest", type=str)
Values are resolved in increasing order of priority:
default ──► environment variable ──► configuration ──► CLI argument
Every key under [extra] in the project configuration is exposed to all tasks
automatically, without being declared:
[extra]
version = "1.2.3"
class PrintVersion(InteractivePythonAction):
def impl(self, *, extra__version: str, **_):
print(extra__version)
The __ separator represents the configuration hierarchy, so extra.version becomes
extra__version. The file is selected with the global --config flag and defaults to
environment.toml.
Task generators¶
A generator produces the tasks available to the CLI. Projects export the generator class — the container instantiates it:
class TaskGenerator(TaskGeneratorImpl):
task_check_version = CheckVersion
task_publish = PublishPackage
Attributes prefixed with task_ are discovered automatically. The module holding the
generator is named in the configuration:
[extra.system]
tasks_module = "_tasks"
The field is optional; without it only built-in tasks are available. Tasks from the project module override built-in tasks of the same name.
Variants¶
One implementation can produce several named tasks, which is how matrix-style CI configurations are built:
class TaskGenerator(TaskGeneratorImpl):
def load_tasks(self) -> Iterator[Task]:
yield from super().load_tasks()
for target in ("target-1", "target-2"):
yield (
self.builder(TestBaseTask)
.apply()
.with_name(target)
.with_params((self._params.ci(),))
.build()
)
This registers test-task:target-1 and test-task:target-2. self.builder() creates
the builder through the container — do not instantiate builders directly.
Method |
Purpose |
|---|---|
|
Creates a builder through the container |
|
Applies the task’s own configuration |
|
Adds a suffix to the task name |
|
Adds or overrides parameters |
|
Produces the final task |
Running tasks from CI¶
A CI job runs a task by name, which keeps the job definition thin:
class CheckVersion(JobBuilderImpl):
_name = "check-version"
_base = base.BaseTask
BaseTask supplies the script, so the generated job is:
check-version:
stage: jobs
script:
- dothat run ${CI_JOB_NAME}
${CI_JOB_NAME} lets one job template serve every variant: GitLab sets it to the job’s
own name, so test-task:target-1 runs the matching task with no per-job script.
CLI¶
dothat list # all registered tasks
dothat help check-version # parameters and documentation
dothat run check-version --version 2.0.0 # run, overriding a parameter
dothat --config other/environment.toml run check-version
dothat help shows where each parameter can come from:
test-task:target-1 test task
--ci (config: ci, environ: CI)
--flavor (config: flavor)
--insecure (config: insecure, opposite of secure)