fix: Quote app-env values in deploy-vps #56
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!56
Loading…
Reference in a new issue
No description provided.
Delete branch "feature/54-appenv-quoting"
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 #54
Problem
deploy-vps writes app-env lines verbatim into the host
.envand the remote step shell-sources it, so a value containing$, a backtick,\or"is silently altered — or, for a backtick, executed on the host. The action's ownDEPLOY_DOMAIN="…"lines have the same shape, and every consumer has to know to quote correctly (expiro works around it with single quotes).Solution
The action now single-quotes every value it writes — its own lines and each app-env value — so the sourcing shell reads back exactly the bytes that were passed. A value the consumer already wrapped in matching quotes is written as given (the consumer owns that quoting), which keeps expiro's live workaround deploying unchanged. A line whose key is not a valid variable name fails the deploy, naming only the key. The README stops teaching the
KEY="…"shape.Review notes
Start with the "Write the runtime environment file" step. The sourcing shell is the effective parser — compose also reads the file, but the sourced process environment takes precedence in compose interpolation. Verified with a local matrix that extracts the exact
run:block from the YAML and round-trips hostile bytes throughset -a; . ./file(17/17, plus a negative control showing the old shape corrupts); details in the plan comment on #54.Risks and trade-offs
Honouring pre-quoted values means a consumer who double-quotes keeps the old expansion semantics for that value — deliberate, documented, and what makes the change safe to roll out without coordinating consumers.
🤖 Generated with Claude Code
https://claude.ai/code/session_01TpKJno2d3VeZeEyV3sJbeq
Self-verification against the acceptance criteria, via the local matrix (extracts the exact
run:block from the YAML in this branch and round-trips it throughset -a; . ./file, the remote step's consumption shape):$, backticks,\,",'(also$(…),${…}, leading/trailing spaces, empty) read back byte-identical after sourcing. 10/10 comparisons pass; a negative control confirms the oldKEY="…"shape corrupts the same bytes (pa$$w0rd\echo BOOM`→paw0rdBOOM`).=or with an invalid key fails the deploy, naming only the key — verified the rejection message does not contain the value.DEPLOY_DOMAIN=ex$mple.comandIMAGE_TAG=v1.0.0-(backtick)whoami(backtick) survive byte-for-byte through the same path.CI is green on this head. Marking ready.
WIP: fix: Quote app-env values in deploy-vpsto fix: Quote app-env values in deploy-vpsApproved. The quoting is correct — I round-tripped it rather than reading it — and the shape of the change is right for a shared action. One finding about how much of
#54this actually closes today, inline.sq()does what it claims. I extracted the function and round-tripped hostile values throughprintf→ file →set -a; . file:Every one comes back byte-identical, including a realistic Postgres URL carrying both
$and'. The'\''replacement is the right idiom and the one people usually get subtly wrong. The old double-quoted shape, as a control, does not survive the same test —$Bdisappears and a backtick runs.The shell around it is sound too, and the details that would have broken it are handled: the step is
shell: bashso${v//…}andlocalare available;set -euo pipefailis on, soemit_app_env'sreturn 1genuinely fails the step rather than writing a partial.env; and[ -z "${APP_ENV}" ] && return 0is exempt fromset -ebecause it is not the final command in its&&list — which is the one place this pattern usually goes wrong.Failing on a malformed key, and naming only the key, is the right call. A line that is not
KEY=VALUEwould otherwise be sourced as whatever it happens to be, and echoing the value back would put a secret in the job log.Finding — the passthrough leaves four of six consumers exactly where they were. Inline.
CI green on
a676e1b5(PR / validate).@ -158,0 +185,4 @@echo "app-env: not a KEY=VALUE line with a valid key: '${key}'" >&2return 1ficase "${val}" inThis branch is what makes the change safe to roll out — and also what means most consumers gain nothing from it yet.
I checked how every consumer currently passes
app-envonmain:suhba/suhba-webKEY=${{ … }})sqahmad/expiroahmad/portfoliobayan/kidpointsahmad/tripsuhba/khittaThe four in the bottom rows carry exactly the values this is about —
JWT_SECRET,MINIO_SECRET_KEY,MONGO_URL,ADMIN_PASSWORD,FORGEJO_OIDC_CLIENT_SECRET,DATABASE_URL,TURNSTILE_SECRET_KEY,MAILTRAP_TOKEN— generated credentials, which is where$and backticks actually come from. And they use the double-quoted shape because it is what the README taught until this PR.So the risks section is right that honouring pre-quoted values makes the rollout safe, and it understates the consequence:
#54's hazard stays open wherever the old shape is still in use, which is most places, and nothing surfaces that. Someone reading "deploy-vps quotes app-env values itself" would reasonably conclude the fleet is covered.I would not change the passthrough — breaking those four is worse — but it is worth making the gap visible rather than waiting for a rotation to produce a mystery. One line here turns it into something each repo sees on its next deploy:
with the single-quoted case staying silent, since it is already correct. That, plus a follow-up to convert the four repos, is what actually closes
#54— this PR is the half that makes those conversions safe to do one at a time.Agreed, and making the gap concrete rather than visible-in-prose: after this merges and the tag moves, I am filing one conversion issue per remaining double-quoting consumer — ahmad/portfolio, bayan/kidpoints, ahmad/trip, suhba/khitta — to pass raw values, with ahmad/expiro#65 (drop the single-quote workaround) as the template. The workspace knowledge file already records that this PR alone is not "the fleet is covered"; the four issues are what closes that gap issue by issue.