Merge pull request #64 from claude-hdb/fix/static-build-fields
fix(apply): express static-site build settings so a monorepo app is served, not run (#63)
This commit is contained in:
commit
9084713a5d
9 changed files with 405 additions and 3 deletions
|
|
@ -221,6 +221,60 @@ softened by an implementation detail):
|
|||
- **Reserved names:** cast never writes `SOURCE_COMMIT` or a `COOLIFY_*` var,
|
||||
under any manifest, in any environment. See below.
|
||||
|
||||
## Build settings (`install_command`, `build_command`, `start_command`, `static`)
|
||||
|
||||
A manifest application's `build` block carries, beyond `pack` and the checkout
|
||||
paths, four optional settings Coolify accepts on both the create
|
||||
(`POST /applications/private-github-app`) and update (`PATCH /applications/{uuid}`)
|
||||
routes:
|
||||
|
||||
```yaml
|
||||
landing:
|
||||
source: { repo: acme/widget, branch: main }
|
||||
build:
|
||||
pack: nixpacks
|
||||
base_directory: /
|
||||
install_command: npm ci
|
||||
build_command: npm run build -w apps/landing-site
|
||||
publish_directory: /apps/landing-site/dist
|
||||
static: true
|
||||
domains: ["https://widget.example.com"]
|
||||
```
|
||||
|
||||
- `install_command` / `build_command` / `start_command` are free-form strings,
|
||||
passed through verbatim — cast does not parse the shell in them. They exist so
|
||||
a **workspace monorepo** can scope the build to one app (`npm run build -w
|
||||
apps/landing-site`) instead of letting nixpacks auto-detect from the repo-root
|
||||
`package.json`, whose scripts may build and run a *different* workspace.
|
||||
- `static: true` maps to Coolify's `is_static`: Coolify then **serves
|
||||
`publish_directory` and runs no start command**. Without it a static site in a
|
||||
monorepo gets built and then *run* as its root `package.json` — for the
|
||||
incubator's `landing` that meant Coolify ran `npm run start -w apps/core`, the
|
||||
API server, which crash-looped on a missing `DATABASE_URL` (#63).
|
||||
|
||||
**All four are rejected on a `dockercompose` app** — a compose app builds and
|
||||
runs from its compose file, so Coolify never consults them — and `static: true`
|
||||
is rejected without a `publish_directory` (nothing to serve). Both are parse-time
|
||||
refusals, like the checkout-path rules above.
|
||||
|
||||
**Managing `is_static` is opt-in.** cast emits it only when the manifest declares
|
||||
`static:` — like the three commands, not on every app. Emitting `is_static:false`
|
||||
by default would make the first apply after this ships PATCH `is_static=false`
|
||||
onto any static/SPA app configured in the UI whose manifest has not yet been
|
||||
migrated — silently disabling static serving and re-creating the very crash, now
|
||||
caused by cast; a `pack: static` app that Coolify couples to `is_static=true`
|
||||
would drift-and-revert forever. So: declare `static: true` to serve, `static:
|
||||
false` to actively guard against a UI flip to `true`, or omit it to leave the
|
||||
field alone (Coolify keeps `pack` and `is_static` independent, which is why this
|
||||
is an explicit field, not inferred from `pack`). The three commands are likewise
|
||||
conditional (an unset command means "let the build pack decide"), diffed only
|
||||
when declared. `projectLiveFields` reads `is_static` back on every app so it is
|
||||
there to compare when a manifest does declare it. `draft` emits all four when the
|
||||
live box carries them (and `static` only alongside a `publish_directory`, so the
|
||||
draft always loads) — they used to sit in its `NO_HOME` list of settings a
|
||||
rebuild silently dropped, and `is_static` was not even there, which is exactly
|
||||
how a rebuilt static site came back wrong.
|
||||
|
||||
## Reserved env var names (`SOURCE_COMMIT`, `COOLIFY_*`)
|
||||
|
||||
Coolify injects a set of values into an application's runtime environment
|
||||
|
|
|
|||
15
src/cli.ts
15
src/cli.ts
|
|
@ -326,6 +326,17 @@ export function projectLiveFields(
|
|||
? { docker_compose_location: raw.docker_compose_location }
|
||||
: {}),
|
||||
...(composeDomains ? { docker_compose_domains: composeDomains } : {}),
|
||||
// is_static is read on every application so it is there to compare WHEN a
|
||||
// manifest declares `static:`. computeDiff compares only fields the DESIRED
|
||||
// side declares, so an app whose manifest omits `static` never diffs on it
|
||||
// (which is what keeps this from PATCHing is_static off an un-migrated
|
||||
// static app), and the three commands are the same — live carrying them
|
||||
// here is safe and never reads as spurious drift. Coolify's Application
|
||||
// model casts is_static to boolean; tolerate a 1/0 defensively.
|
||||
is_static: raw.is_static === true || raw.is_static === 1,
|
||||
...(raw.install_command ? { install_command: raw.install_command } : {}),
|
||||
...(raw.build_command ? { build_command: raw.build_command } : {}),
|
||||
...(raw.start_command ? { start_command: raw.start_command } : {}),
|
||||
};
|
||||
}
|
||||
if (kind === "database") {
|
||||
|
|
@ -2445,6 +2456,10 @@ export function applicationApiFields(
|
|||
const { port, healthcheck, domains, docker_compose_domains, ...rest } =
|
||||
fields;
|
||||
return {
|
||||
// is_static/install_command/build_command/start_command ride through `rest`
|
||||
// unchanged: they are valid API params verbatim, accepted on both the create
|
||||
// (POST /applications/private-github-app) and update (PATCH
|
||||
// /applications/{uuid}) routes — verified against the vendored OpenAPI.
|
||||
...rest,
|
||||
// ports_exposes wants a string; healthcheck -> health_check_path;
|
||||
// domains wants a comma-separated string, not an array.
|
||||
|
|
|
|||
30
src/draft.ts
30
src/draft.ts
|
|
@ -435,9 +435,6 @@ function applicationSpec(
|
|||
const NO_HOME: Array<[string, string]> = [
|
||||
["custom_labels", "custom Traefik/Docker labels (Basic Auth lives here)"],
|
||||
["ports_mappings", "host port mappings"],
|
||||
["install_command", "a custom install command"],
|
||||
["build_command", "a custom build command"],
|
||||
["start_command", "a custom start command"],
|
||||
["pre_deployment_command", "a pre-deployment command"],
|
||||
["post_deployment_command", "a post-deployment command"],
|
||||
["dockerfile", "an inline Dockerfile"],
|
||||
|
|
@ -483,6 +480,33 @@ function applicationSpec(
|
|||
...(!compose && r.raw.publish_directory
|
||||
? { publish_directory: String(r.raw.publish_directory) }
|
||||
: {}),
|
||||
// The three build/run commands and the static flag now HAVE manifest
|
||||
// fields (see manifest.ts), so a rebuild carries them instead of silently
|
||||
// dropping them. `is_static` was previously not even in NO_HOME, so a
|
||||
// rebuild lost it without a word — that is exactly the #63 crash: a static
|
||||
// site whose is_static was true on the box came back as false, got built
|
||||
// and RUN from the repo-root package.json, and crash-looped. draft now
|
||||
// carries it. (Compose apps get none of these — see the else above.)
|
||||
...(!compose && r.raw.install_command
|
||||
? { install_command: String(r.raw.install_command) }
|
||||
: {}),
|
||||
...(!compose && r.raw.build_command
|
||||
? { build_command: String(r.raw.build_command) }
|
||||
: {}),
|
||||
...(!compose && r.raw.start_command
|
||||
? { start_command: String(r.raw.start_command) }
|
||||
: {}),
|
||||
// Gated on publish_directory as well: the schema refuses `static: true`
|
||||
// with nothing to serve, and a draft must only ever emit a manifest that
|
||||
// loads. Coolify's static apps carry a publish_directory (its default is
|
||||
// `/`), so this drops `static` only for a box in a state the manifest could
|
||||
// not express anyway — a loud, correct omission rather than an unloadable
|
||||
// file.
|
||||
...(!compose &&
|
||||
(r.raw.is_static === true || r.raw.is_static === 1) &&
|
||||
r.raw.publish_directory
|
||||
? { static: true }
|
||||
: {}),
|
||||
},
|
||||
...(compose
|
||||
? {}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,19 @@ const AppSpecSchema = z
|
|||
base_directory: repoDirectoryPath("base_directory"),
|
||||
publish_directory: repoDirectoryPath("publish_directory").optional(),
|
||||
compose_file: composeFilePath.optional(),
|
||||
// The three build/run commands and the static flag Coolify accepts on
|
||||
// both the create (POST /applications/private-github-app) and the
|
||||
// update (PATCH /applications/{uuid}) routes. They are free-form
|
||||
// strings passed through verbatim — cast does not parse or validate the
|
||||
// shell in them, only whether they belong on this pack (superRefine
|
||||
// below). `static` maps to Coolify's `is_static`: it makes Coolify
|
||||
// SERVE `publish_directory` and run NO start command, which is exactly
|
||||
// the fix for a static site in a workspace monorepo that otherwise gets
|
||||
// built and RUN from the repo-root package.json (#63).
|
||||
install_command: z.string().optional(),
|
||||
build_command: z.string().optional(),
|
||||
start_command: z.string().optional(),
|
||||
static: z.boolean().optional(),
|
||||
})
|
||||
.strict(),
|
||||
port: z.number().int().optional(),
|
||||
|
|
@ -75,6 +88,26 @@ const AppSpecSchema = z
|
|||
code: "custom",
|
||||
message: "publish_directory not allowed on a dockercompose app",
|
||||
});
|
||||
// A compose app builds and runs from its compose file — Coolify never
|
||||
// consults these on it. Reject them at parse time rather than post them
|
||||
// and have them silently ignored (the same reasoning as publish_directory
|
||||
// and port above).
|
||||
for (const k of [
|
||||
"install_command",
|
||||
"build_command",
|
||||
"start_command",
|
||||
] as const)
|
||||
if (app.build[k] !== undefined)
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: `build.${k} not allowed on a dockercompose app (it builds from its compose file)`,
|
||||
});
|
||||
if (app.build.static !== undefined)
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message:
|
||||
"build.static not allowed on a dockercompose app (a compose file decides what is served)",
|
||||
});
|
||||
} else {
|
||||
if (!app.domains)
|
||||
ctx.addIssue({
|
||||
|
|
@ -87,6 +120,16 @@ const AppSpecSchema = z
|
|||
message:
|
||||
"service_domains/compose_file only allowed with pack dockercompose",
|
||||
});
|
||||
// `static: true` tells Coolify to serve publish_directory and run no
|
||||
// start command — so a static app with nothing to serve is almost
|
||||
// certainly a mistake, and one that would deploy green while serving an
|
||||
// empty site. Catch it in the file, once, not on a live box.
|
||||
if (app.build.static === true && !app.build.publish_directory)
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message:
|
||||
"build.static: true serves publish_directory and runs no start command — but no publish_directory is set, so there is nothing to serve",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -414,6 +414,31 @@ export function desiredFromManifest(
|
|||
...(app.port !== undefined ? { port: app.port } : {}),
|
||||
...(app.healthcheck ? { healthcheck: app.healthcheck } : {}),
|
||||
domains: app.domains,
|
||||
// Emitted only when the manifest DECLARES `static:` — like the
|
||||
// three commands, not unconditionally. Emitting `is_static:false`
|
||||
// on every non-compose app would make the first apply after this
|
||||
// ships PATCH `is_static=false` onto any static/SPA app configured
|
||||
// in the UI whose manifest has not yet been migrated — silently
|
||||
// disabling static serving and re-creating the #63 crash, now
|
||||
// caused by cast. And a `pack: static` app that Coolify couples to
|
||||
// is_static=true would drift-and-revert forever. So managing
|
||||
// is_static is opt-in: declare `static: true` to serve, `static:
|
||||
// false` to actively guard against a UI flip to true, or omit it to
|
||||
// leave the field alone. (Coolify keeps pack and is_static
|
||||
// independent, which is why this stays an explicit field, not a
|
||||
// heuristic off `pack`.)
|
||||
...(app.build.static !== undefined
|
||||
? { is_static: app.build.static }
|
||||
: {}),
|
||||
...(app.build.install_command !== undefined
|
||||
? { install_command: app.build.install_command }
|
||||
: {}),
|
||||
...(app.build.build_command !== undefined
|
||||
? { build_command: app.build.build_command }
|
||||
: {}),
|
||||
...(app.build.start_command !== undefined
|
||||
? { start_command: app.build.start_command }
|
||||
: {}),
|
||||
}),
|
||||
},
|
||||
env: resolveEnvFile(name, app.env_template),
|
||||
|
|
|
|||
|
|
@ -171,6 +171,81 @@ describe("planDraft — the emitted shape", () => {
|
|||
expect(env.generated_secrets).toEqual(["DATABASE_URL"]);
|
||||
});
|
||||
|
||||
// #63: is_static was previously not even in NO_HOME, so a rebuild silently
|
||||
// lost it — the exact crash. draft now carries it, plus the install/build/
|
||||
// start commands that used to be flagged as NO_HOME.
|
||||
it("carries static + install/build/start commands, and does NOT flag them as uncaptured", () => {
|
||||
const p = project({
|
||||
resources: [
|
||||
{
|
||||
kind: "application",
|
||||
name: "Landing",
|
||||
uuid: "a1",
|
||||
raw: {
|
||||
git_repository: "https://github.com/heavy-duty/incubator",
|
||||
git_branch: "main",
|
||||
build_pack: "static",
|
||||
base_directory: "/",
|
||||
publish_directory: "/apps/landing-site/dist",
|
||||
fqdn: "https://landing.example.com",
|
||||
is_static: true,
|
||||
install_command: "npm ci",
|
||||
build_command: "npm run build -w apps/landing-site",
|
||||
},
|
||||
env: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
const plan = planDraft([p], ctx);
|
||||
const manifest = plan.files.find((f) => f.path.endsWith("manifest.yaml"));
|
||||
const dir = mkdtempSync(join(tmpdir(), "cast-draft-"));
|
||||
const path = join(dir, "manifest.yaml");
|
||||
writeFileSync(path, manifest?.content ?? "");
|
||||
const build =
|
||||
loadManifest(path).environments.prod.applications.Landing.build;
|
||||
expect(build.static).toBe(true);
|
||||
expect(build.install_command).toBe("npm ci");
|
||||
expect(build.build_command).toBe("npm run build -w apps/landing-site");
|
||||
// These now have a manifest home, so they must NOT be reported as settings
|
||||
// cast could see but not express.
|
||||
for (const setting of ["is_static", "install_command", "build_command"]) {
|
||||
expect(plan.uncaptured.some((u) => u.setting === setting)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
// A draft must only ever emit a manifest that LOADS. is_static true with no
|
||||
// publish_directory would be `static: true` with nothing to serve, which the
|
||||
// schema refuses — so draft omits `static` for that (rare, malformed) box
|
||||
// rather than writing a file that throws on load.
|
||||
it("does not emit static:true when the box has is_static but no publish_directory", () => {
|
||||
const p = project({
|
||||
resources: [
|
||||
{
|
||||
kind: "application",
|
||||
name: "Odd",
|
||||
uuid: "a1",
|
||||
raw: {
|
||||
git_repository: "https://github.com/heavy-duty/incubator",
|
||||
git_branch: "main",
|
||||
build_pack: "nixpacks",
|
||||
base_directory: "/",
|
||||
fqdn: "https://odd.example.com",
|
||||
is_static: true,
|
||||
},
|
||||
env: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
const plan = planDraft([p], ctx);
|
||||
const manifest = plan.files.find((f) => f.path.endsWith("manifest.yaml"));
|
||||
const dir = mkdtempSync(join(tmpdir(), "cast-draft-"));
|
||||
const path = join(dir, "manifest.yaml");
|
||||
writeFileSync(path, manifest?.content ?? "");
|
||||
// The whole point: it loads (does not throw), and simply carries no `static`.
|
||||
const build = loadManifest(path).environments.prod.applications.Odd.build;
|
||||
expect(build).not.toHaveProperty("static");
|
||||
});
|
||||
|
||||
it("emits an env template every cast reader can parse", () => {
|
||||
const plan = planDraft([project()], ctx);
|
||||
const tpl = plan.files.find((f) => f.path.endsWith(".env.template"));
|
||||
|
|
|
|||
|
|
@ -212,6 +212,77 @@ environments:
|
|||
}),
|
||||
).toThrow(/service_domains/);
|
||||
});
|
||||
// #63: the working (hand-built) config for a static site in a workspace
|
||||
// monorepo — is_static plus workspace-scoped install/build commands that build
|
||||
// only the target app and serve its dist, instead of the repo root's start
|
||||
// script booting a different workspace.
|
||||
it("accepts install/build/start commands and static on a non-compose app, round-tripping them", () => {
|
||||
const m = loadManifest(`${FIX}manifest.yaml`, {
|
||||
overrideText: `
|
||||
project: widget
|
||||
environments:
|
||||
prod:
|
||||
applications:
|
||||
landing:
|
||||
source: { repo: acme/widget, branch: main }
|
||||
build:
|
||||
pack: static
|
||||
base_directory: /
|
||||
publish_directory: /apps/landing-site/dist
|
||||
install_command: npm ci
|
||||
build_command: npm run build -w apps/landing-site
|
||||
start_command: node server.js
|
||||
static: true
|
||||
domains: ["https://landing.example.com"]
|
||||
`,
|
||||
});
|
||||
const b = m.environments.prod.applications.landing.build;
|
||||
expect(b.install_command).toBe("npm ci");
|
||||
expect(b.build_command).toBe("npm run build -w apps/landing-site");
|
||||
expect(b.start_command).toBe("node server.js");
|
||||
expect(b.static).toBe(true);
|
||||
expect(b.publish_directory).toBe("/apps/landing-site/dist");
|
||||
});
|
||||
it("rejects static: true with nothing to serve (no publish_directory)", () => {
|
||||
expect(() =>
|
||||
loadManifest(`${FIX}manifest.yaml`, {
|
||||
overrideText: `
|
||||
project: widget
|
||||
environments:
|
||||
prod:
|
||||
applications:
|
||||
landing:
|
||||
source: { repo: acme/widget, branch: main }
|
||||
build: { pack: static, base_directory: /, static: true }
|
||||
domains: ["https://landing.example.com"]
|
||||
`,
|
||||
}),
|
||||
).toThrow(/nothing to serve/);
|
||||
});
|
||||
it("rejects install/build/start commands and static on a dockercompose app", () => {
|
||||
for (const field of [
|
||||
"install_command: npm ci",
|
||||
"build_command: npm run build",
|
||||
"start_command: node server.js",
|
||||
"static: true",
|
||||
]) {
|
||||
expect(() =>
|
||||
loadManifest(`${FIX}manifest.yaml`, {
|
||||
overrideText: `
|
||||
project: widget
|
||||
environments:
|
||||
prod:
|
||||
applications:
|
||||
core:
|
||||
source: { repo: acme/widget, branch: main }
|
||||
build: { pack: dockercompose, base_directory: /, compose_file: /docker-compose.yaml, ${field} }
|
||||
service_domains:
|
||||
api: ["https://api.example.com"]
|
||||
`,
|
||||
}),
|
||||
).toThrow(/not allowed on a dockercompose app/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadBindings", () => {
|
||||
|
|
|
|||
|
|
@ -158,6 +158,64 @@ environments:
|
|||
value: "secret-v",
|
||||
secret: true,
|
||||
});
|
||||
// None of the four build settings are emitted for an app that declares none:
|
||||
// managing is_static is opt-in (declaring it would otherwise PATCH static
|
||||
// serving OFF on an un-migrated app), and the commands default to "let the
|
||||
// build pack decide".
|
||||
expect(desired[0].fields).not.toHaveProperty("is_static");
|
||||
expect(desired[0].fields).not.toHaveProperty("install_command");
|
||||
expect(desired[0].fields).not.toHaveProperty("build_command");
|
||||
expect(desired[0].fields).not.toHaveProperty("start_command");
|
||||
});
|
||||
it("emits is_static:false when static:false is explicitly declared (a guard against a UI flip)", () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "infra-co-"));
|
||||
mkdirSync(join(dir, ".infra"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(dir, ".infra", "manifest.yaml"),
|
||||
`project: widget
|
||||
environments:
|
||||
staging:
|
||||
applications:
|
||||
core:
|
||||
source: { repo: acme/widget, branch: main }
|
||||
build: { pack: nixpacks, base_directory: /, static: false }
|
||||
domains: ["https://c.example.com"]
|
||||
`,
|
||||
);
|
||||
const { desired } = desiredFromManifest(dir, "staging", {});
|
||||
expect(desired[0].fields.is_static).toBe(false);
|
||||
});
|
||||
// #63: the static-site build settings a workspace monorepo needs.
|
||||
it("emits is_static:true and the three commands for a non-compose app that declares them", () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "infra-co-"));
|
||||
mkdirSync(join(dir, ".infra"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(dir, ".infra", "manifest.yaml"),
|
||||
`project: widget
|
||||
environments:
|
||||
staging:
|
||||
applications:
|
||||
landing:
|
||||
source: { repo: acme/widget, branch: main }
|
||||
build:
|
||||
pack: static
|
||||
base_directory: /
|
||||
publish_directory: /apps/landing-site/dist
|
||||
install_command: npm ci
|
||||
build_command: npm run build -w apps/landing-site
|
||||
start_command: node server.js
|
||||
static: true
|
||||
domains: ["https://landing.example.com"]
|
||||
`,
|
||||
);
|
||||
const { desired } = desiredFromManifest(dir, "staging", {});
|
||||
expect(desired[0].fields).toMatchObject({
|
||||
is_static: true,
|
||||
install_command: "npm ci",
|
||||
build_command: "npm run build -w apps/landing-site",
|
||||
start_command: "node server.js",
|
||||
publish_directory: "/apps/landing-site/dist",
|
||||
});
|
||||
});
|
||||
// The reverse of what this file used to assert. `backup` was deliberately
|
||||
// routed AROUND `fields` into a side channel, because live Coolify was
|
||||
|
|
@ -345,6 +403,12 @@ environments:
|
|||
expect(desired[0].fields).not.toHaveProperty("port");
|
||||
expect(desired[0].fields).not.toHaveProperty("healthcheck");
|
||||
expect(desired[0].fields).not.toHaveProperty("domains");
|
||||
// A compose app builds from its compose file — none of the static/command
|
||||
// fields belong on it, not even is_static (which every NON-compose app gets).
|
||||
expect(desired[0].fields).not.toHaveProperty("is_static");
|
||||
expect(desired[0].fields).not.toHaveProperty("install_command");
|
||||
expect(desired[0].fields).not.toHaveProperty("build_command");
|
||||
expect(desired[0].fields).not.toHaveProperty("start_command");
|
||||
});
|
||||
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-"));
|
||||
|
|
|
|||
|
|
@ -119,6 +119,37 @@ describe("projectLiveFields", () => {
|
|||
});
|
||||
expect(out).not.toHaveProperty("docker_compose_domains");
|
||||
});
|
||||
|
||||
// #63: the static-site fields must read back off a live application so they
|
||||
// diff against the manifest — otherwise a UI flip of is_static is invisible.
|
||||
it("reads is_static and the build/run commands off a live application", () => {
|
||||
const out = projectLiveFields("application", {
|
||||
git_repository: "org/repo",
|
||||
git_branch: "main",
|
||||
build_pack: "static",
|
||||
base_directory: "/",
|
||||
is_static: true,
|
||||
install_command: "npm ci",
|
||||
build_command: "npm run build -w apps/landing-site",
|
||||
start_command: "node server.js",
|
||||
});
|
||||
expect(out.is_static).toBe(true);
|
||||
expect(out.install_command).toBe("npm ci");
|
||||
expect(out.build_command).toBe("npm run build -w apps/landing-site");
|
||||
expect(out.start_command).toBe("node server.js");
|
||||
});
|
||||
|
||||
it("always reports is_static as a boolean, tolerating Coolify's 1/0", () => {
|
||||
expect(projectLiveFields("application", { is_static: 1 }).is_static).toBe(
|
||||
true,
|
||||
);
|
||||
expect(projectLiveFields("application", { is_static: 0 }).is_static).toBe(
|
||||
false,
|
||||
);
|
||||
// Absent on the wire reads as false (its real default), never undefined —
|
||||
// so it compares against the desired side, which always emits it.
|
||||
expect(projectLiveFields("application", {}).is_static).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("compose app idempotency (review finding #2)", () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue