skip to content

In GitLab CI/CD, what are the predefined CI_* variables, and how does CI_COMMIT_REF_SLUG differ from CI_COMMIT_REF_NAME?

level: juniorimportance: should knowfreq 62%

answer

  1. GitLab supplies them; you never declare them
  2. CI_ prefix: commit, pipeline, project, job
  3. one is raw, one is URL-safe
  4. lowercased, non-alphanumerics to hyphens, 63 bytes

basics

~20 s

GitLab injects predefined CI_* variables into every job, describing the commit, pipeline, project, job and runner. CI_COMMIT_REF_NAME is the raw branch or tag name; CI_COMMIT_REF_SLUG lowercases it, replaces every non-alphanumeric character with a hyphen and truncates it.

solid answer

~40 s

Every GitLab CI/CD job starts with a set of variables the platform provides without you declaring them anywhere — `CI_COMMIT_SHA`, `CI_COMMIT_REF_NAME`, `CI_PIPELINE_ID`, `CI_PIPELINE_SOURCE`, `CI_PROJECT_DIR`, `CI_JOB_TOKEN`, `CI_REGISTRY_IMAGE`, `CI_DEFAULT_BRANCH` and many more. `CI_COMMIT_REF_NAME` is the branch or tag exactly as Git has it, so it can contain slashes, uppercase letters and underscores. `CI_COMMIT_REF_SLUG` is the same value lowercased, with everything outside `a-z0-9` replaced by `-`, trimmed of leading and trailing hyphens and shortened to 63 bytes — which makes it safe as a DNS label, hostname, Kubernetes object name or image tag. So a branch called `feature/ABC-1_Fix` gives you `feature/ABC-1_Fix` in one and `feature-abc-1-fix` in the other. Also remember that some predefined variables only exist in certain pipelines: `CI_COMMIT_TAG` only in tag pipelines, `CI_MERGE_REQUEST_IID` only in merge request pipelines.

code

yaml · 10 lines
yaml
build-image:
  stage: build
  script:
    - echo "ref name  = $CI_COMMIT_REF_NAME"
    - echo "ref slug  = $CI_COMMIT_REF_SLUG"
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
    - docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" .
    - docker tag "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG"
    - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
    - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG"

go deeper

for a junior

Be able to name half a dozen predefined variables from memory and say plainly that CI_COMMIT_REF_SLUG is the URL- and DNS-safe form of the branch name.

for a middle

Explain the exact slug transformation and why it is needed for image tags and hostnames, and name variables that are absent in tag or merge request pipelines.

for a senior

Show that you tag artifacts by commit SHA for identity and use the slug only as a moving pointer, and diagnose conditions that silently never match because a variable is undefined.

for a principal

Own the convention: which predefined values become the identity of a build across all repositories, so that any running deployment can be traced back to one commit and one pipeline.

