Chapter 06Lesson 01~135 minutes

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.

VariablesPrecedenceAvailability phasesMaskingProtection

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.
Foundation rule: never diagnose a variable by asking only “what is its name?” Ask where was it defined, at what scope, of what type, with what visibility/protection/expansion settings, in which pipeline source/ref, at which availability phase, and which source wins precedence?

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

Variable dataflow — provenance and phase before 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.
State-separation test: if a job never appears in the graph, changing a job-only variable cannot fix that creation decision. Diagnose configuration, pipeline source, and rule inputs first.

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.
Counter-intuitive example: a project variable named 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>/' 
Do not print sensitive file content. A file variable changes the interface from “value in an environment variable” to “path in an environment variable”; it does not make malicious job code unable to read the 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.
Next lesson

Guided Hands-On Workflow and Core Operations

Build a disposable variable lab, prove precedence and file-variable behavior, then compare protected and unprotected ref contexts without exposing secrets.

Knowledge check

A project variable and a job-level YAML variable have the same key. Which wins under the current standard precedence?

Can a job-only predefined variable be used to decide whether that same job is added by rules?

What does a file type variable expose through the environment variable?

Does masking prevent malicious job code from exfiltrating a variable?

Why record CI_PIPELINE_SOURCE, ref, and SHA with variable evidence?

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.
Version and compatibility note

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.