@fabrika/provider-zerops 0.8.1 → 0.9.0-cloudflare.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/package.json +16 -14
  2. package/src/api/client.ts +18 -0
  3. package/src/api/readers.ts +29 -0
  4. package/src/api/types.ts +64 -0
  5. package/src/api.ts +2 -0
  6. package/src/cli.ts +11 -0
  7. package/src/collaborators.ts +0 -14
  8. package/src/control.ts +35 -57
  9. package/src/execution.ts +113 -102
  10. package/src/index.ts +2 -0
  11. package/src/installation/admin.ts +1 -1
  12. package/src/installation/deploy-options.ts +1 -2
  13. package/src/installation/deploy.ts +225 -80
  14. package/src/installation/generated/platform-console-schema.ts +3 -0
  15. package/src/installation/generated/platform-proxy-manifest.ts +21 -1
  16. package/src/installation/guide.md +48 -18
  17. package/src/installation/hosts.ts +1 -7
  18. package/src/installation/index.ts +13 -16
  19. package/src/installation/init-options.ts +2 -1
  20. package/src/installation/install-environment.ts +47 -108
  21. package/src/installation/install-options.ts +3 -1
  22. package/src/installation/install-secrets.ts +24 -68
  23. package/src/installation/install.ts +9 -4
  24. package/src/installation/sidecar.ts +5 -26
  25. package/src/installation/topology/setups.ts +15 -4
  26. package/src/installation/topology/topology.ts +31 -21
  27. package/src/installation/upgrade-options.ts +2 -2
  28. package/src/managed-env.ts +1 -174
  29. package/src/manifest.ts +11 -14
  30. package/src/namespace/claims.ts +56 -1
  31. package/src/namespace/errors.ts +1 -1
  32. package/src/namespace/manifest.ts +6 -19
  33. package/src/namespace/provisioning.ts +24 -4
  34. package/src/namespace/proxy-attempt.ts +43 -2
  35. package/src/namespace/proxy-reads.ts +12 -22
  36. package/src/namespace/types.ts +2 -0
  37. package/src/namespace-cli.ts +152 -0
  38. package/src/plan.ts +2 -7
  39. package/src/provider.ts +128 -126
  40. package/src/proxy-manifest-carrier.ts +18 -0
  41. package/src/types.ts +0 -2
  42. package/types/src/api/client.d.ts +5 -0
  43. package/types/src/api/errors.d.ts +24 -0
  44. package/types/src/api/polling.d.ts +41 -0
  45. package/types/src/api/readers.d.ts +1115 -0
  46. package/types/src/api/service-env-limit.d.ts +17 -0
  47. package/types/src/api/service-env-response.d.ts +23 -0
  48. package/types/src/api/types.d.ts +687 -0
  49. package/types/src/api.d.ts +6 -0
  50. package/types/src/authoring.d.ts +9 -0
  51. package/types/src/cli-args.d.ts +19 -0
  52. package/types/src/cli.d.ts +13 -0
  53. package/types/src/collaborators.d.ts +16 -0
  54. package/types/src/compile.d.ts +91 -0
  55. package/types/src/control.d.ts +40 -0
  56. package/types/src/database-budget.d.ts +38 -0
  57. package/types/src/descriptor.d.ts +28 -0
  58. package/types/src/execution.d.ts +125 -0
  59. package/types/src/exposure.d.ts +47 -0
  60. package/types/src/index.d.ts +31 -0
  61. package/types/src/installation/admin-options.d.ts +24 -0
  62. package/types/src/installation/admin.d.ts +35 -0
  63. package/types/src/installation/checker-release.d.ts +12 -0
  64. package/types/src/installation/deploy-options.d.ts +37 -0
  65. package/types/src/installation/deploy.d.ts +136 -0
  66. package/types/src/installation/generated/platform-console-schema.d.ts +2 -0
  67. package/types/src/installation/generated/platform-proxy-manifest.d.ts +2 -0
  68. package/types/src/installation/hosts.d.ts +34 -0
  69. package/types/src/installation/index.d.ts +7 -0
  70. package/types/src/installation/init-options.d.ts +38 -0
  71. package/types/src/installation/init.d.ts +60 -0
  72. package/types/src/installation/install-collaborators.d.ts +24 -0
  73. package/types/src/installation/install-environment.d.ts +63 -0
  74. package/types/src/installation/install-options.d.ts +49 -0
  75. package/types/src/installation/install-project.d.ts +13 -0
  76. package/types/src/installation/install-provisioning.d.ts +58 -0
  77. package/types/src/installation/install-secrets.d.ts +17 -0
  78. package/types/src/installation/install-token.d.ts +10 -0
  79. package/types/src/installation/install.d.ts +23 -0
  80. package/types/src/installation/installation-source.d.ts +8 -0
  81. package/types/src/installation/log.d.ts +44 -0
  82. package/types/src/installation/machine-key.d.ts +19 -0
  83. package/types/src/installation/placement-options.d.ts +11 -0
  84. package/types/src/installation/platform-archive.d.ts +7 -0
  85. package/types/src/installation/platform-source.d.ts +8 -0
  86. package/types/src/installation/platform-upload.d.ts +3 -0
  87. package/types/src/installation/sidecar.d.ts +49 -0
  88. package/types/src/installation/source-options.d.ts +1 -0
  89. package/types/src/installation/topology/artifacts.d.ts +8 -0
  90. package/types/src/installation/topology/index.d.ts +5 -0
  91. package/types/src/installation/topology/installation-schemas.d.ts +2242 -0
  92. package/types/src/installation/topology/invariants.d.ts +23 -0
  93. package/types/src/installation/topology/setups.d.ts +59 -0
  94. package/types/src/installation/topology/topology.d.ts +153 -0
  95. package/types/src/installation/topology/validate.d.ts +11 -0
  96. package/types/src/installation/upgrade-options.d.ts +26 -0
  97. package/types/src/installation/upgrade-output.d.ts +6 -0
  98. package/types/src/installation/upgrade.d.ts +88 -0
  99. package/types/src/managed-env.d.ts +24 -0
  100. package/types/src/manifest.d.ts +204 -0
  101. package/types/src/namespace/claims.d.ts +16 -0
  102. package/types/src/namespace/codec.d.ts +334 -0
  103. package/types/src/namespace/constants.d.ts +25 -0
  104. package/types/src/namespace/errors.d.ts +15 -0
  105. package/types/src/namespace/manifest.d.ts +4 -0
  106. package/types/src/namespace/operator.d.ts +12 -0
  107. package/types/src/namespace/provisioning.d.ts +4 -0
  108. package/types/src/namespace/proxy-attempt.d.ts +39 -0
  109. package/types/src/namespace/proxy-reads.d.ts +22 -0
  110. package/types/src/namespace/proxy-secrets.d.ts +8 -0
  111. package/types/src/namespace/proxy-source.d.ts +74 -0
  112. package/types/src/namespace/topology.d.ts +9 -0
  113. package/types/src/namespace/types.d.ts +101 -0
  114. package/types/src/namespace/update.d.ts +6 -0
  115. package/types/src/namespace/validation.d.ts +12 -0
  116. package/types/src/namespace-cli.d.ts +2 -0
  117. package/types/src/namespace.d.ts +9 -0
  118. package/types/src/plan.d.ts +11 -0
  119. package/types/src/provider.d.ts +63 -0
  120. package/types/src/proxy-manifest-carrier.d.ts +24 -0
  121. package/types/src/schema.generated.d.ts +376 -0
  122. package/types/src/service-names.d.ts +4 -0
  123. package/types/src/service-references.d.ts +13 -0
  124. package/types/src/source/constants.d.ts +6 -0
  125. package/types/src/source/port.d.ts +60 -0
  126. package/types/src/source/types.d.ts +4 -0
  127. package/types/src/source/validation.d.ts +19 -0
  128. package/types/src/source.d.ts +2 -0
  129. package/types/src/types.d.ts +119 -0
  130. package/types/src/version-attempt.d.ts +8 -0
  131. package/src/installation/mcp.ts +0 -102
  132. package/src/installation/proxy-manifest.ts +0 -214
  133. package/src/installation/secrets.ts +0 -78
