Skip to content

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:

  1. GitLab schema validation via config.validate_pipeline.GitLabCIValidator
  2. Capability diagnostics via config.capabilities.check_capabilities()
  3. 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.ERROR
  • DiagnosticLevel.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.yml is 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_script overrides default before_script
  • job after_script overrides default after_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 .base are 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 path
  • remote: / url: -> HTTP fetch
  • template: / project: -> skipped
  • component: -> 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_backend
  • use_git_worktrees
  • worktree_root
  • serial
  • warn_on_mutation
  • mutation whitelist patterns

That means runtime behavior is split between pipeline config and local project config.