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-publicthe 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?
-
Create a pipeline from the following
.gitlab-ci.ymldefinitionstages: - 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!" -
Verify the logs from the
buildjob to ensure it contains the following linesService container logs: 2026-08-04T11:49:18.063072213Z from-global from-dotenv
Known limitations / notes for reviewers
- Chained service->service references are still one-level -
SERV_B: "$SERV_A"whereSERV_A: "$DOTENV"yields the literal$DOTENV. This is pre-existing and unchanged by this MR. Tracked separately in Service variables cannot reference other servic... (#39658) .
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)