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

Any variable whose name matches QAConfig.Validation.SENSITIVE_PATTERN ((?i).(PASSWORD|SECRET|TOKEN|_KEY|API_KEY|CREDENTIAL).) is masked by utils.maskSensitiveValues in:

  • FINAL_PREVIEW rendering.

  • The summary box printed at the end of Create / Update.

  • Failure-hint output from utils.failureHints.

Mask form is *. Use utils.isSensitiveVariable(name) if you need to gate logging in new helpers.

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.