Expand service variables against all job variables

What does this MR do?

Variables declared under services:variables could not reference job variables that are not exposed to the service container — most notably ones from dotenv artifacts, which GitLab marks as non-public. The reference silently expanded to an empty string.

The cause: the expansion source was filtered down to public and internal variables before being used, so non-public variables weren't there to resolve against.

The fix separates what the service container is allowed to see from what the expansion is allowed to read. A new Variables.ExpandWith(source) makes that possible, and the logic now lives in one place — Build.GetServiceVariables — called by both the docker and kubernetes executors.

A service container's environment comes from two groups, and they are treated differently:

The service's own variables, declared under services:variables, are expanded against all of the job's variables, including non-public ones. Naming a variable there is an explicit, per-variable opt-in by whoever wrote the service definition. This is what fixes the bug.

The job's public and internal variables are copied across exactly as the build container has them, with no further expansion. Re-expanding them would resolve references to non-public variables that nobody opted into — a public CONN: "$DB_URL" pointing at a non-public DB_URL holding a masked password, say. This also corrects a pre-existing bug on main, which re-expanded these against the filtered set and so silently blanked such references.

So given:

variables:
  CONN: "$DB_URL"                  # public; DB_URL is a non-public project variable

job:
  services:
    - name: postgres
      variables:
        SERV: "$MY_DOTENV_VAR"     # from a dotenv artifact, non-public

the service container gets SERV resolved to the dotenv value, CONN with the same value the build container sees, and neither DB_URL nor MY_DOTENV_VAR present at all.

Expand() is now a thin wrapper around ExpandWith(b); its behaviour is unchanged.

Why was this MR needed?

Customer-reported bug. The service variable expansion added in !3158 (closed) works for variables GitLab marks public, but silently produces an empty value for dotenv-sourced ones. The failure is silent, and the asymmetry with image-name expansion makes it especially confusing to diagnose from a job log.

What's the best way to test this MR?

  1. Create a pipeline from the following.gitlab-ci.yml definition

    stages:
      - prepare
      - check
    
    variables:
      MY_SERVICE_GLOBALVAR: "from-global"
      CI_DEBUG_SERVICES: "true"
    
    prepare:
      stage: prepare
      script:
        - echo 'MY_SERVICE_IMAGE=busybox:latest' >> service.env
        - echo 'MY_SERVICE_DOTENVVAR=from-dotenv' >> service.env
      artifacts:
        reports:
          dotenv: service.env
    
    build:
      stage: check
      needs:
        - job: prepare
          artifacts: true
      services:
        - name: $MY_SERVICE_IMAGE
          entrypoint: [ "sh", "-c", "echo $SERV_GLOBAL $SERV_DOTENV" ]
          variables:
            SERV_GLOBAL: "$MY_SERVICE_GLOBALVAR"
            SERV_DOTENV: "$MY_SERVICE_DOTENVVAR"
      script:
        - echo "hello world!"
  2. Verify the logs from the build job to ensure it contains the following lines

    Service container logs:
    2026-08-04T11:49:18.063072213Z from-global from-dotenv

Known limitations / notes for reviewers

What are the relevant issue numbers?

Closes gitlab#562249 (closed)

Related to Add variable support for services (!3158 - closed) , which added service variable expansion.

Follow-up: Service variables cannot reference other servic... (#39658)

Edited by Vishal Tak

Merge request reports

Loading