From 11e4d1cf11895c14447514cd8ac0b447f612ec09 Mon Sep 17 00:00:00 2001 From: CarmenDou <15951653662@163.com> Date: Fri, 18 Sep 2026 17:00:40 -0700 Subject: [PATCH] fix(template): mirror the platform's portless worker rules in the local manifest twin The platform now executes `type: worker` as its own port-0 service (insta-platform#490) and refuses the shapes that contradict that: a `port`, a `healthcheck`, or `alwaysOn: false` on a worker. The local twin says the same three things before the upload, so an author hears them against the file instead of from a 400. `alwaysOn` joins the ManifestService type, which the postgres bare-shape check already read. --- src/template-manifest.ts | 8 ++++++++ test/template.test.ts | 12 ++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/template-manifest.ts b/src/template-manifest.ts index cc5ac0a..bf8e36c 100644 --- a/src/template-manifest.ts +++ b/src/template-manifest.ts @@ -28,6 +28,7 @@ export type ManifestService = { port?: number healthcheck?: string volume?: boolean // needs a /data disk; the platform owns the size + alwaysOn?: boolean // idle mode; undeclared = the platform default (always-on for compute) env?: ManifestEnv } @@ -156,6 +157,13 @@ export function validateManifest(m: TemplateManifest): string[] { if (svc.port !== undefined && (!Number.isInteger(svc.port) || svc.port < 1 || svc.port > 65535)) { problems.push(`${where}: port must be an integer between 1 and 65535, got: ${svc.port}`) } + // A worker is PORTLESS: the platform deploys it as its own port-0 service, so nothing is routed + // to it and nothing probes it. The server's three refusals, said here before the upload. + if (svc.type === 'worker') { + if (svc.port !== undefined) problems.push(`${where}.port: a worker has no routed port — remove it (a worker is portless; declare type: web to serve HTTP)`) + if (svc.healthcheck !== undefined) problems.push(`${where}.healthcheck: a worker has no HTTP endpoint to probe — its health is the machine's state; remove it (or declare type: web)`) + if (svc.alwaysOn === false) problems.push(`${where}.alwaysOn: a worker cannot scale to zero — nothing is routed to it, so nothing would wake it; remove alwaysOn or set it true`) + } if (svc.type === 'web' && !svc.healthcheck) problems.push(`${where}: web services must declare a healthcheck path`) if (svc.healthcheck && !String(svc.healthcheck).startsWith('/')) problems.push(`${where}: healthcheck must be an absolute path (start with /)`) // Sizing is the platform's, capped for the org's plan. Same answers the diff --git a/test/template.test.ts b/test/template.test.ts index 02b5a37..e2afb9b 100644 --- a/test/template.test.ts +++ b/test/template.test.ts @@ -210,6 +210,18 @@ describe('validateManifest', () => { // The shape a manifest authors today passes. expect(validateManifest({ code: 'x', version: '1', services: { a: { type: 'worker', image: 'a:1', volume: true } } })).toEqual([]) }) + // A worker is PORTLESS (insta-platform#490): the executor deploys it as the platform's port-0 + // service, so a port, a healthcheck or scale-to-zero on one is a contradiction the author hears + // here, before the upload. Mirrors templateManifest.ts. + it('a worker is portless: port, healthcheck and alwaysOn:false are refused, the bare shape passes', () => { + const worker = (extra: Record) => + ({ code: 'x', version: '1', services: { bg: { type: 'worker', image: 'a:1', ...extra } } }) as unknown as TemplateManifest + expect(validateManifest(worker({}))).toEqual([]) + expect(validateManifest(worker({ alwaysOn: true }))).toEqual([]) + expect(validateManifest(worker({ port: 8080 })).join('\n')).toMatch(/a worker has no routed port/) + expect(validateManifest(worker({ healthcheck: '/' })).join('\n')).toMatch(/a worker has no HTTP endpoint/) + expect(validateManifest(worker({ alwaysOn: false })).join('\n')).toMatch(/a worker cannot scale to zero/) + }) }) describe('parseManifestYaml', () => {