feat(resolve): warn that apply cannot enable "Include Source Commit in Build" (#46) #54

Merged
dan-claude-bot merged 1 commit from feat/source-commit-notice into main 2026-07-14 22:42:30 +00:00
3 changed files with 107 additions and 0 deletions

View file

@ -624,6 +624,33 @@ one used — verified against a live private clone.
Coolify 4.1.2 services — configure hostnames manually in the Coolify UI`) Coolify 4.1.2 services — configure hostnames manually in the Coolify UI`)
once per run for every service that declared any. Set service hostnames once per run for every service that declared any. Set service hostnames
in the Coolify UI by hand. 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 - **The redis default image is an unverified extrapolation.** Coolify's
"New Resource" wizard drives PostgreSQL version selection through a "New Resource" wizard drives PostgreSQL version selection through a
verified `postgres:<version>-alpine` image string; Redis has no version verified `postgres:<version>-alpine` image string; Redis has no version

View file

@ -345,6 +345,34 @@ export function desiredFromManifest(
return env; return env;
}; };
for (const [name, app] of Object.entries(envSpec.applications)) { 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({ desired.push({
kind: "application", kind: "application",
name, name,

View file

@ -305,7 +305,9 @@ environments:
join(dir, ".infra", "env", "core.prod.env.template"), join(dir, ".infra", "env", "core.prod.env.template"),
"PORT=3000\n", "PORT=3000\n",
); );
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
const { desired } = desiredFromManifest(dir, "prod", {}); const { desired } = desiredFromManifest(dir, "prod", {});
warn.mockRestore();
expect(desired).toHaveLength(1); expect(desired).toHaveLength(1);
expect(desired[0]).toMatchObject({ expect(desired[0]).toMatchObject({
kind: "application", kind: "application",
@ -325,6 +327,56 @@ environments:
expect(desired[0].fields).not.toHaveProperty("healthcheck"); expect(desired[0].fields).not.toHaveProperty("healthcheck");
expect(desired[0].fields).not.toHaveProperty("domains"); 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", () => { it("throws when the env is missing from the manifest", () => {
const dir = mkdtempSync(join(tmpdir(), "infra-co-")); const dir = mkdtempSync(join(tmpdir(), "infra-co-"));
mkdirSync(join(dir, ".infra"), { recursive: true }); mkdirSync(join(dir, ".infra"), { recursive: true });