fix(compose): keep env behind an embedded variable the re-parse canno… - #677
Open
Baaaki wants to merge 1 commit into
Open
fix(compose): keep env behind an embedded variable the re-parse canno…#677Baaaki wants to merge 1 commit into
Baaaki wants to merge 1 commit into
Conversation
…t resolve
A `${VAR}` expression embedded in a larger string lost its provenance.
`resolveComposeValue` carries `variable`/`required` only when the expression is
the entire value, so `postgres://u:${PW}@db` resolved to the non-empty
`postgres://u:@db` and named no variable at all.
`keepUnresolvedEnv` exists to stop a push-deploy re-parse from overwriting a
value the user configured in the wizard — the repo's `.env` holds the secret and
is not committed, so the re-parse cannot resolve it. Both of its guards
(`value === ""`, `source === "missing"`) miss the embedded shape, so a working
DATABASE_URL was replaced with the broken one on the next push that touched the
compose file.
Interpolation now reports the variables that contributed nothing, carried on the
meta as `unresolvedVariables` (names only, never values) plus `required` for the
mandatory operators. A satisfied `${VAR:-default}`, or a `${VAR:+alt}` that
correctly yielded "", is not reported — those resolved as authored.
`maskEnvironmentMeta` is an allowlist, so the new field is named there or it
never reaches the client.
Refs oblien#673
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A compose
${VAR}expression embedded in a larger string lost the record ofwhich variable it referenced, so a half-interpolated value like
postgres://u:@dbwas indistinguishable from a literal the author typed. Theparser now names those variables, and
keepUnresolvedEnvstops a push-deployre-parse from overwriting the env the user configured behind one.
Behavior
${VAR:?msg}with no value → the meta names the variable and marks itrequired;
keepUnresolvedEnvkeeps the stored value instead of overwriting it.${VAR}/$VARwith no value → named, not marked required.${VAR:-default}→ resolved; nothing reported, and a genuine upstreamedit still drifts normally.
${VAR:+alt}that yielded""→ resolved exactly as authored;nothing reported.
.envor the caller's env → nothing reported, the valueinterpolates as before.
source: "missing"+variable+required).requiredis theOR of them.
${}at all → unchanged, no meta fields added.Motivation
resolveComposeValuebuilds meta carryingvariable/requiredonly when theexpression is the entire value. An embedded one falls through to the
interpolateComposeStringbranch, which recordedsource: "interpolated"andnothing else:
The half-interpolated value is non-empty, so emptiness — the signal
everything downstream keys off — says nothing about it.
The concrete damage is in
keepUnresolvedEnv. That function exists precisely tostop a push-deploy re-parse from overwriting a value the user configured in the
wizard: the repo's
.envholds the secret and is not committed, so the re-parsecannot resolve it. Both of its guards miss the embedded shape —
— so the working
DATABASE_URLthe user had typed is replaced with the brokenone on the next push that touches the compose file. Silently, and on a push that
had nothing to do with that variable.
Related issue
Refs #673.
Deliberately a plain reference rather than an auto-closing link. This fixes the
provenance loss and the overwrite above, but
${}is still resolved once at scantime and frozen onto the service row, so the "dynamic env" the issue title asks
for is not delivered here. The issue should stay open. See Out of scope below.
Per CONTRIBUTING this is a bug fix, so it did not need prior scope agreement —
the issue is linked because it reported the symptom.
Changes
apps/api
src/lib/compose-parser.ts— interpolation optionally reports variables thatcontributed nothing (new
UnresolvedSink), threaded through nested defaults.resolveComposeValuecollects them onto the meta asunresolvedVariables(names only, never values) plus
required: truewhen any used:?/?.A satisfied
${VAR:-default}, or a${VAR:+alt}that correctly yielded"",is not reported — those resolved as authored. The bare
$VARspellingreports too.
src/modules/deployments/build.service.ts—keepUnresolvedEnvnow decidesvia
isUnresolvedParse. Its first clause is the previous condition verbatim(
value === "" && source === "missing"); the embedded arm is pure addition,so nothing that was kept before stops being kept.
src/lib/secret-env.ts—maskEnvironmentMetarebuilds the object from anallowlist, so a field not named there never reaches the client. The new one
holds variable NAMES only — the same class as the existing
variable, whichis already kept unmasked — so it is passed through while the value-bearing
fields (
resolvedValue,defaultValue,expression) stay masked.No schema, endpoint, or dependency changes.
Verification
7 new tests. 5 of them fail without the source change — verified by stashing
apps/api/src/and re-running with the tests in place:The two parser tests that pass either way are deliberate: they pin the cases
that must not start reporting (a resolved variable, a satisfied default).
Without them, widening the
:-arm later would silently breakkeepUnresolvedEnv. The last two failures above are regression guards forbehaviour that already worked — they fail here only because the function was not
exported before, and they are what proves the rewritten guard did not change it.
Before / after, same compose file, no
.envpresent:A note on
bun format: run as CONTRIBUTING prescribes, then reviewed and theunrelated parts dropped. It rewrites ~200 files in this repo — all three source
files I touch are already Prettier-drifted at HEAD — so committing its output
would have violated the "diff is scoped" rule. My added lines are Prettier-clean
(
prettier --checkon them passes); the diff contains no reformatting of linesI was not otherwise changing.
Out of scope
Two things this deliberately does not do:
missingEnvCountfilters onsource === "missing", and widening it needs adecision about the masking/reveal interaction — an unedited value arrives at
the client as the mask sentinel, so "has the user fixed this yet?" is not
answerable from the value alone. That felt like yours to make. Worth noting
the API already returns
missingRequiredEnvfor this case and nothing in thedashboard reads it.
${}is still not dynamic. It is resolved once at scan time and frozenonto the service row; only
{{publicUrl:…}}and{{env:svc:KEY}}aredeploy-time. Making it dynamic means persisting the expression and
re-interpolating against the merged env layers at deploy — a much larger
change, and the reason this PR stops short of the issue's headline ask.
Diff