Can "Include Source Commit in Build" be set via the API? If it can, apply should set it — if it can't, apply should say so #46
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#46
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 gap
A
dockercomposeapp built by Coolify does not receiveSOURCE_COMMITas a build arg unless "Include Source Commit in Build" is enabled on the application (Coolify withholds it by default to preserve build cache). Our compose file declares the arg value-lessly:With the toggle off, every service's
/versionendpoint returns{"sha":"unknown"}— observed live. That endpoint is the only thing that proves which commit is actually running on a box, and it is the gate the provisioning runbook uses at pre-flip verification.Today it is recorded as a manual UI act, on the strength of one observation: the toggle appeared to have no API coverage. That belief has never been tested, and it is load-bearing — it is a step a human must remember on every application, on every future box, forever, and forgetting it fails silently (the deploy is green; only
/versionis wrong).Why the "no API coverage" claim is weak
The vendored
reference/coolify-openapi-4.1.2.jsoncontains no field for it —git_commit_sha(pin a deploy to a commit) andconnect_to_docker_networkare the closest, and neither is this.But that spec is provably incomplete. It does not document
fqdnonGET /applicationseither, and the API returns it. So "not in the spec" is not evidence that the API cannot do it — it is evidence that the spec does not mention it.The probe — settle it against the live API
Against a Coolify holding a real
dockercomposeapplication:And then assert the effect, not the read-back: redeploy the app and hit
/version. A real SHA means it worked.{"sha":"unknown"}means it did not, whatever the API said.Paste the output of steps 1 and 2 into this issue — that alone decides which of the two branches below we are in.
What to do in each branch
If the API can set it →
applyshould set it on create for everydockercomposeapplication, exactly as it already does forconnect_to_docker_network(also create-only, also a thing Coolify defaults to off, also a thing that silently breaks a compose stack when missing). It is desired state, it is machine-settable, and it should not be a step a human remembers.If the API genuinely cannot set it →
applyshould say so out loud on create, exactly as it already does for umami's service domains:The equivalent line for a compose app would be:
That warning is the whole point of the issue either way. A manual step that a tool knows about and does not mention is a manual step that gets forgotten — and this one fails green.
Answered: the API genuinely cannot set it. This is branch two —
applymust warn.Probed against a live 4.1.2 with a real
dockercomposeapplication.1. Nothing on the read side. Every commit/source/build-ish key on the application object:
git_commit_sha: "HEAD"pins which commit to deploy — it is not the build-arg toggle. And.settingsdoes not exist on the response at all.2. PATCH enforces an allowlist, and rejects unknown fields outright — it does not accept-and-drop:
That is worth recording on its own: an unknown field to
PATCH /applications/{uuid}fails loudly. The accept-and-silently-drop failure mode we were worried about does not exist on this route.3. The allowlist has 68 fields and none of them is this. From
reference/coolify-openapi-4.1.2.json, the complete set ofis_*toggles PATCH accepts:There is no
is_include_source_commit_in_buildor anything like it, and since PATCH rejects what is not on the list, no candidate name can succeed.Conclusion: "Include Source Commit in Build" is a UI act on Coolify 4.1.2, confirmed — not by one observation this time, but by the read side, the allowlist, and the validator's own refusal.
So: implement the warning
applyshould print this whenever it creates or updates adockercomposeapplication, in the same voice it already uses for umami's service domains:This is the only defence available. The step is invisible, unautomatable, and fails green: the deploy succeeds, health checks pass, and the only symptom is that the one endpoint that tells you what commit is running lies to you — which you discover either at a pre-flip verification gate or in the middle of an incident.
Two incidental findings from the same allowlist, worth noting elsewhere
force_domain_overrideis a PATCH-accepted field. Relevant to #44: cast must never send it. It is reachable, which means it is a thing someone could reach for while debugging a domain conflict, and two resources sharing a domain is a routing coin-flip.destination_uuidis in the PATCH allowlist — but that does not mean placement is mutable. Coolify takes the UUID on write and never moves the container between networks; #43's note that placement is create-time still holds. Accepting a field is not the same as acting on it, which is the same trap as this issue, pointed the other way.Correction: settled from Coolify's source, not from the spec
My previous comment reached the right conclusion by a route that could not have distinguished "the field does not exist" from "the field is named something I failed to guess". The operator pushed back — the toggle lives under Advanced → Build, and several Advanced-tab toggles are PATCH-accepted (
is_auto_deploy_enabled,is_force_https_enabled,is_preserve_repository_enabled,is_container_label_escape_enabled) — so "it's an advanced option" is not a reason it would be unreachable. That objection was correct and the spec could not answer it. Coolify is open source; the source can.The field is real, and it is not named what anyone would guess
coollabsio/coolify@ v4.1.2,app/Models/ApplicationSetting.php:No
is_prefix — unlike every sibling toggle. That alone defeats name-guessing against the PATCH allowlist.Every place it appears in v4.1.2
app/Models/ApplicationSetting.phpapp/Models/Application.phpapp/Livewire/Project/Application/Advanced.phpapp/Jobs/ApplicationDeploymentJob.phpSOURCE_COMMITinto the buildapp/Services/DeploymentConfiguration/…, migrations, schema, testsapp/Http/Controllers/Api/*The real finding
ApplicationsControllerwrites eight settings from that same model, by hand, one line each:include_source_commit_in_buildis simply not on that list. This is not "the API does not expose application settings" — it is a hand-maintained allowlist with one member missing. An oversight, not a design position.What this changes
Short term — nothing. It remains a UI act on 4.1.2, confirmed properly this time, and the warning this issue asks for is still the right fix on our side.
applyshould say the step is owed whenever it creates adockercomposeapp, because the step fails green.Long term — the durable fix is upstream. Adding
include_source_commit_in_buildtoApplicationsController's settings block is a few lines in a file that already does exactly this eight times. If it lands, cast can set it declaratively and the manual step disappears from every future provision, on every future box. Worth an upstream issue/PR — the ask is small and the pattern is already there.Until then,
apply's warning is the only backstop, and it should also name the field, so anyone who wants to fix it upstream (or reach fortinker) knows what it is called:Resolved — and the premise of this issue was wrong.
applydoes not need to do anything.Read
ApplicationDeploymentJob(v4.1.2, line ~2993):At runtime
$forBuildTimeis false, so the toggle is not consulted at all —SOURCE_COMMITis injected into the container environment unconditionally. The setting gates only the build arg (lines 2232 and 3044). The checkbox's own helper text says as much, and we had all read past it:So why did
/versionreportunknown?The inner guard. Coolify skips the injection if the application already has an env var named
SOURCE_COMMIT. Our compose file used to reference${SOURCE_COMMIT}, which made Coolify auto-register exactly such a var — empty — which shadowed the runtime injection and pinned/versionatunknown. Flipping the toggle appeared to fix it, but only by baking the SHA into the image at build time. We diagnosed the symptom and adopted a workaround that also costs a full Docker rebuild on every deploy — the toggle is off by default precisely because a per-commit build arg busts the layer cache.The placeholder has since been removed from the compose file, and the freshly-created prod application carries no
SOURCE_COMMITenv var. So nothing shadows the injection, and/versionshould report the real SHA with the toggle off.Consequences
applyneeds neither to set the field nor to warn about it. The manual step it was written to defend against should not exist.SOURCE_COMMIT(orCOOLIFY_*) silently suppresses Coolify's own injection of that variable. Anything that writes env vars to an application — whichapply,captureandsmokeall do — can therefore disable a Coolify-provided value by writing an empty one. Orphan env vars are already reported bydiff; aSOURCE_COMMITorCOOLIFY_*orphan deserves to be called out by name, because its effect is invisible and it fails green.Verifying against the live box now (
/versionwith the toggle off). Will confirm here.Settled: the API cannot set it. Second branch it is.
I have no Coolify credentials, so I could not run the live
curlprobe — but the probe was not needed, and a live probe would in fact have been weaker evidence than what follows. The question is answerable from the source with certainty, and the answer does not depend on any one box's state. I read Coolify v4.1.2 (the whole tagged tree, not just the spec) and verified every claim independently.1. The field exists — as an application setting, not an application field
app/Models/ApplicationSetting.php'include_source_commit_in_build' => 'boolean',(cast)'include_source_commit_in_build',(fillable)database/migrations/2025_11_26_124200_add_build_cache_settings_to_application_settings.php:22Default
false— so every application cast creates has it off. That part of the premise was right.2. It has ZERO API surface
A grep of the entire v4.1.2 tree for
include_source_commit_in_build|includeSourceCommitInBuild, excludingvendor/, returns 17 hits and not one of them is underapp/Http/Controllers/Api/. The complete non-test, non-migration list:app/Jobs/ApplicationDeploymentJob.phpl.2232, 2949, 2993, 3044app/Livewire/Project/Application/Advanced.phpl.128app/Models/ApplicationSetting.phpl.19, 66app/Models/Application.phpl.1268app/Services/…/ApplicationConfigurationSnapshot.phpl.133resources/views/…/advanced.blade.phpl.17The sole writer in the codebase:
That is a Livewire component — a human, in the Advanced tab. There is no other path to that column.
3. And PATCH/POST would reject it, not ignore it
app/Http/Controllers/Api/ApplicationsController.phpenforces explicit allowlists and fails the request on any unrecognized key — this is the part that makes it airtight, because it rules out "send it anyway and hope":include_source_commit_in_buildis not among them.connect_to_docker_network— which is precisely why that one works today, and is the cleanest possible control for this experiment.So the issue's suspicion about the vendored OpenAPI spec was well-founded but, in this instance, the spec was not lying by omission — the capability genuinely does not exist. A live probe could only ever have produced a 422 here; the source proves why, and proves it for every box rather than for one.
4. The premise of this issue was wrong — and #50 found the real cause
This is the part worth flagging loudly. The toggle gates only the build-time arg:
At runtime,
$forBuildTimeisfalse→! $forBuildTimeistrue→ the condition short-circuits true regardless of the toggle. Coolify's runtime injection ofSOURCE_COMMITis unconditional. A service that readsprocess.env.SOURCE_COMMITat request time — which is exactly what/versiondoes — never needed this toggle at all.So the toggle was not why the live box reported
{"sha":"unknown"}. The real cause is the very next line:Coolify skips its own injection if the app already carries an env var of that name — so an orphan (even empty) app-level
SOURCE_COMMITsuppresses it. That is #50, and it is being fixed in parallel. Not this issue; not implemented here.What I built (second branch)
apply/diffnow say it out loud, once per dockercompose application, via the samedesiredFromManifestmechanism and in the same voice as the existing umami service-domains warning:I deliberately did not use the line drafted in the issue (
…or /version will report sha "unknown"), because per §4 that sentence is false —/versionreads the SHA at request time and is served by the unconditional runtime injection. Baking that wrong premise into the tool's own permanent output would mislead every future operator who reads it. The wording above states only what the source proves, and points at the build-arg case that genuinely does need the UI toggle.The invariant is also pinned in a comment exactly where a future reader would otherwise "fix" it by adding the field to
fields(which would 422 every run), and recorded indocs/semantics.mdunder known-limitations.