cast can write an env var that silently suppresses Coolify's own injection of it (SOURCE_COMMIT, COOLIFY_*) — and it fails green #50
Labels
No labels
blocked
blocker:ci-red
blocker:conflict
blocker:drill-pending
blocker:unrequested
bug
claimed
documentation
enhancement
epic
merge-next
needs-triage
ready
release
scope:apply
scope:capture
scope:coolify-api
scope:fleet
scope:manifest
scope:secrets
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/cast#50
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
The trap
Coolify injects a set of values into every application's runtime environment itself —
SOURCE_COMMIT, and theCOOLIFY_*family. ReadingApplicationDeploymentJob(v4.1.2) turned up the guard that matters:Coolify skips its own injection of a name if the application already carries an env var of that name.
So an application env var named
SOURCE_COMMIT— even, especially, an empty one — does not merely fail to help. It silently suppresses the value Coolify would otherwise have provided, and it does so on a deploy that is green, whose health checks pass, and whose only symptom is that the one endpoint that reports which commit is running (/version, readingprocess.env.SOURCE_COMMITat request time) reportsunknown. That is the endpoint the provisioning runbook gates a production cutover on. It fails green. (Written up as D-266 in heavy-duty/incubator#10.)Why this is cast's problem, and not just a compose-file footgun
Anything that writes env vars can set this trap, and cast is a thing that writes env vars. Three live paths, none of them guarded —
grep -rn "SOURCE_COMMIT\|COOLIFY_" src/returns nothing:draftwill copy it off a live box.isProviderGenerated(draft.ts:113) is the only filter on what a live env var becomes in a drafted manifest, and it recognizes exactly two families:SERVICE_(FQDN|URL|USER|PASSWORD|…)and datastore-connection names.SOURCE_COMMITsplits to[SOURCE, COMMIT]— no datastore word, no connection word, noSERVICE_prefix → captured verbatim, with its live value. The box we drafted from carries exactly such an orphanSOURCE_COMMIT, empty. So drafting a working box reproduces the trap in the new box's manifest, and the nextapplywrites it. Same for anyCOOLIFY_*var: theCOOLIFY_MAGICregex atdraft.ts:80matchesSERVICE_only.capturewill store it if a manifest's env template refers to it — no name is off-limits.applywill write it.syncEnv(cli.ts:2129) bulk-UPSERTs whatever the resolved template holds.And
difffiles it under the wrong heading: aSOURCE_COMMITsitting on a live app that the manifest does not declare is reported as aremove-candidateorphan var — the category whose documented meaning is "apply never removes these; read them by eye." It gets read as cosmetic residue. It is not residue. It is an active suppression of a platform-provided value, and it is a bug, never cosmetic drift.Scope, and how this differs from #46
#46 asks whether "Include Source Commit in Build" can be set through the API. That is the build-time half, and it is now settled: the toggle gates only the build arg (
if (! $forBuildTime || $settings->include_source_commit_in_build)), the field isApplicationSetting.include_source_commit_in_build, its only writer in v4.1.2 is the Livewire Advanced tab, it appears in zero API controllers, andPATCH /applications/{uuid}enforces a 68-field allowlist that rejects unknown fields outright. So: not settable via the API, and — for a service that reads the SHA at request time — not needed, because the runtime injection is unconditional.This issue is the runtime half, which #46 does not cover and which is the one that actually broke a box: not "can cast turn the toggle on" but "cast can write an env var that turns Coolify's own injection off, and nothing in cast knows those names are special."
Suggested
A single reserved-name rule, applied at every place cast touches an env var. Name shape:
SOURCE_COMMIT, or anything matching^COOLIFY_.apply/resolve— refuse. A resolved env template that declares a reserved name fails the run before any write, naming it:Presence, not value — exactly the rule
forbidden_var_patternsalready uses (envtemplate.ts:77: "a forbidden var set tofalsestill refuses the apply"). This is the same mechanism, but it is not a per-environment state setting: it is a property of Coolify, true on every box, and so it belongs in cast's own code rather than in something a manifest change could lower.draft/capture— never copy one. Add the reserved names to the disposition machinery as their own provenance (suppressed, say): excluded from the template, and listed in the uncaptured report with the reason — the report that exists precisely so that what cast declines to carry is stated out loud rather than dropped.diff— promote it out of the orphan list. A reserved name on a live resource is not aremove-candidate; it is a finding. It should print as a warning with the consequence attached, so the operator deletes it in the UI rather than reading past it. (apply never deletesstill holds — cast reports it, the human removes it.)smokewrites an env var too; assert it can never pick one of these names.The value of doing all four together is that the rule is then true of cast, not of one code path: cast will not write a name whose meaning belongs to the platform, and it will tell you when it finds one.