Merge pull request #54 from claude-hdb/feat/source-commit-notice

feat(resolve): warn that apply cannot enable "Include Source Commit in Build" (#46)
This commit is contained in:
Daniel Marin 2026-07-14 23:42:29 +01:00 committed by GitHub
commit 7c6092dd87
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 107 additions and 0 deletions

View file

@ -638,6 +638,33 @@ one used — verified against a live private clone.
Coolify 4.1.2 services — configure hostnames manually in the Coolify UI`)
once per run for every service that declared any. Set service hostnames
in the Coolify UI by hand.
- **"Include Source Commit in Build" cannot be enabled via the API in Coolify
4.1.2 — `apply` warns instead.** A dockercompose application whose build
consumes `SOURCE_COMMIT` as a **build arg** only receives it if the
per-application setting *Include Source Commit in Build* is on; Coolify
withholds it by default to preserve build cache. That setting
(`ApplicationSetting.include_source_commit_in_build`, default `false`) has no
API surface in 4.1.2: it appears in **zero** API controllers, and both the
create and PATCH allowlists in `ApplicationsController.php` (l.914, l.2368)
reject unrecognized keys outright (`"This field is not allowed."`), so cast
cannot smuggle it through — sending it would fail the whole request. Its only
writer is the Livewire *Advanced* tab
(`app/Livewire/Project/Application/Advanced.php:128`), i.e. a human in the UI.
Contrast `connect_to_docker_network`, which *is* in both allowlists and which
`apply` therefore does set on create. So `desiredFromManifest` **warns** once
per dockercompose application (`application <name> builds with dockercompose,
but apply cannot enable "Include Source Commit in Build" on Coolify 4.1.2 …`)
rather than pretending it is desired state. Enable it in the Coolify UI and
redeploy if your image bakes the SHA in at build time.
**This toggle gates the build-time arg only.** Coolify's **runtime** injection
of `SOURCE_COMMIT` is unconditional with respect to it
(`ApplicationDeploymentJob.php:2949` — `if (! $forBuildTime || …)`
short-circuits true at runtime), so a service that reads
`process.env.SOURCE_COMMIT` per request does **not** need the toggle at all.
What silently suppresses *that* value is an application-level env var of the
same name (`ApplicationDeploymentJob.php:2950`) — a distinct trap, and the
actual cause of a live box reporting `{"sha":"unknown"}`.
- **The redis default image is an unverified extrapolation.** Coolify's
"New Resource" wizard drives PostgreSQL version selection through a
verified `postgres:<version>-alpine` image string; Redis has no version

View file

@ -345,6 +345,34 @@ export function desiredFromManifest(
return env;
};
for (const [name, app] of Object.entries(envSpec.applications)) {
if (app.build.pack === "dockercompose") {
// Coolify gates the SOURCE_COMMIT *build arg* behind a per-application
// setting — `ApplicationSetting.include_source_commit_in_build`, default
// false — and in 4.1.2 that setting has NO API surface. Verified against
// the v4.1.2 source: it appears in zero API controllers, and both the
// create and the PATCH allowlists in ApplicationsController.php (l.914,
// l.2368) reject unrecognized keys outright ("This field is not
// allowed."), so sending it would fail the whole request rather than be
// quietly ignored. Its only writer is the Livewire Advanced tab
// (app/Livewire/Project/Application/Advanced.php:128) — i.e. a human, in
// the UI. Do NOT add it to `fields` below expecting apply to set it the
// way it sets `connect_to_docker_network` (which *is* in the allowlist,
// which is why that one works): apply would 422 on every run. Warning is
// the only honest move — a manual step the tool knows about and does not
// mention is one that gets forgotten, and this one fails green.
//
// Scope: the toggle gates the BUILD-time arg only. Coolify's *runtime*
// injection of SOURCE_COMMIT is unconditional with respect to it
// (ApplicationDeploymentJob.php:2949 — `if (! $forBuildTime || ...)`,
// which short-circuits true at runtime), so a service that reads
// process.env.SOURCE_COMMIT per request does not need this toggle at all.
// What *does* silently suppress that runtime value is an application-level
// env var of the same name (ApplicationDeploymentJob.php:2950) — a
// different bug, tracked separately.
console.warn(
`application ${name} builds with dockercompose, but apply cannot enable "Include Source Commit in Build" on Coolify 4.1.2 — the setting is absent from the API's field allowlist. If the build consumes SOURCE_COMMIT as a build arg, enable it in the Coolify UI and redeploy; Coolify injects SOURCE_COMMIT at runtime regardless.`,
);
}
desired.push({
kind: "application",
name,

View file

@ -305,7 +305,9 @@ environments:
join(dir, ".infra", "env", "core.prod.env.template"),
"PORT=3000\n",
);
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
const { desired } = desiredFromManifest(dir, "prod", {});
warn.mockRestore();
expect(desired).toHaveLength(1);
expect(desired[0]).toMatchObject({
kind: "application",
@ -325,6 +327,56 @@ environments:
expect(desired[0].fields).not.toHaveProperty("healthcheck");
expect(desired[0].fields).not.toHaveProperty("domains");
});
it('warns that apply cannot enable "Include Source Commit in Build" on a dockercompose app (unsettable via the Coolify 4.1.2 API)', () => {
const dir = mkdtempSync(join(tmpdir(), "infra-co-"));
mkdirSync(join(dir, ".infra"), { recursive: true });
writeFileSync(
join(dir, ".infra", "manifest.yaml"),
`project: widget
environments:
prod:
applications:
core:
source: { repo: acme/widget, branch: main }
build: { pack: dockercompose, base_directory: /, compose_file: docker-compose.yaml }
service_domains:
api: ["https://api.widget.example.com"]
`,
);
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
const { desired } = desiredFromManifest(dir, "prod", {});
expect(warn).toHaveBeenCalledTimes(1);
expect(warn.mock.calls[0][0]).toMatch(/application core/);
expect(warn.mock.calls[0][0]).toMatch(/Include Source Commit in Build/);
expect(warn.mock.calls[0][0]).toMatch(/Coolify UI/);
warn.mockRestore();
// The setting is absent from Coolify 4.1.2's create/PATCH allowlists, which
// reject unknown keys outright — so it must never reach `fields`, or apply
// would 422 on every run. Guards the fix a future reader would reach for.
expect(desired[0].fields).not.toHaveProperty(
"include_source_commit_in_build",
);
});
it("does not warn about the source-commit toggle for a non-dockercompose app (the build arg is a compose concern)", () => {
const dir = mkdtempSync(join(tmpdir(), "infra-co-"));
mkdirSync(join(dir, ".infra"), { recursive: true });
writeFileSync(
join(dir, ".infra", "manifest.yaml"),
`project: widget
environments:
prod:
applications:
site:
source: { repo: acme/widget, branch: main }
build: { pack: nixpacks, base_directory: / }
domains: ["https://widget.example.com"]
`,
);
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
desiredFromManifest(dir, "prod", {});
expect(warn).not.toHaveBeenCalled();
warn.mockRestore();
});
it("throws when the env is missing from the manifest", () => {
const dir = mkdtempSync(join(tmpdir(), "infra-co-"));
mkdirSync(join(dir, ".infra"), { recursive: true });