Variable Substitution

How ${VAR} and ${VAR:-default} placeholders in Compose bundles are resolved when a per-env docker-compose.yml is rendered.

Placeholder syntax

Supported inside Compose blocks and .env files:

Syntax Meaning

${VAR}

Value of VAR from the binding. Empty String if unset (Compose itself warns on unset vars, but the library substitutes them as empty to match Compose’s own behaviour).

${VAR:-default}

Value of VAR if set; otherwise the literal after :-. Both VAR:- and VAR- (no :) are accepted by Compose; this library treats them equivalently.

${VAR:?err} and ${VAR:+value} are not used inside this library. If you need them, keep them confined to a single Compose file and resolve before serviceComposer sees them.

Resolution order

utils.substituteVariables(text, binding) walks the binding in this order — the first match wins:

  1. Service-specific env: resources/services/<service>/.env or .env.<db> (whichever matches the DB of the env being rendered).

  2. Global env: resources/services/.env.<db>.

  3. Template default: the :-default fallback inside the Compose block.

Example: rendering ${MYSQL_PORT:-3306} for an env with DATABASE_TYPE=mysql:

Priority Source Outcome

1

resources/services/mysql/.env.mysql sets MYSQL_PORT=3307

3307

2

resources/services/.env.mysql sets MYSQL_PORT=3308 (and service file is silent)

3308

3

neither file sets it

3306 (template default from ${MYSQL_PORT:-3306})

Overrides collected from the Jenkins GLOBAL_VARIABLES / SERVICE_VARIABLES parameters are applied as-if they lived in the corresponding .env file — the same precedence chain applies.

Recursion and nesting

substituteVariables is recursive with a bounded depth. You can reference a variable that itself contains another placeholder:

FTACS_DB_HOST=${DB_HOST:-mysql}
JDBC_URL=jdbc:mysql://${FTACS_DB_HOST}:3306/ftacs

Unbounded recursion (a variable that references itself) triggers a hard fail rather than an infinite loop.

Sensitive values

Two distinct mechanisms exist, and they do not match the same set of names. Know which one you are relying on.

utils.isSensitiveVariable(name) tests a variable name against QAConfig.Validation.SENSITIVE_PATTERN ((?i).(PASSWORD|SECRET|TOKEN|_KEY|API_KEY|CREDENTIAL).). The pattern is unanchored, so it also catches names that merely contain a token — FT_UI_BACKEND_KEYWORD matches on _KEY. Use it to gate logging in new helpers.

utils.maskSensitiveValues(text) redacts values inside a blob of text using its own inline regex — not SENSITIVE_PATTERN. It anchors on the separator, so a token must be immediately followed by = or :; that is why it carries KEYWORD as an explicit token and why SSL_KEY_STORE=<path> is deliberately left alone. It handles both the .env form KEY=value and the JSON form "KEY":"value" used by the SERVICE_VARIABLES payload, and its tail stops at a line break so a multi-line .env blob is masked key by key. The replacement always renders as KEY=[REDACTED], so a redacted JSON entry appears as "KEY=[REDACTED]" — the value is gone, only the JSON punctuation looks odd.

Mask form is [REDACTED]. The only caller today is applyServiceVarsOverlay, which logs the head of the SERVICE_VARIABLES payload and the post-write verify diagnostics.

Masking covers the Jenkins console and .qa-build.log only. SERVICE_VARIABLES is a build parameter, so its submitted value is also visible in plain text on the build’s Parameters page — see Known Issues.

Rewriting .env files losslessly

utils.updateEnvVariables(envContent, varsToUpdate):

  • Preserves comments and blank lines.

  • Preserves original ordering of keys.

  • Updates KEY=val in place for keys present in varsToUpdate.

  • Pre-processes lines glued by the Active Choices DRP by calling utils.splitConcatenatedAssignments first — see trailing-comma trap.

utils.parseEnvFileComments is the underlying helper; it is @NonCPS and returns a structure you can toBasic-ify before persisting.

Extracting keys

utils.extractEnvKeys(content) returns an ordered Set<String> of keys. Used by applyServiceVarsOverlay to know which keys in the user-submitted overlay already exist in the target file (those are updated) versus which are new (those are appended).

Rendering flow for a single env

  1. Pick DATABASE_TYPE → decide which bundle (compose-mysql.yml / compose-oracle.yml) and which .env.<db> to read.

  2. Pick SELECTED_SERVICES → filter the bundle to just those services. Strip profiles: from each kept service (commit cc8566b).

  3. Build the binding:

    1. Parse resources/services/.env.<db> → global map.

    2. For each selected service, parse <service>/.env.<db> or .env → overlay onto the global map (service-specific wins).

    3. Overlay any GLOBAL_VARIABLES / SERVICE_VARIABLES submitted by the operator.

  4. utils.substituteVariables walks the Compose text and resolves every placeholder.

  5. Write the rendered file to MASTER_ENVS_DIR/<env>/docker-compose.yml.

See vars/serviceComposer.groovy for the actual implementation.