package/src/execution.ts CHANGED
@@ -2,15 +2,20 @@
2
2
  // runs. Of the authored setup only `build.base`, `build.os` and the SPA's `build.buildCommands` survive; the rest of
3
3
  // the build, the deploy gate and the whole `run` section are generated here. PURE — no network, no filesystem.
4
4
 
5
+ import { type ContractRecipe, contractRecipe } from '@fabrika/app/build'
5
6
  import {
6
7
  type ContractArtifact,
8
+ DEV_LAUNCHER_BOOTED_FILE,
9
+ DEV_LAUNCHER_CONFIG_FILE,
10
+ DEV_LAUNCHER_MODULE,
11
+ type DevLauncherConfig,
7
12
  FABRIKA_CONTRACT_DIGEST,
8
13
  FABRIKA_SOURCE_MAP_UPLOAD,
9
14
  FABRIKA_SOURCE_MAP_UPLOAD_KEY,
10
15
  type FrameworkVariableName,
11
16
  } from '@fabrika/auth-core'
12
17
  import type { OperationsAssetRoot, OperationsSourceMapBuildUpload } from '@fabrika/operations-contract/releases'
13
- import { FRAMEWORK_RELEASE_PATTERN, type JsonValue } from '@fabrika/provider-contract'
18
+ import type { JsonValue } from '@fabrika/provider-contract'
14
19
  import packageManifest from '../package.json'
