diff --git a/docs/semantics.md b/docs/semantics.md index ac80ca1..fd1babc 100644 --- a/docs/semantics.md +++ b/docs/semantics.md @@ -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 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:-alpine` image string; Redis has no version diff --git a/src/resolve.ts b/src/resolve.ts index 43bb888..b02cf7c 100644 --- a/src/resolve.ts +++ b/src/resolve.ts @@ -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, diff --git a/test/resolve.test.ts b/test/resolve.test.ts index 0600702..620cb53 100644 --- a/test/resolve.test.ts +++ b/test/resolve.test.ts @@ -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 });