## What "predefined" means GitLab CI/CD variables come from several places: the instance, group and project settings screens, the `variables:` keyword in `.gitlab-ci.yml`, values typed into a manual pipeline run — and from GitLab itself. The last group is the **predefined** set. You never declare them; the GitLab server computes them for each job and the runner exports them into the job's shell environment. Nearly all of them start with `CI_`, plus a handful of others such as `GITLAB_CI="true"` and `GITLAB_USER_LOGIN`. They are the reason a `.gitlab-ci.yml` can be written once and behave differently per branch, per tag and per merge request without any templating. ## The groups worth knowing - **Commit and ref:** `CI_COMMIT_SHA` (full 40-character SHA), `CI_COMMIT_SHORT_SHA`, `CI_COMMIT_REF_NAME`, `CI_COMMIT_REF_SLUG`, `CI_COMMIT_BRANCH`, `CI_COMMIT_TAG`, `CI_COMMIT_MESSAGE`, `CI_DEFAULT_BRANCH`. - **Pipeline:** `CI_PIPELINE_ID` (unique across the instance), `CI_PIPELINE_IID` (per-project counter), `CI_PIPELINE_SOURCE` — how the pipeline started, with values such as `push`, `web`, `schedule`, `api`, `trigger`, `merge_request_event` and `parent_pipeline`. - **Project and job:** `CI_PROJECT_ID`, `CI_PROJECT_PATH`, `CI_PROJECT_DIR` (where the repo is checked out on the runner), `CI_JOB_ID`, `CI_JOB_NAME`, `CI_JOB_STAGE`, `CI_JOB_TOKEN`. - **Registry:** `CI_REGISTRY`, `CI_REGISTRY_IMAGE`, `CI_REGISTRY_USER`, `CI_REGISTRY_PASSWORD` — enough to log in and push without storing any credential yourself. - **Environment:** `CI_ENVIRONMENT_NAME`, `CI_ENVIRONMENT_SLUG`, `CI_ENVIRONMENT_URL`, present only when the job declares an environment. ## REF_NAME versus REF_SLUG in detail Git ref names are permissive. `feature/ABC-1_Fix` is a perfectly good branch, and `CI_COMMIT_REF_NAME` hands it to you verbatim. That string is useless in most of the places a pipeline wants to put it: DNS labels cannot contain `/` or `_` or uppercase letters; container image tags reject `/` in the tag position; Kubernetes object names must match a lowercase alphanumeric pattern. `CI_COMMIT_REF_SLUG` is GitLab's answer. It is documented as `CI_COMMIT_REF_NAME` lowercased, shortened to 63 bytes, with everything except `0-9` and `a-z` replaced by `-`, and with no leading or trailing `-`. That is exactly the shape of a DNS label, which is why it is the standard building block for per-branch URLs and per-branch image tags: ```yaml build: script: - docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG" . ``` The truncation and the character folding mean the slug is **not** reversible and **not** guaranteed unique: `feature/a-b` and `feature/a_b` both slug to `feature-a-b`. Where uniqueness matters, pair it with `CI_COMMIT_SHORT_SHA` or `CI_PIPELINE_IID`. ## Availability is conditional A predefined variable that does not apply is simply absent, not empty-but-defined in every case, and this catches people out: - `CI_COMMIT_TAG` exists only in a pipeline for a tag. - `CI_COMMIT_BRANCH` exists in branch pipelines but not in tag pipelines and not in merge request pipelines — a condition written against it silently never matches on merge requests. - `CI_MERGE_REQUEST_IID`, `CI_MERGE_REQUEST_SOURCE_BRANCH_NAME` and friends exist only in merge request pipelines. - `CI_ENVIRONMENT_*` exist only when the job declares an environment. `CI_COMMIT_REF_NAME` and `CI_COMMIT_REF_SLUG` are the safe choices precisely because they are defined for branches and tags alike. ## Precedence and overriding Predefined variables sit at the bottom of GitLab's variable precedence list, so a variable you define with the same name shadows them. Don't: the rest of the pipeline, and GitLab features that read those values, assume the platform's meaning. Treat them as read-only inputs and derive new names from them instead. ## The job token `CI_JOB_TOKEN` deserves a special mention because it is predefined **and** sensitive. It is minted per job, valid only while that job runs, and authenticates a limited slice of the GitLab API, the container registry and the package registry as that job. It is the reason many pipelines need no stored credential at all to talk back to GitLab.

  • Why would you tag an image with CI_COMMIT_SHORT_SHA rather than CI_COMMIT_REF_SLUG?
    The slug is stable per branch, so every pipeline on that branch overwrites the same tag — you cannot tell which build a running container came from, and you cannot roll back to a previous one. A short SHA is unique per commit, giving each build its own immutable tag. Many teams push both: the SHA for identity, the slug as a moving convenience pointer.
  • A rules condition on $CI_COMMIT_BRANCH never matches in a merge request pipeline. Why?
    `CI_COMMIT_BRANCH` is not defined in merge request pipelines at all, so the condition evaluates against an unset variable and is false. Use `CI_MERGE_REQUEST_SOURCE_BRANCH_NAME` for the source branch of a merge request, or `CI_COMMIT_REF_NAME`, which is defined for branch, tag and merge request pipelines alike.
  • What is CI_PIPELINE_SOURCE useful for?
    It reports how the pipeline was started — `push`, `web`, `schedule`, `api`, `trigger`, `merge_request_event`, `parent_pipeline` and others. Pipelines commonly branch on it: run the nightly security scan only when the source is `schedule`, skip the expensive end-to-end suite unless the source is a merge request, or refuse to deploy from an ad-hoc `api` trigger.

saying these in an interview costs you the question

  • Thinking CI_COMMIT_REF_SLUG is just the branch name lowercased
  • Assuming every predefined variable exists in every pipeline
  • Believing slugs are unique per branch
  • Overriding predefined variables with your own values
  • Treating CI_JOB_TOKEN as a long-lived personal token

context