15
20
  import { renderYaml } from './compile'
16
21
  import { parseZeropsDescriptor, type PreparedZeropsDescriptor, selectZeropsDescriptorSetup } from './descriptor'
@@ -29,8 +34,7 @@ export const ZEROPS_APPLICATION_RUNTIME = 'alpine/bun@1.3'
29
34
  */
30
35
  export const FABRIKA_CHECK_VERSION: string = packageManifest.version
31
36
 
32
- /** The compiler the platform type-checks with. It is fetched by version, never taken from the application's lockfile. */
33
- export const PLATFORM_TYPESCRIPT_VERSION = '5.9.3'
37
+ export { PLATFORM_TYPESCRIPT_VERSION } from '@fabrika/app/build'
34
38
 
35
39
  /** System packages an application may add to its runtime image, installed by the generated `run.prepareCommands`. */
36
40
  export const ZEROPS_RUNTIME_PACKAGES = ['ffmpeg', 'ghostscript', 'imagemagick', 'poppler-utils', 'vips'] as const
@@ -42,12 +46,6 @@ export interface ZeropsContractJob {
42
46
  readonly schedule: string
43
47
  }
44
48
 
45
- /** The framework's file scan job (`@fabrika/app`'s `FILE_SCAN_JOB`), run every minute when a collection scans. */
46
- export const ZEROPS_FILE_SCAN_JOB = 'fabrika-file-scan'
47
- /** The framework's file sweep job (`@fabrika/app`'s `FILE_SWEEP_JOB` and its schedule), run when the contract has files. */
48
- export const ZEROPS_FILE_SWEEP_JOB = 'fabrika-file-sweep'
49
- const FILE_SWEEP_SCHEDULE = '41 * * * *'
50
-
51
49
  /**
52
50
  * The RAM in GB a container of a scanning application starts with: one `clamscan` over a batch peaks at about 1 GB
53
51
  * resident while it loads its signatures (measured with ClamAV 1.4), beside the application's own process.
@@ -107,13 +105,26 @@ export interface ZeropsLockedExecution {
107
105
  readonly runtimePackages: readonly ZeropsRuntimePackage[]
108
106
  /** Where the build uploads its source maps, without the bearer; null when Control minted no upload for the run. */
109
107
  readonly sourceMaps: OperationsSourceMapBuildUpload | null
108
+ /**
109
+ * Present for an environment of kind `dev` (ADR-0049): `run.start` runs the dev launcher instead of the serving
110
+ * entry. The environment's kind selects it; an application never authors it.
111
+ */
112
+ readonly dev?: ZeropsDevExecution
113
+ }
114
+
115
+ export interface ZeropsDevExecution {
116
+ /** The commit the bootstrap run builds: what a new container holds before the launcher's first sync. */
117
+ readonly head: string
110
118
  }
111
119
 
112
120
  /** The application ids the generated scripts may name: the contract's own id rule, so a script needs no quoting. */
113
- export const LOCKED_APP_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/
121
+ export { CONTRACT_APP_ID_PATTERN as LOCKED_APP_ID_PATTERN } from '@fabrika/app/build'
114
122
 
115
123
  export const CONTRACT_DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/
116
124
 
125
+ /** A literal path of unreserved characters: it is written into the generated probes unquoted by any shell. */
126
+ export const READINESS_PATH_PATTERN = /^(?:\/[A-Za-z0-9_~-][A-Za-z0-9._~-]*)+$/
127
+
117
128
  const byName = <T extends { readonly name: string }>(left: T, right: T): number => left.name < right.name ? -1 : left.name > right.name ? 1 : 0
118
129
 
119
130
  /**
@@ -241,31 +252,6 @@ export function lockedAuthoredBuild(entry: { readonly [key: string]: JsonValue }
241
252
 
242
253
  const SOURCE_ROOT = '/build/source'
243
254
  const RUNTIME_ROOT = '/var/www'
244
- const FABRIKA_BIN = './node_modules/.bin/fabrika'
245
- const MIGRATE_SCRIPT = 'fabrika/migrate.generated.sh'
246
- const VERIFY_SCRIPT = 'fabrika/verify.generated.sh'
247
- const START_SCRIPT = 'fabrika/start.generated.sh'
248
- const PLATFORM_TSCONFIG = 'fabrika/tsconfig.platform.json'
249
-
250
- /** The compiler options of the platform's type check; the application's own `tsconfig.json` and its `paths` are not read. */
251
- const PLATFORM_TSCONFIG_CONTENTS = JSON.stringify({
252
- compilerOptions: {
253
- target: 'ES2022',
254
- module: 'ESNext',
255
- moduleResolution: 'bundler',
256
- lib: ['ES2022', 'DOM', 'DOM.Iterable'],
257
- types: ['bun'],
258
- strict: true,
259
- noUncheckedIndexedAccess: true,
260
- noImplicitOverride: true,
261
- forceConsistentCasingInFileNames: true,
262
- skipLibCheck: true,
263
- resolveJsonModule: true,
264
- isolatedModules: true,
265
- noEmit: true,
266
- },
267
- include: ['../fabrika.contract.ts', '../src', 'entry.generated.ts'],
268
- })
269
255
 
270
256
  /** One `printf` writing `lines` to `path`. Each line is single-quoted, so it may hold neither `'` nor an expansion. */
