Configuration system
Bitrab accepts GitLab CI YAML and then layers its own local capability model on top.
Source formats
The runtime config is YAML, normally:
.gitlab-ci.yml.bitrab-ci.yml
The config path rule is centralized in bitrab.cli.resolve_config_path(), so run, validate, list, graph, debug, and watch all choose the same file.
Loader responsibilities
config.loader.ConfigurationLoader is responsible for:
- YAML parsing with
ruamel.yaml - include resolution
- recursive include deduplication
- raw dict merging
It is deliberately not responsible for semantic job interpretation.
Validation layers
bitrab validate combines three distinct checks:
- GitLab schema validation via
config.validate_pipeline.GitLabCIValidator - Capability diagnostics via
config.capabilities.check_capabilities() - Basic semantic checks in
cli.cmd_validate()
That layering explains a lot of code review questions:
- something can be valid GitLab YAML but still unsupported locally
- something can be warned-and-skipped instead of rejected
- some unsupported constructs fail in the loader before validation gets very far
Capability model
config.capabilities.py defines a two-level model:
DiagnosticLevel.ERRORDiagnosticLevel.WARNING
Practical meaning:
- ERROR means bitrab cannot safely emulate the feature locally and should stop
- WARNING means the feature is ignored or only partially meaningful locally, but keeping one shared
.gitlab-ci.ymlis still useful
Examples:
| Feature | Current behavior |
|---|---|
include: component |
error |
trigger: |
error |
inputs: |
error |
image: / services: |
warning, ignored |
workflow: rules |
supported; can skip pipeline execution |
resource_group: |
supported with cross-process local locks |
environment: |
warning |
rules: changes |
supported with local baseline semantics |
Defaults and overrides
PipelineProcessor._process_default_config() normalizes the top-level default: block into DefaultConfig.
Then _process_job() applies precedence roughly like this:
global variables
-> default variables
-> job variables
For scripts:
- job
before_scriptoverrides defaultbefore_script - job
after_scriptoverrides defaultafter_script
This is intentionally closer to GitLab's inheritance model than simple concatenation.
extends:
PipelineProcessor._resolve_extends() resolves inheritance before job dataclasses are created.
Behavior:
- accepts a string or list of parents
- hidden jobs like
.baseare valid templates - parent dictionaries deep-merge
- lists and scalars are replaced, not merged
- circular references raise
GitlabRunnerError
Includes
Loader include behavior:
- string include -> local file path
local:-> local file pathremote:/url:-> HTTP fetchtemplate:/project:-> skippedcomponent:-> hard error
The merge policy is recursive dict merge in _merge_configs(), with the current file overriding included values.
Rule expressions
config.rules._evaluate_if() supports a practical subset:
$VAR$VAR == "value"$VAR != "value"$VAR =~ /regex/$VAR !~ /regex/- top-level
&&and||
rules: exists is implemented with filesystem glob checks. rules: changes uses the shared git-aware change resolver;
see the user-facing differences page for its baseline ladder.
GitLab !reference nodes are represented as placeholders during YAML parsing, then resolved against the fully merged
include graph before extends processing. Remote URL includes have a short-lived atomic cache; vendor snapshots remain
the explicit offline/reproducible mechanism.
Configuration outside YAML
Bitrab also reads pyproject.toml for local execution behavior in mutation.py:
parallel_backenduse_git_worktreesworktree_rootserialwarn_on_mutation- mutation whitelist patterns
That means runtime behavior is split between pipeline config and local project config.