Variables & Secrets

Jobs receive variables from multiple sources, layered by precedence (lowest to highest):

PrioritySourceHow to set
1 (lowest)CI-derivedAutomatic (git SHA, branch, etc.)
2Global YAMLvariables: at top of .gitlab-ci.yml
3Instance variablesFetched from GitLab API (admin token; --secrets all)
4Group variablesFetched from GitLab API (--secrets all)
5Project variablesFetched from GitLab API
6--env flagsglci run --env KEY=VALUE
7--env-fileglci run --env-file .env.local
8Pipeline preset envenv of a --pipeline/context preset in .glciconfig.toml
9.glci.envAuto-loaded from project root (add to .gitignore)
10Dotenv artifactsartifacts: reports: dotenv: from dependency jobs
11 (highest)Job YAMLvariables: inside a job definition

This mirrors GitLab’s order: instance, group, and project CI/CD variables override the global variables: block, with the more specific scope (project) winning over the broader ones (group, instance).

Inspecting resolved variables#

When a value isn’t what you expect — or a rules: clause matches when it shouldn’t (or doesn’t when it should) — inspect the resolved variables instead of guessing.

glci variables — static inspection#

glci variables resolves the variables for every job without running the pipeline, attributing each value to the layer it came from and showing how each job’s rules: evaluated. Because nothing executes, jobs that are excluded by their rules: are still shown (marked excluded), which is the main way to debug why a rule did or did not match.

It focuses on the variables you actually control. Predefined CI_* variables are hidden by default (pass --all to include them), with one exception: any predefined variable referenced by a rule’s if: is always shown, so you can see the value the rule was evaluated against.

glci variables                          # all jobs, current context
glci variables deploy_prod              # only the named job(s)
glci variables --stage test             # only jobs in a stage
glci variables --context branch=main    # simulate a different context
glci variables --all                    # include predefined CI_* variables
glci variables --json                   # machine-readable output

Example:

deploy_prod  (stage: deploy)  [included, when: on_success]

  Rules:
    #0  $CI_COMMIT_BRANCH (main) == $CI_DEFAULT_BRANCH (main)  → true

  Variables:
    KEY               VALUE       SOURCE
    CI_COMMIT_BRANCH  main        predefined
    DEPLOY_ENV        production  rules
    GREETING          hello       global

The Rules section shows each rule’s condition with every $VAR expanded to its value, and whether the condition evaluated true or false (a failing changes:/exists: rule appends a short reason). The Variables SOURCE column reports where the winning value came from: predefined, global, flag (--env), env-file, glci-env, preset, job, rules, or predefined:job (per-job CI_JOB_NAME/CI_JOB_STAGE).

A job excluded by its rules still prints, so you can see why:

only_dev  (stage: test)  [excluded, when: never]

  Rules:
    #0  $CI_COMMIT_BRANCH (main) == "dev"  → false

  Variables:
    KEY               VALUE    SOURCE
    CI_COMMIT_BRANCH  main     predefined
    ...

With --json, each job carries a rule_trace (with matched booleans and the refs map of referenced variable values) and the full variables array — suitable for programmatic use.

Long values (e.g. CI_COMMIT_DESCRIPTION) and multi-line values are truncated to 512 characters and shown on a single line so the table stays readable. Pass --expand to print full, raw values. (--json always carries the full value.)

glci variables resolves everything available locally (no Docker, no network). It does not fetch GitLab API group/project secrets or runtime values (dotenv, CI_JOB_ID); for those, use --show-variables below.

glci run --show-variables#

Adding --show-variables to glci run prints each job’s fully resolved variable set — including API secrets, dotenv imports, and runtime CI_* values — together with its rule trace, to the terminal just before the job’s log:

glci run --show-variables                 # secrets masked, predefined CI_* hidden
glci run --show-variables --unmask        # reveal masked/secret values
glci run --show-variables --all-variables # include predefined CI_* variables
glci run --show-variables --expand        # do not truncate long values
  deploy ── variables ──
    Rules:
      #0  $CI_COMMIT_BRANCH (main) == $CI_DEFAULT_BRANCH (main)  → true

    Variables:
      KEY                   VALUE       SOURCE
      CI_COMMIT_BRANCH      main        predefined
      CI_REGISTRY_PASSWORD  [masked]    registry
      DEPLOY_ENV            production  rules

Like glci variables, predefined CI_* variables are hidden by default (use --all-variables), except those a rule references. Secret values (API group/project variables and any variable marked masked) are shown as [masked] by default; pass --unmask to print them in clear text.

Remote secrets#