271
257
  const writeFileCommand = (path: string, lines: readonly string[]): string => {
@@ -275,39 +261,6 @@ const writeFileCommand = (path: string, lines: readonly string[]): string => {
275
261
  return `printf '%s\\n' ${lines.map((line) => `'${line}'`).join(' ')} > ${path}`
276
262
  }
277
263
 
278
- const migrateScript = (appId: string): readonly string[] => [
279
- '#!/bin/sh',
280
- 'set -eu',
281
- `cd ${RUNTIME_ROOT}`,
282
- `FABRIKA_APP_ID=${appId} exec ${FABRIKA_BIN} db migrate`,
283
- ]
284
-
285
- // The committed schema types must match the migrated schema and every generated isolation test must pass on a throwaway
286
- // schema. The build has no database, so they run at container start, after the migrations, as init commands: a failed
287
- // init command stops the activation, and the readiness window opens only with `run.start`, after the init commands (both
288
- // verified live), so a run as long as the CRM's ~3 minutes of generated cases does not race it.
289
- const verifyScript = (appId: string): readonly string[] => [
290
- '#!/bin/sh',
291
- 'set -eu',
292
- `cd ${RUNTIME_ROOT}`,
293
- `FABRIKA_APP_ID=${appId} ${FABRIKA_BIN} db codegen --verify`,
294
- `FABRIKA_APP_ID=${appId} exec ${FABRIKA_BIN} db test-isolation`,
295
- ]
296
-
297
- const startScript: readonly string[] = ['#!/bin/sh', 'set -eu', `cd ${RUNTIME_ROOT}`, 'exec bun fabrika/entry.generated.ts']
298
-
299
- /** The scripts the build writes and every container's init commands run, in this order. */
300
- const initScripts = (execution: ZeropsLockedExecution): readonly { readonly path: string; readonly lines: readonly string[] }[] =>
301
- execution.databaseUrl === null
302
- ? []
303
- : [{ path: MIGRATE_SCRIPT, lines: migrateScript(execution.appId) }, { path: VERIFY_SCRIPT, lines: verifyScript(execution.appId) }]
304
-
305
- const INSTALL = 'bun install --frozen-lockfile --ignore-scripts'
306
-
307
- // After every check: a map is uploaded only for a build that can deploy. A failed upload never fails the build; the
308
- // provider reads the state from the report line.
309
- const sourceMapUpload = (release: string): string => `cd /tmp && bun x --bun -p @fabrika/check@${release} fabrika-source-maps ${SOURCE_ROOT} || true`
310
-
311
264
  /**
312
265
  * The upload's build variables: the destination and asset roots as a literal, and the bearer lifted from the service
313
266
  * variable the deploy wrote sensitive, so the descriptor never holds it.
@@ -319,26 +272,81 @@ const sourceMapBuildVariables = (upload: OperationsSourceMapBuildUpload): Record
319
272
  return { [FABRIKA_SOURCE_MAP_UPLOAD]: value, [FABRIKA_SOURCE_MAP_UPLOAD_KEY]: `\${RUNTIME_${FABRIKA_SOURCE_MAP_UPLOAD_KEY}}` }
320
273
  }
321
274
 
322
- /**
323
- * The generated build commands, in order; the authored SPA commands run before all of them. The application's tests
324
- * run first: they are authored code, so everything after them is reinstalled, regenerated or checked again. They run
325
- * `--isolate`, as the application skill runs them: a test file's DOM globals otherwise leak into the next file.
326
- */
327
- export function generatedBuildCommands(execution: ZeropsLockedExecution): readonly string[] {
328
- if (!LOCKED_APP_ID_PATTERN.test(execution.appId)) throw new Error('zerops: a contract application id must match [a-z0-9][a-z0-9-]*')
329
- if (!FRAMEWORK_RELEASE_PATTERN.test(execution.checkRelease)) throw new Error('zerops: the checker release must be a release version')
330
- return [
331
- INSTALL,
332
- 'bun test --isolate --pass-with-no-tests',
333
- `rm -rf node_modules && ${INSTALL}`,
334
- `bun -e "await import('@fabrika/app/build').then((build) => build.writeGeneratedEntry('.', 'bun'))"`,
335
- `cd /tmp && bun x --bun -p @fabrika/check@${execution.checkRelease} fabrika-check ${SOURCE_ROOT}`,
336
- writeFileCommand(PLATFORM_TSCONFIG, [PLATFORM_TSCONFIG_CONTENTS]),
337
- `cd /tmp && bun x --bun -p typescript@${PLATFORM_TYPESCRIPT_VERSION} tsc -p ${SOURCE_ROOT}/${PLATFORM_TSCONFIG}`,
338
- ...initScripts(execution).map((script) => writeFileCommand(script.path, script.lines)),
339
- writeFileCommand(START_SCRIPT, startScript),
340
- ...(execution.sourceMaps === null ? [] : [sourceMapUpload(execution.checkRelease)]),
341
- ]
275
+ // The readiness check is retried no sooner than every 10 s (the platform's floor), and a server is rarely listening at
276
+ // the first attempt, one second after `run.start`. So one attempt waits for the health endpoint itself, for less than
277
+ // the retry period; it passes only on a 2xx, like the `httpGet` it replaces. A dev container also passes once its launcher
278
+ // reported the boot, served or failed: the launcher keeps running and reports a failed migration with its remedy.
279
+ const readinessCommand = (path: string, dev: boolean): string => {
280
+ if (!READINESS_PATH_PATTERN.test(path)) throw new Error('zerops: a readiness path must be a literal path of unreserved characters')
281
+ const url = `http://127.0.0.1:${ZEROPS_APPLICATION_PORT}${path}`
282
+ const booted = dev ? `if (await Bun.file('${RUNTIME_ROOT}/${DEV_LAUNCHER_BOOTED_FILE}').exists()) process.exit(0); ` : ''
283
+ return `bun -e "const end = Date.now() + 9000; while (Date.now() < end) { ${booted}try { if ((await fetch('${url}', { signal: AbortSignal.timeout(2000) })).ok) process.exit(0) } catch {} await Bun.sleep(200) } process.exit(1)"`
284
+ }
285
+
286
+ /** `@fabrika/app/build`'s recipe at this installation's paths; the dev launcher checks in the runtime root. */
287
+ const recipeOf = (execution: ZeropsLockedExecution, sourceRoot: string = SOURCE_ROOT): ContractRecipe =>
288
+ contractRecipe({
289
+ appId: execution.appId,
290
+ checkRelease: execution.checkRelease,
291
+ sourceRoot,
292
+ runtimeRoot: RUNTIME_ROOT,
293
+ // The build has no database; a container's init commands run the migrations and schema checks, and the readiness
294
+ // window opens only with `run.start`, after them (both verified live), so the CRM's ~3 minutes does not race it.
295
+ // A dev container has none: its init would run before Control says whether a reset is pending, over the schema the
296
+ // previous container left, so its launcher migrates and verifies after that answer.
297
+ database: execution.databaseUrl !== null && execution.dev === undefined,
298
+ sourceMaps: execution.sourceMaps !== null,
299
+ jobs: execution.contract.jobs,
300
+ files: execution.contract.files === true,
301
+ scan: execution.contract.scan === true,
302
+ })
303
+
304
+ // A dev environment starts the launcher of the application's own framework release, which runs the serving entry as its
305
+ // child. Double quotes inside: a generated file line may hold no single quote.
306
+ const devStartScript: readonly string[] = [
307
+ '#!/bin/sh',
308
+ 'set -eu',
309
+ `cd ${RUNTIME_ROOT}`,
310
+ `exec bun -e "await import(\\"${DEV_LAUNCHER_MODULE}\\").then((dev) => dev.runDevLauncher(process.env))"`,
311
+ ]
312
+
313
+ /** One command writing `content` to `path` through base64: the content may hold quotes and expansions, the command none. */
314
+ const writeBase64Command = (path: string, content: string): string => {
315
+ const encoded = btoa(String.fromCharCode(...new TextEncoder().encode(content)))
316
+ return `printf '%s' '${encoded}' | base64 -d > ${path}`
317
+ }
318
+
319
+ /** What the dev launcher reads (`DEV_LAUNCHER_CONFIG_FILE`): the platform's, written after the checks. */
320
+ export function devLauncherConfig(execution: ZeropsLockedExecution, authoredSpa: readonly string[]): DevLauncherConfig {
321
+ const dev = execution.dev
322
+ if (dev === undefined) throw new Error('zerops: a dev launcher configuration needs a dev environment')
323
+ if (!/^[0-9a-f]{40}$/.test(dev.head)) throw new Error('zerops: a dev environment builds an exact commit')
324
+ const recipe = recipeOf(execution)
325
+ return {
326
+ format: 1,
327
+ appId: execution.appId,
328
+ head: dev.head,
329
+ port: ZEROPS_APPLICATION_PORT,
330
+ healthPath: execution.contract.readinessPath,
331
+ serve: recipe.serve,
332
+ migrate: recipe.migrate,
333
+ spaBuild: authoredSpa,
334
+ // The build's check chain, in its order, on what the container holds.
335
+ checks: recipeOf(execution, RUNTIME_ROOT).checks,
336
+ }
337
+ }
338
+
339
+ /** The generated build commands, in order; the authored SPA commands run before all of them. */
340
+ export function generatedBuildCommands(execution: ZeropsLockedExecution, authoredSpa: readonly string[] = []): readonly string[] {
341
+ const recipe = recipeOf(execution)
342
+ return recipe.build.flatMap((step) => {
343
+ if ('run' in step) return [step.run]
344
+ if (execution.dev === undefined || step.write !== recipe.start) return [writeFileCommand(step.write, step.lines)]
345
+ return [
346
+ writeFileCommand(recipe.start, devStartScript),
347
+ writeBase64Command(DEV_LAUNCHER_CONFIG_FILE, JSON.stringify(devLauncherConfig(execution, authoredSpa))),
348
+ ]
349
+ })
342
350
  }
343
351
 
344
352
  const sortedUnique = (values: readonly string[]): string[] => [...new Set(values)].sort()
@@ -354,16 +362,11 @@ const storageVariables = (hostname: string): Record<StorageVariable, string> =>
354
362
  })
