Pipeline Presets
Define reusable pipeline configurations in .glciconfig.toml and run them with -p:
[pipelines.quick]
stages = ["lint", "test"]
context = "merge_request"
env = { CI_QUICK = "1" }
concurrency = 4
[pipelines.quick.skip]
jobs = ["slow-integration"]
job_patterns = [".*windows.*"]
[pipelines.full]
no_skip_all = true
manual_all = true
concurrency = 0
Usage#
glci run -p quick # run only lint + test, skip windows jobs
glci run -p full # run everything, including manual jobs
glci show -p quick # visualize what the preset would run
glci run -p quick --skip extra # CLI flags are additive with preset
Preset fields#
| Field | Description |
|---|---|
extends | Inherit from another preset by name |
stages | Only run jobs in these stages (additive with jobs) |
jobs | Only run these specific jobs, supports globs (additive with stages) |
context | CI context (branch, merge_request, tag) or named context preset |
mr_source, mr_target | MR source/target branches |
env | Variable overrides (key-value map) |
concurrency | Max parallel jobs (0 = unlimited) |
image | Default Docker image |
executor | Executor type (docker, kubernetes) |
no_skip_all | Disable all top-level skip rules |
manual_all | Auto-run all manual jobs |
no_manual | Skip manual jobs |
dirty | Include uncommitted files (pointer: not set = inherit default) |
Composable presets with extends#
Use extends to build tiered presets where each tier inherits from the previous one:
[pipelines.tier1]
jobs = ["lint-yaml", "haml-lint", "qa:selectors"]
context = "merge_request"
[pipelines.tier2]
extends = "tier1"
jobs = ["rubocop", "eslint-changed-files"]
[pipelines.tier3]
extends = "tier2"
jobs = ["static-analysis 1/2", "static-analysis 2/2"]
Running glci run -p tier3 includes all jobs from tier1, tier2, and tier3.
Merge semantics:
| Field type | Behavior |
|---|---|
jobs, stages | Additive — union of parent and child (deduplicated) |
skip, manual, token | Merged — lists are additive, child booleans override |
env | Merged — child keys override parent keys on conflict |
Scalars (context, image, executor, etc.) | Child wins if set, otherwise inherited from parent |
Boolean flags (no_skip_all, manual_all, etc.) | Either layer setting true enables the flag |
dirty | Child wins if set, otherwise inherited from parent |
Constraints:
extendsnames a single preset (string, not array).- Maximum chain depth is 5 levels.
- Circular references are detected at config load time.
Skip rules in presets#
Each preset can define its own skip rules, which are additive with the top-level [skip] section:
[pipelines.fast.skip]
jobs = ["slow-integration", "e2e-full"]
job_patterns = [".*windows.*"]
stages = ["deploy"]
Manual rules in presets#
Control which manual jobs auto-run:
[pipelines.deploy.manual]
auto_run = true # auto-run ALL manual jobs
jobs = ["deploy-staging"] # or specific job names
job_patterns = ["deploy-.*"] # or regex patterns
Token forwarding in presets#
Override token forwarding rules per preset:
[pipelines.deploy.token]
forward_host_token = true
stages = ["deploy"]
jobs = ["release"]
job_patterns = [".*-publish$"]
Glob patterns#
The jobs field supports * as a glob metacharacter:
[pipelines.tier3]
jobs = [
"rubocop", # exact match
"lint-*-yaml", # matches "lint-doc-yaml", "lint-ci-yaml", ...
"docs-lint *", # matches "docs-lint 1/4", "docs-lint 2/4", ...
"qa:*", # matches "qa:selectors", "qa:internal-links", ...
"static-analysis *", # matches all parallel shards
]
context = "merge_request"
Globs also work in CLI positional args (quote to prevent shell expansion):
glci run 'qa:*'
glci run 'static-analysis *'
Only * is a glob metacharacter. ?, [, ] are literal so matrix job names like test: [ruby 3.0] work as exact matches. Entries without * are exact matches with O(1) lookup.
Full example#
A more complete preset showing all available sections:
[pipelines.deploy-staging]
stages = ["build", "test", "deploy"]
context = "branch=staging"
mr_source = "feature/deploy"
mr_target = "staging"
env = { DEPLOY_ENV = "staging", CI_DEBUG = "1" }
concurrency = 2
image = "ruby:3.2"
no_skip_all = true
manual_all = true
dirty = false
[pipelines.deploy-staging.skip]
jobs = ["e2e-windows"]
[pipelines.deploy-staging.manual]
jobs = ["deploy-staging"]
[pipelines.deploy-staging.token]
stages = ["deploy"]
Viewing effective config#
Use glci config to see how your presets are resolved after merging global and project configs:
glci config # show merged config including all presets
glci config --network # show network/TLS settings
glci config --gitlab # show resolved GitLab instance config
Additive stages + jobs#
When both stages and jobs are set in a preset, they are combined as a union: the resulting job set includes every job that matches either filter. This makes it easy to start with a broad stage and pull in individual jobs from other stages:
[pipelines.lint-plus]
stages = ["lint"] # all lint-stage jobs
jobs = ["workhorse:verify: [1.25]"] # plus this one from the test stage
[pipelines.lint-plus.skip]
jobs = ["semgrep-sast", "secret_detection"] # minus the heavy ones
The resolved set is (jobs in lint stage) ∪ (workhorse:verify: [1.25]) − skip.
When only one of stages or jobs is set, it acts as a standalone filter (unchanged behavior). skip always subtracts from the resolved set regardless of how a job was included.
Merging behavior#
Preset skip/manual/token rules are additive with top-level [skip]/[manual]/[token]. CLI flags (--stage, --skip, --env, etc.) override preset scalar values and are additive for list values.
The precedence for scalar settings is: CLI flag > preset > parent preset > global config > default. For list settings (skip jobs, manual jobs, etc.), all layers are merged together.
When using extends, the full inheritance chain is resolved before merging with top-level config and CLI flags.