fix: Refuse app-env values a compose parse cannot survive #61
No reviewers
Labels
No labels
priority/P0
priority/P1
priority/P2
priority/P3
size/L
size/M
size/S
size/XL
size/XS
state/blocked
state/done
state/in-progress
state/in-review
state/needs-refinement
state/ready
type/bug
type/chore
type/feature
type/refactor
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
amtronics/platform-actions!61
Loading…
Reference in a new issue
No description provided.
Delete branch "feature/60-refuse-quote"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Issue
Closes #60
Problem
emit_app_envescapes an embedded'as'\''— byte-exact for the sourcing shell, unreadable for compose's dotenv parser, which exits 1 and abandons the whole env file, failing the deploy at compose with an error pointing nowhere near the cause. Separately, the honour-consumer-quoting pass-through has had no user since the fleet-wide raw conversion closed, and its value-that-looks-quoted edge writes an unquoted line.Solution
Validation becomes its own pass before anything is emitted: a valid key and no
'in the value, refusal naming only the key. Every value then goes throughsq()uniformly — the pass-through branch is deleted. Input description and README updated (raw values, one forbidden byte).Review notes
The verification matrix was rebuilt for the new semantics: hostile bytes minus
'round-trip byte-for-byte; a double-quoted value now keeps its quotes as literal bytes; a value containing'(including the old single-quote workaround shape) is refused naming the key, with the value absent from both stderr and the file; and the emitted file passes a livedocker compose configparse. 12/12.Risks and trade-offs
A consumer branch still passing pre-quoted values would now get literal quote bytes (double) or a refusal (single) — the census says none exists on any main. Residual, out of scope here: the action's own inputs (grafana token et al.) go through
sq()with no'check, so the compose-side exposure persists for them; flagged on the issue for a follow-up if the review agrees.🤖 Generated with Claude Code
https://claude.ai/code/session_01TpKJno2d3VeZeEyV3sJbeq
Self-verification against the acceptance criteria:
'value fails naming the key, no partial app-env content — matrix kill cases: refusal exits nonzero, stderr names only the key, the value appears in neither stderr nor the file, and nothing from app-env is written (validation is a separate pre-emit pass). The emitted file from a passing run parses under a livedocker compose config.'(the closed census); the matrix round-trips the hostile set byte-for-byte throughset -a; . ./file.PR / validategreen ondd01751, read back from the commit status. Marking ready.WIP: fix: Refuse app-env values a compose parse cannot surviveto fix: Refuse app-env values a compose parse cannot surviveApproved. This came from my report on
suhba/suhba-docs#11, so I held it to the same standard rather than a lighter one: I extracted the step's script from the action atdd01751and ran it, including a livedocker composeparse of what it emits.Every claimed behaviour, measured
On the two refusals I checked the thing that matters most: the value appears in neither stderr nor the partial file. The validation pass runs to completion before the emit pass, so
.env.deployholds onlyDEPLOY_DOMAINandIMAGE_TAGwhen the step aborts, and the step aborts —set -euo pipefailat the top carries the function'sreturn 1out of the{ … } > .env.deploygroup. Splitting validation from emission rather than checking inline is what makes that true, and it is the right shape.The emitted file survives a real compose parse, which is the whole point of the change:
read back through
docker compose configfrom the file the step wrote.On deleting the pass-through, I widened the check rather than reusing the census. My earlier count covered a fixed list of thirteen repositories; this one walks every repository the bot can see — 21 of them, 11
deploy-vpsconsumers (Bayan/bayan-web,Bayan/kidpoints,Bayan/stem,ahmad/expiro,ahmad/larda,ahmad/namey,ahmad/portfolio,ahmad/trip,suhba/khitta,suhba/suhba-docs,suhba/suhba-web) — and finds 184 raw, 0 single-quoted, 0 double-quoted. So the branch really has no user and the behaviour change reaches nobody. One caveat the census cannot cover: that ismainonly, so an unmerged branch still carrying a pre-quoted block would meet the new behaviour on its first deploy after this lands — a refusal for single quotes, literal quote bytes for double.The residual you flagged is real, and I confirmed it rather than agreeing on principle. With
GRAFANA_PROM_TOKEN=tok'en:So the action's own inputs reproduce exactly the failure this PR fixes for
app-env, minus the named refusal: the step passes, and the deploy dies later at compose pointing at a line rather than a cause.DEPLOY_DOMAIN,IMAGE_TAGandCOMPOSE_PROFILESare the same shape. Agreed it is out of scope for #60 — the fix is to run the same check over those four before the block, and the fact that it is a silent pass makes it worth the follow-up rather than leaving it as known-and-tolerated.One note inline on a comment the change leaves behind.
@ -174,3 +172,1 @@# knows what (the error names only the key, never the value). A value# the consumer already wrapped in matching quotes is written verbatim:# the consumer owns that quoting (documented on the input).# app-env lines are KEY=VALUE. The key must be a valid name and theThis new comment is accurate, but the one immediately above it (lines 160-167, on
sq()itself) still ends:That parenthetical is the belief #60 disproved. Compose's reading is not a secondary concern that the process environment papers over — a parse failure abandons the whole file before any interpolation happens, which is precisely the bug being fixed here. As it stands the file carries two comments ten lines apart that disagree about whether the compose parse matters, and the wrong one is the one attached to
sq(), which is what a reader looking at the escaping will read first.It is a comment, so nothing misbehaves at runtime — but this is the shared action every consumer's author reads to decide what is safe to pass, and that sentence is how a future change talks itself into loosening the escaping again. Worth folding into this PR since it is the same subject: replace the parenthetical with the fact, that both parsers read this file and only single quotes satisfy both, which is now why the value is checked at all.
fix: Refuse app-env values a compose parse cannot surviveto WIP: fix: Refuse app-env values a compose parse cannot surviveNew commits pushed, approval review dismissed automatically according to repository settings
Taken — fixed in
fe6f580: the sq() comment now states both readers count and why the single-quote refusal exists, instead of the precedence claim #60 disproved. Re-marking ready.WIP: fix: Refuse app-env values a compose parse cannot surviveto fix: Refuse app-env values a compose parse cannot surviveApproved. The delta (
dd01751..fe6f580, one commit) is thesq()comment and nothing else — one hunk, no code touched, CI green onfe6f580.The replacement is accurate on every claim, and I had measured each of them while reviewing the previous head rather than reading them off the docs:
docker compose configcleanly;KEY='ab$HOME'staysab$HOME, where unquoted and double-quoted both expand);'\''is shell-only — compose rejects it and abandons the whole file, which is#60.Nothing left open from my side on this PR. The follow-up for the action's own four inputs —
DEPLOY_DOMAIN,IMAGE_TAG,COMPOSE_PROFILES,GRAFANA_PROM_TOKEN, which still reachsq()unchecked and fail silently — stands where you filed it.