355
363
 
356
364
  /** The crontab lines that run a job of the contract or of the framework, each on one container. */
357
- const jobCrontab = (contract: ZeropsContractRun): { readonly timing: string; readonly command: string; readonly allContainers: boolean }[] => [
358
- ...contract.jobs.map((job) => ({ timing: job.schedule, command: `${FABRIKA_BIN} job run ${job.name}`, allContainers: false })),
359
- ...(contract.files === true
360
- ? [{ timing: FILE_SWEEP_SCHEDULE, command: `${FABRIKA_BIN} job run ${ZEROPS_FILE_SWEEP_JOB}`, allContainers: false }]
361
- : []),
362
- ...(contract.scan === true ? [{ timing: '* * * * *', command: `${FABRIKA_BIN} job run ${ZEROPS_FILE_SCAN_JOB}`, allContainers: false }] : []),
363
- ]
365
+ const jobCrontab = (execution: ZeropsLockedExecution): { readonly timing: string; readonly command: string; readonly allContainers: boolean }[] =>
366
+ recipeOf(execution).jobs.map((job) => ({ timing: job.schedule, command: job.command, allContainers: false }))
364
367
 
365
368
  /** How many job processes the crontab may run at once, each opening the application's database. */
366
- export const zeropsJobProcesses = (contract: ZeropsContractRun): number => jobCrontab(contract).length
369
+ export const zeropsJobProcesses = (execution: ZeropsLockedExecution): number => jobCrontab(execution).length
367
370
 
