@fabrika/provider-zerops 0.7.0 → 0.8.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.
- package/package.json +9 -9
- package/src/control.ts +7 -3
- package/src/database-budget.ts +2 -2
- package/src/execution.ts +30 -23
- package/src/index.ts +2 -2
- package/src/namespace/proxy-reads.ts +25 -14
- package/src/plan.ts +1 -1
- package/src/provider.ts +23 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fabrika/provider-zerops",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -43,14 +43,14 @@
|
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"zod": "^3.25.76",
|
|
46
|
-
"@fabrika/auth": "0.
|
|
47
|
-
"@fabrika/auth-core": "0.
|
|
48
|
-
"@fabrika/email": "0.
|
|
49
|
-
"@fabrika/iam-contract": "0.
|
|
50
|
-
"@fabrika/installation-init": "0.
|
|
51
|
-
"@fabrika/operations-contract": "0.
|
|
52
|
-
"@fabrika/provider-contract": "0.
|
|
53
|
-
"@fabrika/proxy-core": "0.
|
|
46
|
+
"@fabrika/auth": "0.8.1",
|
|
47
|
+
"@fabrika/auth-core": "0.8.1",
|
|
48
|
+
"@fabrika/email": "0.8.1",
|
|
49
|
+
"@fabrika/iam-contract": "0.8.1",
|
|
50
|
+
"@fabrika/installation-init": "0.8.1",
|
|
51
|
+
"@fabrika/operations-contract": "0.8.1",
|
|
52
|
+
"@fabrika/provider-contract": "0.8.1",
|
|
53
|
+
"@fabrika/proxy-core": "0.8.1",
|
|
54
54
|
"yaml": "^2.9.1"
|
|
55
55
|
}
|
|
56
56
|
}
|
package/src/control.ts
CHANGED
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
type ZeropsNamespaceTarget,
|
|
49
49
|
zeropsNamespaceTargetCodec,
|
|
50
50
|
} from './namespace'
|
|
51
|
-
import { assertNamespaceProxyReads, type
|
|
51
|
+
import { assertNamespaceProxyReads, type OlderProxyReaders } from './namespace/proxy-reads'
|
|
52
52
|
import { createZeropsDeployer, ZEROPS_VERSION_DEADLINE_MS } from './provider'
|
|
53
53
|
import { allowedReferenceHosts, assertNoPlatformOnlyReference, foreignReferenceHost, serviceReferenceHosts } from './service-references'
|
|
54
54
|
import { normalizeZeropsSourceRepository } from './source'
|
|
@@ -102,8 +102,8 @@ export interface ZeropsControlProviderOptions {
|
|
|
102
102
|
readonly proxySource: ZeropsNamespaceProxySource
|
|
103
103
|
readonly iamUrl: string
|
|
104
104
|
readonly iamKey: string
|
|
105
|
-
/** The
|
|
106
|
-
readonly olderProxies:
|
|
105
|
+
/** The manifest parsers of the older releases' proxies, which a namespace pinned to an older source may run. */
|
|
106
|
+
readonly olderProxies: OlderProxyReaders
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
@@ -340,6 +340,10 @@ export const createZeropsControlProvider = (options: ZeropsControlProviderOption
|
|
|
340
340
|
}
|
|
341
341
|
},
|
|
342
342
|
resolveSource,
|
|
343
|
+
validateAccess: ({ environment, access }) => {
|
|
344
|
+
const placement = resolvedEnvironment(environment, { requireReady: true })
|
|
345
|
+
assertNamespaceProxyReads(placement.namespace.id, access, placement.namespaceTarget.proxySource, options.namespaces)
|
|
346
|
+
},
|
|
343
347
|
deploy: async (input: ProviderDeployInput): Promise<ProviderDeployOutcome> => {
|
|
344
348
|
input.signal.throwIfAborted()
|
|
345
349
|
assertAuthoredManagedInputs({ variableNames: Object.keys(input.vars), secretNames: Object.keys(input.secrets) })
|
package/src/database-budget.ts
CHANGED
|
@@ -15,7 +15,7 @@ export const ZEROPS_POSTGRES_MIN_RAM_GB = 0.25
|
|
|
15
15
|
export const ZEROPS_POSTGRES_UNAVAILABLE_CONNECTIONS = 7
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* The most connections one container's
|
|
18
|
+
* The most connections one container's init commands hold at once: `fabrika db test-isolation` peaks at 4 since the
|
|
19
19
|
* test kit opens a file's connections together, and at 5 with a `@fabrika/db` before that (measured locally,
|
|
20
20
|
* 2026-10-16, with the Notes and CRM examples). Migrating and reading the schema take fewer, one after the other.
|
|
21
21
|
*/
|
|
@@ -53,7 +53,7 @@ export function zeropsPostgresMaxConnections(ramGB: number, profile: string | un
|
|
|
53
53
|
const poolHolders = (demand: ZeropsDatabaseDemand): number => 2 * demand.maxContainers + demand.jobs
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
|
-
* The connections each process may open so that all of them, with one container's
|
|
56
|
+
* The connections each process may open so that all of them, with one container's init commands, stay within what the
|
|
57
57
|
* application's roles can take; `null` when that is below `MIN_PROCESS_CONNECTIONS`.
|
|
58
58
|
*/
|
|
59
59
|
export function zeropsProcessConnections(maxConnections: number, demand: ZeropsDatabaseDemand): number | null {
|
package/src/execution.ts
CHANGED
|
@@ -55,7 +55,7 @@ const FILE_SWEEP_SCHEDULE = '41 * * * *'
|
|
|
55
55
|
export const ZEROPS_SCAN_MIN_RAM_GB = 2
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
|
-
* The RAM in GB a container of an application with a database starts with. Its
|
|
58
|
+
* The RAM in GB a container of an application with a database starts with. Its init commands run `fabrika db
|
|
59
59
|
* test-isolation`, whose CLI and `bun test` child together peak at 161 MiB of anonymous memory for the Notes example
|
|
60
60
|
* and 225 MiB for the CRM example (measured locally, 2026-10-15), beside Bun's own mapped binary. Zerops' 0.125 GB
|
|
61
61
|
* floor kills that within a second, faster than vertical autoscaling reacts.
|
|
@@ -243,6 +243,7 @@ const SOURCE_ROOT = '/build/source'
|
|
|
243
243
|
const RUNTIME_ROOT = '/var/www'
|
|
244
244
|
const FABRIKA_BIN = './node_modules/.bin/fabrika'
|
|
245
245
|
const MIGRATE_SCRIPT = 'fabrika/migrate.generated.sh'
|
|
246
|
+
const VERIFY_SCRIPT = 'fabrika/verify.generated.sh'
|
|
246
247
|
const START_SCRIPT = 'fabrika/start.generated.sh'
|
|
247
248
|
const PLATFORM_TSCONFIG = 'fabrika/tsconfig.platform.json'
|
|
248
249
|
|
|
@@ -281,24 +282,26 @@ const migrateScript = (appId: string): readonly string[] => [
|
|
|
281
282
|
`FABRIKA_APP_ID=${appId} exec ${FABRIKA_BIN} db migrate`,
|
|
282
283
|
]
|
|
283
284
|
|
|
284
|
-
// The
|
|
285
|
-
// schema
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
const
|
|
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[] => [
|
|
289
290
|
'#!/bin/sh',
|
|
290
291
|
'set -eu',
|
|
291
292
|
`cd ${RUNTIME_ROOT}`,
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
`sh ${RUNTIME_ROOT}/${MIGRATE_SCRIPT}`,
|
|
295
|
-
`FABRIKA_APP_ID=${appId} ${FABRIKA_BIN} db codegen --verify`,
|
|
296
|
-
`FABRIKA_APP_ID=${appId} ${FABRIKA_BIN} db test-isolation`,
|
|
297
|
-
]
|
|
298
|
-
: []),
|
|
299
|
-
'exec bun fabrika/entry.generated.ts',
|
|
293
|
+
`FABRIKA_APP_ID=${appId} ${FABRIKA_BIN} db codegen --verify`,
|
|
294
|
+
`FABRIKA_APP_ID=${appId} exec ${FABRIKA_BIN} db test-isolation`,
|
|
300
295
|
]
|
|
301
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
|
+
|
|
302
305
|
const INSTALL = 'bun install --frozen-lockfile --ignore-scripts'
|
|
303
306
|
|
|
304
307
|
// After every check: a map is uploaded only for a build that can deploy. A failed upload never fails the build; the
|
|
@@ -324,7 +327,6 @@ const sourceMapBuildVariables = (upload: OperationsSourceMapBuildUpload): Record
|
|
|
324
327
|
export function generatedBuildCommands(execution: ZeropsLockedExecution): readonly string[] {
|
|
325
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-]*')
|
|
326
329
|
if (!FRAMEWORK_RELEASE_PATTERN.test(execution.checkRelease)) throw new Error('zerops: the checker release must be a release version')
|
|
327
|
-
const database = execution.databaseUrl !== null
|
|
328
330
|
return [
|
|
329
331
|
INSTALL,
|
|
330
332
|
'bun test --isolate --pass-with-no-tests',
|
|
@@ -333,8 +335,8 @@ export function generatedBuildCommands(execution: ZeropsLockedExecution): readon
|
|
|
333
335
|
`cd /tmp && bun x --bun -p @fabrika/check@${execution.checkRelease} fabrika-check ${SOURCE_ROOT}`,
|
|
334
336
|
writeFileCommand(PLATFORM_TSCONFIG, [PLATFORM_TSCONFIG_CONTENTS]),
|
|
335
337
|
`cd /tmp && bun x --bun -p typescript@${PLATFORM_TYPESCRIPT_VERSION} tsc -p ${SOURCE_ROOT}/${PLATFORM_TSCONFIG}`,
|
|
336
|
-
...(
|
|
337
|
-
writeFileCommand(START_SCRIPT, startScript
|
|
338
|
+
...initScripts(execution).map((script) => writeFileCommand(script.path, script.lines)),
|
|
339
|
+
writeFileCommand(START_SCRIPT, startScript),
|
|
338
340
|
...(execution.sourceMaps === null ? [] : [sourceMapUpload(execution.checkRelease)]),
|
|
339
341
|
]
|
|
340
342
|
}
|
|
@@ -363,6 +365,16 @@ const jobCrontab = (contract: ZeropsContractRun): { readonly timing: string; rea
|
|
|
363
365
|
/** How many job processes the crontab may run at once, each opening the application's database. */
|
|
364
366
|
export const zeropsJobProcesses = (contract: ZeropsContractRun): number => jobCrontab(contract).length
|
|
365
367
|
|
|
368
|
+
/** The commands every container runs before `run.start`, in order; any failure stops the activation. */
|
|
369
|
+
export function generatedInitCommands(execution: ZeropsLockedExecution): string[] {
|
|
370
|
+
return [
|
|
371
|
+
// A container starts from the prepared image, whose signatures are as old as its build; a failed refresh leaves
|
|
372
|
+
// them to the daily one, and the scanner refuses signatures older than three days, so the scan fails closed.
|
|
373
|
+
...(execution.contract.scan === true ? [`${FRESHCLAM} || echo 'fabrika: freshclam did not refresh the malware signatures'`] : []),
|
|
374
|
+
...initScripts(execution).map((script) => `sh ${RUNTIME_ROOT}/${script.path}`),
|
|
375
|
+
]
|
|
376
|
+
}
|
|
377
|
+
|
|
366
378
|
/** The generated selected setup: the authored build kept, everything else the platform's. */
|
|
367
379
|
export function renderLockedSetup(
|
|
368
380
|
setup: string,
|
|
@@ -385,12 +397,7 @@ export function renderLockedSetup(
|
|
|
385
397
|
const packages = sortedUnique([...execution.runtimePackages, ...(scan ? [SCANNER_PACKAGE] : [])])
|
|
386
398
|
// Every container keeps its own signatures; one scan pass at a time holds the job's lease.
|
|
387
399
|
const crontab = [...jobCrontab(contract), ...(scan ? [{ timing: FRESHCLAM_SCHEDULE, command: FRESHCLAM, allContainers: true }] : [])]
|
|
388
|
-
|
|
389
|
-
// them to the daily one, and the scanner refuses signatures older than three days, so the scan fails closed.
|
|
390
|
-
const initCommands = [
|
|
391
|
-
...(scan ? [`${FRESHCLAM} || echo 'fabrika: freshclam did not refresh the malware signatures'`] : []),
|
|
392
|
-
...(execution.databaseUrl === null ? [] : [`sh ${RUNTIME_ROOT}/${MIGRATE_SCRIPT}`]),
|
|
393
|
-
]
|
|
400
|
+
const initCommands = generatedInitCommands(execution)
|
|
394
401
|
const prepareCommands = packages.length === 0 ? [] : [`sudo apk add --no-cache ${packages.join(' ')}`, ...(scan ? [FRESHCLAM] : [])]
|
|
395
402
|
return {
|
|
396
403
|
setup,
|
package/src/index.ts
CHANGED
|
@@ -101,8 +101,8 @@ export type {
|
|
|
101
101
|
ZeropsProxyRepublishInput,
|
|
102
102
|
} from './namespace'
|
|
103
103
|
export { postgresSchema as zeropsNamespacePostgresSchema } from './namespace/codec'
|
|
104
|
-
export {
|
|
105
|
-
export type {
|
|
104
|
+
export { entriesNeedingCurrentProxy } from './namespace/proxy-reads'
|
|
105
|
+
export type { OlderProxyReader, OlderProxyReaders } from './namespace/proxy-reads'
|
|
106
106
|
export { buildZeropsPlan, resolveDeployHostname } from './plan'
|
|
107
107
|
export type { ZeropsStep, ZeropsStepKind } from './plan'
|
|
108
108
|
export { CANCELLED, createZeropsDeployer, interpolateManifest } from './provider'
|
|
@@ -1,35 +1,46 @@
|
|
|
1
1
|
// What a namespace proxy pinned to an older source cannot read. Its manifest parser is strict: one row of a form it does
|
|
2
|
-
// not know
|
|
3
|
-
// deploy that would publish
|
|
2
|
+
// not know, or a CSP directive it does not know, refuses the whole manifest and blocks the namespace's next republish.
|
|
3
|
+
// So a deploy that would publish either is refused before publication while the namespace's proxy source is not
|
|
4
|
+
// this control plane's own.
|
|
4
5
|
|
|
5
6
|
import type { ProviderApprovedAccess } from '@fabrika/provider-contract'
|
|
7
|
+
import type { CspOrigins } from '@fabrika/proxy-core/headers'
|
|
6
8
|
import { contractProxyTable, type ProxyEndpointRow } from '@fabrika/proxy-core/manifest'
|
|
7
9
|
import type { ZeropsNamespaceProxySource } from './types'
|
|
8
10
|
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
+
/**
|
|
12
|
+
* What the namespace proxy of one older platform release parses of an application's manifest entry: a row, and the
|
|
13
|
+
* entry's CSP directives, each by that release's own parser.
|
|
14
|
+
*/
|
|
15
|
+
export interface OlderProxyReader {
|
|
16
|
+
readonly row: (row: ProxyEndpointRow) => boolean
|
|
17
|
+
readonly csp: (csp: CspOrigins) => boolean
|
|
18
|
+
}
|
|
11
19
|
|
|
12
20
|
/**
|
|
13
21
|
* The namespace proxies of the older releases this platform serves (ADR-0062). Control holds their parsers, as it holds
|
|
14
|
-
* their contract readers; a pin's release is not recorded, so
|
|
22
|
+
* their contract readers; a pin's release is not recorded, so what any of them reads is what the pin may read.
|
|
15
23
|
*/
|
|
16
|
-
export type
|
|
24
|
+
export type OlderProxyReaders = readonly [OlderProxyReader, ...OlderProxyReader[]]
|
|
17
25
|
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
/**
|
|
27
|
+
* What of the approved contract's manifest entry no older release's proxy parses, as the namespace publishes it: each
|
|
28
|
+
* such row by its id, and `csp` when no older proxy parses the entry's CSP (`workerSrc`, which only this release knows).
|
|
29
|
+
*/
|
|
30
|
+
export function entriesNeedingCurrentProxy(access: ProviderApprovedAccess, olderProxies: OlderProxyReaders): string[] {
|
|
31
|
+
const table = contractProxyTable(access.contract, access.contractDigest)
|
|
32
|
+
const rows = table.endpoints.filter((row) => !olderProxies.some((proxy) => proxy.row(row))).map((row) => row.id)
|
|
33
|
+
return olderProxies.some((proxy) => proxy.csp(table.csp)) ? rows : [...rows, 'csp']
|
|
23
34
|
}
|
|
24
35
|
|
|
25
36
|
/** This control plane's own proxy source, and the parsers of the older releases a pinned proxy may run. */
|
|
26
37
|
export interface PlatformProxies {
|
|
27
38
|
readonly proxySource: ZeropsNamespaceProxySource
|
|
28
|
-
readonly olderProxies:
|
|
39
|
+
readonly olderProxies: OlderProxyReaders
|
|
29
40
|
}
|
|
30
41
|
|
|
31
42
|
/**
|
|
32
|
-
* Refuses a deploy whose approved contract holds a row the namespace's pinned proxy may not read: its source is not the
|
|
43
|
+
* Refuses a deploy whose approved contract holds a row or a CSP directive the namespace's pinned proxy may not read: its source is not the
|
|
33
44
|
* one this control plane deploys new placements from. `platform` is absent where this control plane manages no
|
|
34
45
|
* namespace proxy, which then has nothing to compare.
|
|
35
46
|
*/
|
|
@@ -42,7 +53,7 @@ export function assertNamespaceProxyReads(
|
|
|
42
53
|
if (access === undefined || platform === undefined || pinned.commitSha === platform.proxySource.commitSha) {
|
|
43
54
|
return
|
|
44
55
|
}
|
|
45
|
-
const rows =
|
|
56
|
+
const rows = entriesNeedingCurrentProxy(access, platform.olderProxies)
|
|
46
57
|
if (rows.length === 0) {
|
|
47
58
|
return
|
|
48
59
|
}
|
package/src/plan.ts
CHANGED
|
@@ -48,7 +48,7 @@ export const buildZeropsPlan = (manifest: FabrikaManifest, target: ZeropsRuntime
|
|
|
48
48
|
steps.push({
|
|
49
49
|
id: 'raise-start-memory',
|
|
50
50
|
kind: 'raise-start-memory',
|
|
51
|
-
description: "Raise the deploy service's minimum RAM to what its
|
|
51
|
+
description: "Raise the deploy service's minimum RAM to what its init commands need",
|
|
52
52
|
})
|
|
53
53
|
steps.push({
|
|
54
54
|
id: 'size-database',
|
package/src/provider.ts
CHANGED
|
@@ -280,14 +280,27 @@ export function interpolateManifest(
|
|
|
280
280
|
}
|
|
281
281
|
|
|
282
282
|
/** Human-readable failure for a version that finished in a non-`ACTIVE` state. */
|
|
283
|
-
const terminalFailure = (version: ZeropsAppVersion): string => {
|
|
283
|
+
const terminalFailure = (version: ZeropsAppVersion, checks: boolean): string => {
|
|
284
284
|
if (version.status === 'BACKUP') {
|
|
285
285
|
return `app-version ${version.id} was superseded by a newer version before it went live`
|
|
286
286
|
}
|
|
287
287
|
if (version.status === 'CANCELLED') {
|
|
288
288
|
return `app-version ${version.id} was cancelled`
|
|
289
289
|
}
|
|
290
|
-
return `Zerops pipeline failed: app-version ${version.id} is ${version.status ?? 'in an unknown state'}`
|
|
290
|
+
return `Zerops pipeline failed: app-version ${version.id} is ${version.status ?? 'in an unknown state'}${activationHint(version, checks)}`
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* What a version that built but did not activate tells the operator. Zerops' own log of it is the refused readiness
|
|
295
|
+
* attempts, which read as a server that never started whatever stopped it. `checks`: the generated init commands run
|
|
296
|
+
* the migrations, the schema check and the isolation tests, as for a contract application with a database.
|
|
297
|
+
*/
|
|
298
|
+
const activationHint = (version: ZeropsAppVersion, checks: boolean): string => {
|
|
299
|
+
if (version.status !== 'DEPLOY_FAILED') return ''
|
|
300
|
+
const failed = ': an init command failed or the readiness check never answered'
|
|
301
|
+
return checks
|
|
302
|
+
? `${failed}. The init commands run the migrations, the schema check and the generated isolation tests; the runtime lines above show how far they got`
|
|
303
|
+
: failed
|
|
291
304
|
}
|
|
292
305
|
|
|
293
306
|
/**
|
|
@@ -415,7 +428,7 @@ const openLog = async (env: StepEnv): Promise<ZeropsLogAccess | null> => {
|
|
|
415
428
|
* poll loop without telling it leaves a build container burning through the pipeline's hour. A lost lease
|
|
416
429
|
* gives no such permission (ADR-0044), so it stops without touching the build.
|
|
417
430
|
*/
|
|
418
|
-
const awaitVersion = async (env: StepEnv, appVersionId: string, processId: string | undefined): Promise<void> => {
|
|
431
|
+
const awaitVersion = async (env: StepEnv, appVersionId: string, processId: string | undefined, checks: boolean): Promise<void> => {
|
|
419
432
|
const { zerops, signal, log } = env
|
|
420
433
|
const access = await openLog(env)
|
|
421
434
|
const seen = new Set<string>()
|
|
@@ -461,14 +474,16 @@ const awaitVersion = async (env: StepEnv, appVersionId: string, processId: strin
|
|
|
461
474
|
|
|
462
475
|
if (process?.status !== undefined && ZEROPS_PROCESS_TERMINAL.has(process.status) && process.status !== ZEROPS_PROCESS_FINISHED) {
|
|
463
476
|
throw new Error(
|
|
464
|
-
`zerops: pipeline process ${process.id} is ${process.status} while app-version ${appVersionId} is ${version.status ?? 'in an unknown state'}
|
|
477
|
+
`zerops: pipeline process ${process.id} is ${process.status} while app-version ${appVersionId} is ${version.status ?? 'in an unknown state'}${
|
|
478
|
+
activationHint(version, checks)
|
|
479
|
+
}`,
|
|
465
480
|
)
|
|
466
481
|
}
|
|
467
482
|
if (version.status !== undefined && ZEROPS_TERMINAL.has(version.status)) {
|
|
468
483
|
if (version.status === ZEROPS_ACTIVE) {
|
|
469
484
|
return
|
|
470
485
|
}
|
|
471
|
-
throw new Error(terminalFailure(version))
|
|
486
|
+
throw new Error(terminalFailure(version, checks))
|
|
472
487
|
}
|
|
473
488
|
if (Date.now() > env.deadlineMs) await timedOut()
|
|
474
489
|
await zerops.sleep(POLL_INTERVAL_MS, signal)
|
|
@@ -669,7 +684,7 @@ const withMinRam = (custom: ZeropsServiceAutoscaling, minRam: number): ZeropsSer
|
|
|
669
684
|
})
|
|
670
685
|
|
|
671
686
|
/**
|
|
672
|
-
* Raises the deploy service's minimum RAM to what the generated
|
|
687
|
+
* Raises the deploy service's minimum RAM to what the generated init commands need (`zeropsStartMinRam`), before the
|
|
673
688
|
* version's containers start. An import cannot: Zerops ignores a changed `verticalAutoscaling` of an existing service,
|
|
674
689
|
* so an existing service converges here, on every deploy. It only raises, and decides by reading the service back.
|
|
675
690
|
*/
|
|
@@ -970,7 +985,8 @@ const runStep = async (spec: ZeropsStep, env: StepEnv): Promise<void> => {
|
|
|
970
985
|
}
|
|
971
986
|
const appVersionId = requireVersion(env, 'await-version')
|
|
972
987
|
assertRunning(signal)
|
|
973
|
-
|
|
988
|
+
const locked = lockedExecutionFor(artifact, run, dryRun)
|
|
989
|
+
await awaitVersion(env, appVersionId, attempt.processId, locked !== null && locked.databaseUrl !== null)
|
|
974
990
|
return
|
|
975
991
|
}
|
|
976
992
|
|