GitLab CI/CD Variables Debugging Prompt
Diagnose why a GitLab CI/CD variable is missing, masked oddly, expanded wrong, or scoped to the wrong environment — protected, masked, file-type, inheritance, environment scope.
- Target user
- DevOps engineers debugging variable behavior in GitLab pipelines
- Difficulty
- Intermediate
- Tools
- Claude, ChatGPT
The prompt
You are a senior DevOps engineer who can read GitLab variable resolution like a debugger. You know the precedence order (project < group < instance ... vs. job/file overrides), the difference between protected/masked/file/expanded, and the environment-scope footgun. I will provide: - The symptom: variable missing in job, value wrong, masking broken, protected variable not appearing - Where the variable is defined: Project CI/CD → Variables, Group → Variables, Instance, `.gitlab-ci.yml`, scheduled pipeline, manual run input - The variable's scope settings: protected? masked? expand variable references? environment scope? - The job that's affected: branch, environment, pipeline source, runner tags - Output of `echo "$VAR"` from the job log (if not masked) — or the symptom (`$VAR is empty`, `unbound variable`, etc.) Your job: 1. **Walk variable precedence** (highest priority first): 1. Variables defined in `.gitlab-ci.yml`'s `variables:` block AT THE JOB LEVEL 2. Variables passed via `--variable` to `trigger:` or in `pipeline trigger token` 3. Variables defined at pipeline schedule level 4. Manual pipeline run "Run Pipeline" UI variables 5. Project-level CI/CD variables 6. Group-level CI/CD variables (closest ancestor wins) 7. Instance-level CI/CD variables 8. Variables defined at `default:` or top-level `variables:` in `.gitlab-ci.yml` 9. Predefined `$CI_*` variables (always set) 2. **Protected variables**: - "Protected" means: only available on protected branches/tags - If branch isn't protected, the variable is invisible to the job — silently - Common bug: variable shows in admin UI but not in the job because the branch isn't protected 3. **Masked variables**: - Replace value with `[MASKED]` in job logs - Have constraints: 8+ chars, no newlines, base64-charset by default (relaxed in newer GitLab) - Masking the WRONG variable (e.g., a hash users compare against) makes debugging impossible 4. **File variables (`File` type)**: - GitLab writes the value to a temp file; the variable contains the PATH to that file - `$KUBECONFIG` defined as File works because tools read the file by env var - `$API_KEY` defined as File breaks if the consumer expects the value itself - Use File for cert/key contents; use regular Variable for tokens/passwords 5. **Variable expansion**: - GitLab can expand `$VAR` references in variable VALUES if "Expand variable reference" is checked - If a variable contains `$OTHER`, that gets expanded at runtime — sometimes desired, sometimes a footgun - Some tools double-expand (`$$VAR`) — be precise 6. **Environment scope**: - A variable scoped to environment `production/*` only appears for jobs with matching `environment:` keyword - Easy to misconfigure: variable expected on a job without `environment:` → empty 7. **Job-level `variables:`** OVERRIDE project/group settings for that job — common cause of "I set it in the UI but the job has the old value." 8. **`dotenv` artifact propagation**: - `artifacts:reports:dotenv` lets a job export variables to downstream jobs - Common pattern: `build` job sets `VERSION=1.2.3` in a `.env` file → exported via dotenv → consumed by `deploy` 9. **`include:` and variables**: included files' `variables:` blocks merge into the pipeline-level variables; later includes override earlier 10. **Verify with `printenv` not `echo`**: `echo "$VAR"` works; `echo $VAR` (no quotes) eats whitespace and is misleading 11. **For masked variables that "stopped working"**: the value may have been changed to one that doesn't meet the mask charset/length requirement → masking silently disabled For each finding, give the EXACT location to fix (Project / Group / `.gitlab-ci.yml` / Instance) and the override behavior. --- Symptom: [DESCRIBE — missing, wrong value, masking broken, etc.] Pipeline context: [branch / env / source / runner] Variable name and where it's defined: ``` [DESCRIBE — name, location, scope settings] ``` Job's `.gitlab-ci.yml` excerpt (especially `variables:`, `environment:`, `rules:`): ```yaml [PASTE] ``` Job log output (sanitized): ``` [PASTE] ```
Why this prompt works
Variable resolution in GitLab has many layers (instance/group/project/file/job), several attributes (protected/masked/file/expanded), and per-job overrides. “Variable missing” is a category, not a diagnosis. This prompt forces the model to walk the precedence chain and identify the actual loser.
How to use it
- Confirm where the variable is defined. Instance, group (which one — direct parent or higher?), project, scheduled, manual run, or in YAML?
- Confirm the branch / environment. Protected variables on non-protected branches and scoped variables on non-matching environments are the top two causes.
- Check for a job-level override. A
variables:block in the job will replace the UI value for that job. - Capture the job log output (sanitized) so you can see what the runner sees.
Useful commands
# Inside a job (debugging)
- env | grep MY_VAR # see ALL env vars (careful — masks apply but tools may print plaintext earlier)
- echo "MY_VAR is set to: [$MY_VAR]" # quotes prevent word-splitting weirdness
- printenv MY_VAR || echo "NOT SET"
- echo "Length: ${#MY_VAR}" # zero means empty
# Check via GitLab API: pipeline variables
curl --header "PRIVATE-TOKEN: <token>" \
"https://gitlab.example.com/api/v4/projects/<id>/pipelines/<pid>/variables" | jq
# Project-level variables (admin or maintainer)
curl --header "PRIVATE-TOKEN: <token>" \
"https://gitlab.example.com/api/v4/projects/<id>/variables" | jq '.[] | {key,variable_type,protected,masked,environment_scope}'
# Group-level variables
curl --header "PRIVATE-TOKEN: <token>" \
"https://gitlab.example.com/api/v4/groups/<id>/variables" | jq
# Instance-level (admin)
curl --header "PRIVATE-TOKEN: <token>" \
"https://gitlab.example.com/api/v4/admin/ci/variables" | jq
Common findings this catches
- Variable defined as Protected, branch isn’t protected → invisible to job. Fix: mark branch protected OR un-protect the variable (security trade-off).
- Variable scoped to
production, job hasenvironment: prod→ mismatch. The scope is a glob;prod≠production/*. - Variable masked, value contains a
/or space → silently un-masked. Re-create with mask-compliant value. - Variable changed via UI but old pipeline ran with old value → variables resolve at pipeline creation time; restart pipeline.
variables:block in YAML withMY_VAR: $OLD_VAR+ Expand off → literal$OLD_VARstring in jobs. Turn on expansion or rewrite.include:brings invariables:that override your local → check merge order; place yourvariables:block AFTER includes.- Downstream pipeline missing variables →
trigger:doesn’t pass variables by default. Addtrigger:project:+variables:block under the trigger. dotenvartifact from build job not in deploy job → checkneeds:includes the build job; dotenv requires explicitneeds:to propagate.
Variable type cheatsheet
| Type | Stored as | Use for |
|---|---|---|
| Variable (String) | Plain value | API tokens, hostnames, simple strings |
| File | Value written to a temp file; var = path | TLS certs, kubeconfigs, SSH keys |
| Variable expanded | Value expands $REFS at use time | Composed values; rarely needed |
Best practices
- UI-managed variables for ALL secrets; never in YAML
- Mask all secrets that fit the charset constraints
- Use File-type for multi-line content (certs, kubeconfigs, SSH keys)
- Document the variable contract for each repo (especially templates)
- Audit instance/group variables for stale entries periodically
- Don’t use
$CI_JOB_TOKENfor everything — it can do more than you expect
When to escalate
- Suspected variable leak in logs (mask failed) — rotate the credential immediately; review log retention.
- Variables managed in two places conflicting (e.g., group + project both set the same name) — consolidate at the right level; document the precedence.
- Need for dynamic per-environment values that can’t be done via environment scope — consider external secrets (Vault) instead of GitLab variables.
Related prompts
-
CI/CD Secret Exposure Review Prompt
Audit GitHub Actions, GitLab CI, CircleCI, or Jenkins pipelines for secret leaks — logged secrets, exfiltration via unscoped tokens, third-party action risks.
-
GitLab CI/CD `rules:` Debugging Prompt
Diagnose why a GitLab job did or didn't run — decode `rules:` evaluation, `only/except` legacy syntax, workflow rules, and complex `$CI_*` variable conditions.
-
GitLab CI/CD Debugging Prompt
Diagnose failing GitLab CI/CD pipelines from job logs, .gitlab-ci.yml, and runner configuration.