CI/CD Variables, Predefined Variables, File Variables, Masking, Protection, Expansion, and Precedence: Concepts, Architecture, and Mental Model
GitLab CI/CD variables are not one flat dictionary. Their meaning depends on where they are defined, when they become available, which source has precedence, whether they are ordinary or file type, whether a ref is protected, and which expansion mechanism consumes them. This lesson builds that state model before any precedence experiment.
Learning objectives
- Explain variables as provenance-bearing data: definition source, scope, type, protection, visibility, expansion policy, availability phase, precedence, and consumer.
- Distinguish pre-pipeline, pipeline, and job-only predefined variables and explain why a job-only value cannot decide whether the pipeline or job exists.
- Describe the current precedence chain from policy/manual-job/pipeline variables through project/group/instance/dotenv/YAML/deployment/predefined sources.
- Distinguish ordinary environment variables from file type variables and explain why a file variable exposes a temporary path rather than the stored value directly.
- Explain masking, hiding, and protection as different controls and state why masking is not a data-loss-prevention boundary.
1. The problem: identical names can represent different state
A pipeline can contain DEPLOY_TARGET in four or more
places and still produce one effective value in a job. The
repository may define a default, a job may override it, the project
settings may define the same key, and a manually started pipeline
may pass another value. If you inspect only
.gitlab-ci.yml, you can be looking at a
lower-precedence value that never reaches the job.
Variables also exist at different times. Some predefined values
exist before pipeline creation, some while GitLab constructs the
pipeline, and some only after a runner starts the job. A value that
exists only in the job environment cannot retroactively influence
include:rules or decide whether that job was added to
the graph.
2. Mental model: variable provenance becomes an effective value
flowchart TD
A[Definition sources] --> B[Scope + type + protection]
B --> C[Availability phase]
C --> D[Precedence resolution]
D --> E[Compiled pipeline / job environment]
E --> F[GitLab or Runner expansion]
F --> G[Shell expansion / tool consumption]
G --> H[Logs, files, reports, external side effects]
H --> I[Exposure and audit evidence]
The diagram is causal. Definition source and access controls determine whether a value is eligible. Availability timing determines whether GitLab can use it during compilation. Precedence chooses among eligible values with the same key. Expansion can then happen in GitLab, in Runner, or in the execution shell. Finally, job code can consume, transform, log, upload, or accidentally exfiltrate the result.
3. Every variable has multiple dimensions
| Dimension | Questions to ask | Example evidence |
|---|---|---|
| Source | YAML, project, group, instance, pipeline/manual job, policy, dotenv, deployment, predefined? | Settings location or YAML path; pipeline input/source; policy identity. |
| Scope | Project/group/instance/environment/ref? | Project path, environment scope, protected-ref state. |
| Type | Ordinary environment value or file type? | UI/API variable type; job receives value or temporary file path. |
| Visibility | Visible, masked, or masked-and-hidden? | Settings metadata; never reveal a hidden or sensitive value. |
| Protection | Available only to protected refs? | Protected branch/tag state plus presence/absence test. |
| Expansion | Literal or allowed to reference another variable? | variables:expand or UI expand setting. |
| Phase | Pre-pipeline, pipeline, or job-only? | Predefined-variable reference and the keyword that consumes it. |
| Precedence | Which eligible source wins this key? | Controlled synthetic matrix and effective non-secret output. |
4. Predefined variables have availability phases
GitLab documents three availability phases.
Pre-pipeline values exist before the pipeline is
created and are the only predefined variables available to
include:rules. Pipeline values exist
while GitLab constructs the pipeline and can participate in job
rules. Job-only values arrive only
when a runner executes a job.
| Phase | What exists | What it can influence | What it cannot influence |
|---|---|---|---|
| Pre-pipeline | Values such as project/default-branch/ref context documented for that phase. | Includes and pipeline construction where supported. | Runner-only facts that do not exist yet. |
| Pipeline | Pipeline source and other values available while graph is created. | Job rules and graph decisions. | Job-only runtime data. |
| Job-only | Job ID, runner-specific/job runtime facts and other documented job-only values. | Scripts/tools after the job starts. | Whether the pipeline/job was created. |
5. Current precedence is a ladder, not “nearest YAML wins”
GitLab resolves duplicate keys by source precedence. The current
documentation places policy variables at the top for the jobs those
policies add, then manual-job variables, then pipeline variables,
project variables, group variables, instance variables, dotenv
variables, YAML variables, deployment variables, and most predefined
variables. Within YAML, rules:variables outranks job
variables, which outrank workflow:rules:variables,
which outrank top-level default variables.
| Priority | Source | Important nuance |
|---|---|---|
| 1 | Pipeline/scan execution policy variables | Applies to policy-added jobs according to current policy behavior. |
| 2 | Manual job variables | Supplied when running a manual job. |
| 3 | Pipeline variables | Run-pipeline UI, schedule, API/trigger, push option, forwarded upstream values. |
| 4 | Project variables | Can override all YAML values with the same key. |
| 5 | Group variables | Closest subgroup wins among duplicate group definitions. |
| 6 | Instance variables | Self-Managed/Dedicated admin scope. |
| 7 | Dotenv report variables | Imported from upstream jobs through supported dependencies/needs. |
| 8 | YAML variables |
rules:variables → job →
workflow:rules:variables → top-level.
|
| 9 | Deployment variables | Environment/deployment-derived values. |
| 10 | Predefined variables | Most are lowest; some documented environment/page variables cannot be overridden. |
LAB_WINNER overrides both a job-level and top-level
YAML value of the same name. A pipeline variable can then override
the project value. Diagnose by provenance, not by visually “closest”
YAML.
6. Ordinary and file type variables solve different interface problems
An ordinary variable arrives as an environment key/value pair. A
file type project/group/instance variable stores
the configured value in a temporary file and exposes the file path
through the environment variable. This is useful when a tool expects
--config FILE, a certificate path, or another
file-oriented interface.
Predefined variables and variables defined directly in
.gitlab-ci.yml are ordinary variable type. You cannot
mark a YAML variable as file type; if a YAML value must become a
file, your job writes it to a controlled file explicitly.
inspect_file_variable:
script:
- test -n "${LAB_CONFIG_FILE:-}"
- test -f "$LAB_CONFIG_FILE"
- printf 'file_path=%s\n' "$LAB_CONFIG_FILE"
- printf 'bytes=%s\n' "$(wc -c < "$LAB_CONFIG_FILE")"
- sha256sum "$LAB_CONFIG_FILE" | sed 's/ .*/ <temporary-file>/'
7. Masking, hiding, and protection are different controls
| Control | What it changes | What it does not guarantee |
|---|---|---|
| Masked |
Attempts to replace the exact variable value in job logs
with [MASKED].
|
It does not stop job code from reading, transforming, sending, or writing the value elsewhere. |
| Hidden | Prevents the stored value from being revealed again in the CI/CD settings UI; created as masked-and-hidden. | It does not stop authorized job code from receiving the value. |
| Protected | Makes the variable available only to pipelines on protected branches/tags, subject to current MR/merged-results behavior. | It does not make a protected pipeline’s code trustworthy by itself. |
GitLab explicitly warns that masking is not a guaranteed defense against malicious scripts. Review CI configuration before running it with valuable variables, especially for fork/merge-request scenarios. A transformed value may no longer match the masker’s exact pattern.
8. Expansion happens in more than one place
“The variable expands” is incomplete. GitLab can expand some values before handing a job to Runner; Runner performs expansion for supported fields; and then the shell expands environment references while executing the script. These layers use different syntax and timing.
variables:
BASE_FRAGMENT: "blue"
EXPANDED_FRAGMENT: "$BASE_FRAGMENT-canary"
LITERAL_FRAGMENT:
value: "$BASE_FRAGMENT-canary"
expand: false
show_expansion:
script:
- printf 'expanded=%s\n' "$EXPANDED_FRAGMENT"
- printf 'literal=%s\n' "$LITERAL_FRAGMENT"
For UI-defined variables, current GitLab documentation says variable-reference expansion is disabled by default. Masked/hidden values should not be combined with expansion. Treat expansion as a configuration feature for non-secret composition, not as a secret templating system.
9. Variables are data channels; repository code is the consumer
A variable can be encrypted at rest, masked in normal logs, hidden in settings, and protected to a ref—yet still be exfiltrated by malicious commands that legitimately receive it. The primary control is therefore which code is allowed to execute with which variable, on which ref, runner, and identity boundary.
-
Do not use
env,printenv, shell tracing, or full context dumps in variable troubleshooting. - Print an allowlist of non-sensitive identifiers such as pipeline source, SHA, job ID, and a synthetic demo value.
- Do not transform or encode a real secret to “test masking.” If teaching the limitation, use a fake synthetic string only.
- Do not put secrets in artifacts, caches, dotenv reports, or debug bundles merely because the source variable was masked.
10. Read-only inspection: prove context before changing values
printf 'pipeline_source=%s\n' "$CI_PIPELINE_SOURCE"
printf 'pipeline_id=%s\n' "$CI_PIPELINE_ID"
printf 'job_id=%s\n' "$CI_JOB_ID"
printf 'ref=%s\n' "$CI_COMMIT_REF_NAME"
printf 'sha=%s\n' "$CI_COMMIT_SHA"
printf 'runner_id=%s\n' "${CI_RUNNER_ID:-unknown}"
printf 'runner_desc=%s\n' "${CI_RUNNER_DESCRIPTION:-unknown}"
These values identify the execution context without enumerating arbitrary variables. Pair them with the project Settings → CI/CD → Variables metadata for the specific synthetic lab keys you are studying. Do not screenshot or export secret values.
11. Foundation mistakes to eliminate early
- “Job YAML wins because it is closer to the script.” Project/group/pipeline sources can outrank YAML entirely.
- “Masked means secret code cannot read it.” Masking is primarily a log-redaction mechanism.
- “File variable means the value is not in the job.” The job receives a path to a temporary file containing the value and can read it.
- “Protected variable exists in every pipeline.” Availability depends on protected-ref context and current MR behavior.
- “All predefined variables exist during rules evaluation.” Job-only variables do not.
- “Expansion is just shell expansion.” GitLab and Runner also perform keyword-specific expansion before the shell runs.
Knowledge check
A project variable and a job-level YAML variable have the same key. Which wins under the current standard precedence?
The project variable. Project variables outrank all YAML variables in the current precedence order.
Can a job-only predefined variable be used to decide whether that same job is added by rules?
No. Job-only variables exist only after a runner starts the job, after pipeline/job graph creation.
What does a file type variable expose through the environment variable?
A path to a temporary file whose contents are the stored variable value.
Does masking prevent malicious job code from exfiltrating a variable?
No. Masking is a log-safety aid, not a security boundary against code that receives the value.
Why record CI_PIPELINE_SOURCE, ref, and SHA with variable evidence?
Variable availability and rules can depend on pipeline/ref context, and the SHA identifies the exact code/configuration that consumed the value.
Official references and version notes
- GitLab CI/CD variables — project/group/instance variables, visibility, masking, hiding, protection, file variables, expansion, precedence, pipeline-variable restrictions, and security guidance.
- Predefined CI/CD variables reference — current pre-pipeline, pipeline, and job-only availability phases and variable definitions.
- Where variables can be used — GitLab, GitLab Runner, and execution-shell expansion mechanisms and keyword-specific behavior.
-
CI/CD YAML syntax reference
—
variables, job variables,variables:expand,rules:variables,workflow:rules:variables, and inheritance semantics. - CI/CD inputs — typed/configuration inputs, interpolation, and the current recommendation to prefer inputs over ad-hoc pipeline variables for newer parameterized pipelines where applicable.
- Protected branches — current protected-ref behavior that underpins protected-variable availability.
Variable behavior in this chapter was rechecked against current primary GitLab documentation on 2026-09-11. The current documentation orders policy variables above manual-job and pipeline variables, then project/group/instance values, dotenv values, YAML variables, deployment variables, and most predefined variables. It also documents project/group variable visibility as Masked by default since GitLab 18.3, UI variable-reference expansion disabled by default since GitLab 18.6, and pipeline inputs as the preferred parameter mechanism over pipeline variables for newer designs where applicable. These are version-sensitive details: verify them again before publishing or automating production policy.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.