Project, group, and instance CI/CD variables are fetched automatically when a GitLab token is available. Results are cached in daemon memory for 6 hours (never written to disk). The cache is lost on daemon restart.

glci run --secrets all      # project + group + instance variables (default)
glci run --secrets project  # project variables only
glci run --secrets none     # skip remote fetching entirely
glci run --refresh-secrets  # force a fresh fetch, bypassing the cache

Instance-level variables (GET /admin/ci/variables) require an administrator token. For non-admin tokens GitLab returns 403, and glci skips them with a warning in the daemon log — project and group variables are unaffected. This matches how real runners receive instance variables injected server-side; locally, glci can only read them when your token is allowed to. If your includes or jobs depend on an instance variable you can’t fetch (for example a non-admin token on a shared instance), supply it locally instead via .glci.env, --env, or a pipeline preset.

Configure the cache TTL in .glciconfig.toml:

[gitlab]
secrets_ttl = "6h"   # default; set to "0" to disable caching

Local overrides with .glci.env#

Auto-loaded KEY=VALUE file from project root for local secrets. Add it to .gitignore. Masked and file-type variable attributes from the GitLab API are preserved and passed to the runner.

Variables in include:#

GitLab expands $VAR / ${VAR} in include: project:, ref:, and file: before fetching the included file, so a pipeline can point at a template repo through variables:

include:
  - project: $_GITLAB_TEMPLATES_REPO
    ref: $_GITLAB_TEMPLATES_REF
    file: '/includes/main.yml'

glci resolves these the same way. The variables available during include expansion are, lowest to highest precedence: predefined CI_* variables, the global variables: block, then your locally-supplied variables (--env, --env-file, pipeline preset env, .glci.env). The local layers stand in for the instance/group/project CI/CD variables GitLab would normally supply.

On many self-managed instances (Drupal contrib’s gitlab_templates, for example) these are instance-level variables. glci fetches instance variables only with an admin token (see Remote secrets), and remote variables are fetched in parallel with parsing — so they are not available at include-expansion time. Supply any variable an include path depends on locally:

# .glci.env (gitignored)
_GITLAB_TEMPLATES_REPO=project/gitlab_templates
_GITLAB_TEMPLATES_REF=1.0.x

Inspect the resolved include paths with glci jobs or glci variables; a literal $VAR reaching the API (an HTTP 404 fetching projects/$VAR/...) means the variable wasn’t supplied at any of the layers above.

The same rule applies to child and cross-project pipelines (trigger: with include: or project:). They inherit the parent’s local layers (--env, --env-file, pipeline preset env, .glci.env), so any variable you supply locally is also available when the child’s own include: paths expand. Remote-fetched instance/group/project variables are excluded from include expansion in child pipelines too — if a child include path depends on one, supply it locally as above.

Token forwarding#

By default, the host’s real GitLab token is forwarded into jobs as CI_JOB_TOKEN. Disable with --no-token, or restrict which jobs receive it in .glciconfig.toml:

[token]
forward_host_token = true
stages = ["deploy"]              # only deploy stage gets real token
jobs = ["release"]               # specific job names
job_patterns = [".*-publish$"]   # regex patterns

Jobs not matching the filter receive a synthetic mock-server token instead.

GitLab API token#

Required for include: project:, include: component:, fetching remote CI/CD variables, and auto-cloning trigger: project: targets.

Token resolution order (first match wins; --token flag always overrides):

  1. GITLAB_TOKEN or GITLAB_PRIVATE_TOKEN environment variable
  2. [gitlab] token in .glciconfig.toml / ~/.glci/config.toml
  3. glab auth credential-helper – supports keyring, OAuth2 refresh, and PAT/CI tokens
  4. Direct read of ~/.config/glab-cli/config.yml – legacy fallback

The simplest setup is glab auth login – the token is picked up automatically. The project path is auto-detected from your git remote; override with --project.

Fallback project path#

When no git remote is configured (common for local-only test repos), CI_PROJECT_PATH defaults to local/<dirname> so variables like $CI_PROJECT_PATH are always available in job scripts.

Unsupported variables#

About 20 CI/CD variables cannot be set locally because they require a real GitLab server: CI_DEPLOY_*, CI_UPSTREAM_*, CI_EXTERNAL_PULL_REQUEST_*, KUBECONFIG, CHAT_*. GITLAB_USER_* variables are approximated from git config.

GLCI_PREFER_API#

Set GLCI_PREFER_API=1 to use the GitLab Lint API as the primary parser instead of the offline parser. Useful for edge cases where the offline parser differs from GitLab’s server-side behavior.

Context-derived variables#

glci sets CI variables based on the --context flag. See Context Simulation for the full variable table per context type.

Esc