In GitLab CI/CD, the same variable name is set both in the project's CI/CD settings and in .gitlab-ci.yml. Which value does the job get, and what is the overall precedence order?
answer
- settings beat the file
- outside the repository outranks inside it
- closest group wins over parents
- manual and trigger values sit on top
- predefined variables are the floor
basics
~20 sThe project setting wins. GitLab ranks variables defined outside the repository above ones defined inside it: manual, trigger and schedule values first, then project, then group, then instance, then the values in .gitlab-ci.yml, with predefined variables last.
solid answer
~40 sGitLab resolves a duplicate name by precedence, and the rule that surprises people is that UI- and API-defined variables beat the ones written in `.gitlab-ci.yml`. From highest to lowest, as of GitLab 17: variables typed into a manual pipeline run, passed with a trigger, or attached to a scheduled pipeline; then project variables; then group variables, where the group closest to the project wins over its parents; then instance variables; then values inherited from an earlier job's `dotenv` report; then a job's own `variables:` block; then the global top-level `variables:`; and finally deployment and predefined variables at the bottom. So a project-level `DEPLOY_ENV` silently overrides the one in your YAML, which is exactly the intended design — settings hold environment-specific and secret values, the file holds the defaults.
go deeper
Remember the headline: a variable set in the project's CI/CD settings beats the same name written in .gitlab-ci.yml, and predefined variables sit at the bottom.
Recite the order from manual-run values down through project, group, instance, dotenv inheritance and the two YAML levels, and explain why settings deliberately outrank the file.
Diagnose a job receiving an unexplained value by walking that list, including pipeline-schedule variables and dotenv inheritance, and design where each value should live.
Own the convention across many projects: which values belong to the instance, which to a group, which to a project, and how manual-run overrides are restricted so they cannot become an unaudited path to production.
## The one-line answer The project's CI/CD settings value wins. Anything defined **outside** the repository outranks anything defined **inside** it. ## The full order As of GitLab 17, from highest precedence to lowest: 1. Pipeline execution policy variables (from security policies, on the tiers that have them). 2. Trigger variables, scheduled pipeline variables, and variables typed into a manual pipeline run. 3. **Project** variables (Settings → CI/CD → Variables). 4. **Group** variables. If a project sits under nested groups and several define the name, the group closest to the project wins. 5. **Instance** variables (set by an administrator for the whole GitLab instance). 6. Variables inherited from an upstream job through a `dotenv` report and `dependencies`. 7. Job-level `variables:` in `.gitlab-ci.yml`. 8. Global (top-level) `variables:` in `.gitlab-ci.yml`. 9. Deployment variables contributed by integrations. 10. Predefined `CI_*` variables. Job-level YAML beats global YAML, which is the part everyone guesses correctly. The part people get wrong is that both of them sit **below** the settings screens. ## Why the design is that way round The file is versioned, readable by everyone with repository access, and identical for every environment. The settings screens are access-controlled, per project or group, and can hold secrets. If the file could override them, the value of a variable would depend on whoever last edited a branch — and anyone who can push a branch could redefine a production credential's name to something they control. Ranking settings above the file means a stored secret cannot be shadowed by YAML. The practical consequence: put safe **defaults** in the YAML and the real, environment-specific or sensitive values in settings. ```yaml variables: DEPLOY_ENV: "staging" # default; a project variable of the same name wins LOG_LEVEL: "info" deploy: variables: LOG_LEVEL: "debug" # beats the global block, still loses to a project variable script: - ./deploy.sh "$DEPLOY_ENV" ``` ## Scoping narrows the same list Precedence answers "which of two values wins". Scoping answers "is this value present at all": - **Environment scope.** A project or group variable can be limited to an environment pattern such as `production` or `review/*`. It is exported only to jobs whose `environment:` matches. Two variables with the same name and different scopes are the normal way to hold one credential per environment. - **Protection.** A protected variable is only exported to jobs running on protected branches or protected tags. On any other ref it is absent, not empty — see the separate protected/masked/file distinction. - **Inheritance.** Group and instance variables flow down to every project beneath them unless a nearer definition replaces them. ## Debugging a value you cannot explain When a job gets a value nobody admits to setting, work down the list. Check the pipeline's own variables first (a scheduled pipeline carries its own set, which is invisible in the YAML and in project settings). Then project, then each group from the project upward, then instance. `dotenv` inheritance is the sneaky one: a job that declares `artifacts:reports:dotenv` injects its variables into downstream jobs that depend on it, and those override the YAML. A quick diagnostic step is to echo the value at the very start of the failing job, since the exported environment is the resolved result. Debug tracing shows the whole environment but exposes secret values in the log, so it is gated by role and should not be left on. ## Overriding on a manual run The top of the list matters operationally: when you start a pipeline from the UI or the API you can supply variables that outrank everything except policy variables. That is how you do a one-off deploy to a different target without editing the repository — and also why the ability to run pipelines manually on a protected branch is itself a privilege worth restricting.
- A parent group and a subgroup both define REGISTRY_URL. Which one reaches the project's jobs?The subgroup's — the group closest to the project wins, and the parent's value is shadowed. This is what makes nested groups usable for defaults: the top-level group sets the organisation-wide value, and any subgroup that needs something different overrides it locally without touching the parent or the project.
- How can a variable that appears nowhere in the settings or the YAML still reach a job?Three common routes: it was typed into a manual or triggered pipeline run and so belongs to that pipeline; it was attached to the pipeline schedule; or an earlier job published it through `artifacts:reports:dotenv` and this job inherits it via `dependencies` or `needs`. Dotenv inheritance outranks the YAML, which makes it the hardest of the three to spot.
- Why put defaults in .gitlab-ci.yml at all if settings always beat them?Because the file documents the contract. A reader sees which variables the pipeline expects and what a sane value looks like, forks and merge requests run without anyone provisioning settings, and the pipeline fails loudly rather than mysteriously when a value is missing. Settings then carry only what is environment-specific or secret.
saying these in an interview costs you the question
- Assuming .gitlab-ci.yml overrides the project settings
- Thinking the most recently edited definition wins
- Believing a parent group beats a subgroup
- Confusing precedence with protection or environment scope
- Forgetting that dotenv inheritance outranks YAML variables