368
371
  /** The commands every container runs before `run.start`, in order; any failure stops the activation. */
369
372
  export function generatedInitCommands(execution: ZeropsLockedExecution): string[] {
@@ -371,7 +374,7 @@ export function generatedInitCommands(execution: ZeropsLockedExecution): string[
371
374
  // A container starts from the prepared image, whose signatures are as old as its build; a failed refresh leaves
372
375
  // them to the daily one, and the scanner refuses signatures older than three days, so the scan fails closed.
373
376
  ...(execution.contract.scan === true ? [`${FRESHCLAM} || echo 'fabrika: freshclam did not refresh the malware signatures'`] : []),
374
- ...initScripts(execution).map((script) => `sh ${RUNTIME_ROOT}/${script.path}`),
377
+ ...recipeOf(execution).init.map((path) => `sh ${RUNTIME_ROOT}/${path}`),
375
378
  ]
376
379
  }
377
380
 
@@ -396,7 +399,7 @@ export function renderLockedSetup(
396
399
  const scan = contract.scan === true
397
400
  const packages = sortedUnique([...execution.runtimePackages, ...(scan ? [SCANNER_PACKAGE] : [])])
398
401
  // Every container keeps its own signatures; one scan pass at a time holds the job's lease.
399
- const crontab = [...jobCrontab(contract), ...(scan ? [{ timing: FRESHCLAM_SCHEDULE, command: FRESHCLAM, allContainers: true }] : [])]
402
+ const crontab = [...jobCrontab(execution), ...(scan ? [{ timing: FRESHCLAM_SCHEDULE, command: FRESHCLAM, allContainers: true }] : [])]
400
403
  const initCommands = generatedInitCommands(execution)
401
404
  const prepareCommands = packages.length === 0 ? [] : [`sudo apk add --no-cache ${packages.join(' ')}`, ...(scan ? [FRESHCLAM] : [])]
402
405
  return {
@@ -404,21 +407,29 @@ export function renderLockedSetup(
404
407
  build: {
405
408
  base: authored.base ?? [ZEROPS_APPLICATION_RUNTIME],
406
409
  ...(authored.os === null ? {} : { os: authored.os }),
407
- buildCommands: [...authored.commands, ...generatedBuildCommands(execution)],
410
+ buildCommands: [...authored.commands, ...generatedBuildCommands(execution, authored.commands)],
408
411
  ...(execution.sourceMaps === null ? {} : { envVariables: sourceMapBuildVariables(execution.sourceMaps) }),
409
412
  deployFiles: ['./'],
410
413
  cache: ['node_modules'],
411
414
  },
412
415
  deploy: {
413
- readinessCheck: { httpGet: probe, retryPeriod: '10s', failureTimeout: '180s' },
416
+ readinessCheck: {
417
+ exec: { command: readinessCommand(contract.readinessPath, execution.dev !== undefined) },
418
+ retryPeriod: '10s',
419
+ failureTimeout: '180s',
420
+ },
414
421
  },
415
422
  run: {
416
423
  base: ZEROPS_APPLICATION_RUNTIME,
417
424
  ...(prepareCommands.length === 0 ? {} : { prepareCommands }),
418
425
  ...(initCommands.length === 0 ? {} : { initCommands }),
419
- start: `sh ${RUNTIME_ROOT}/${START_SCRIPT}`,
426
+ start: `sh ${RUNTIME_ROOT}/${recipeOf(execution).start}`,
420
427
  ports: [{ port: ZEROPS_APPLICATION_PORT, httpSupport: true }],
421
- healthCheck: { httpGet: probe, execPeriod: '10s', disconnectTimeout: '30s', recoveryTimeout: '10s', failureTimeout: '120s' },
428
+ // A dev container keeps its launcher running while the application it respawns fails: Zerops restarting it
429
+ // would only boot the same failing head again.
430
+ ...(execution.dev === undefined
431
+ ? { healthCheck: { httpGet: probe, execPeriod: '10s', disconnectTimeout: '30s', recoveryTimeout: '10s', failureTimeout: '120s' } }
432
+ : {}),
422
433
  ...(crontab.length === 0 ? {} : { crontab }),
423
434
  envVariables,
424
435
  },
package/src/index.ts CHANGED
@@ -18,6 +18,8 @@ export type {
18
18
  ZeropsAppVersion,
19
19
  ZeropsAppVersionStatus,
20
20
  ZeropsAppVersionUpload,
21
+ ZeropsBackupConfig,
22
+ ZeropsBackupRetentionPolicy,
21
23
  ZeropsImportedService,
22
24
  ZeropsImportResult,
23
25
  ZeropsIntegrationToken,
@@ -15,10 +15,10 @@ import {
15
15
  ensureFirstAdministrator,
16
16
  type FirstAdministratorApi,
17
17
  type FirstAdministratorResult,
18
+ platformOrigin,
18
19
  } from '@fabrika/installation-init'
19
20
  import type { PlatformAdminInput } from './admin-options'
20
21
  import { REISSUE_FLAG } from './admin-options'
21
- import { platformOrigin } from './hosts'
22
22
  import { MACHINE_KEY_NEXT_STEP_TITLE, machineKeyNextStepLines } from './machine-key'
23
23
 
24
24
  /**
@@ -12,8 +12,7 @@
12
12
  // The command is unattended by construction: nothing here prompts, and a missing required value is an
13
13
  // error naming both the flag and the variable that would supply it.
14
14
 
15
- import { describeArgument, readEnv, requiredSetting, setting } from '@fabrika/installation-init'
16
- import { type McpClient, parseMcpClientsJson } from './mcp'
15
+ import { describeArgument, type McpClient, parseMcpClientsJson, readEnv, requiredSetting, setting } from '@fabrika/installation-init'
17
16
  import { parsePlatformHosts, parsePlatformScheme } from './placement-options'
18
17
  import { readInstallationSourceDir } from './source-options'
19
18