@fabrika/provider-zerops 0.0.3 → 0.0.5
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 +5 -5
- package/src/api.ts +137 -12
- package/src/cli.ts +42 -2
- package/src/codec.ts +70 -12
- package/src/collaborators.ts +5 -0
- package/src/control.ts +302 -43
- package/src/index.ts +119 -1
- package/src/manifest.ts +60 -3
- package/src/provider.ts +171 -31
- package/src/source-connection.ts +394 -0
- package/src/source.ts +1043 -0
- package/src/types.ts +18 -1
package/src/manifest.ts
CHANGED
|
@@ -3,8 +3,10 @@ import type { JsonValue, ProviderCodec } from '@fabrika/provider-contract'
|
|
|
3
3
|
import { compileImport, ENV_ISOLATION, OVERRIDE, renderYaml } from './compile'
|
|
4
4
|
import type { ZeropsAppConfig, ZeropsNamespaceResourceRequirement, ZeropsProxySpec, ZeropsSharedPostgresBinding } from './types'
|
|
5
5
|
|
|
6
|
-
export const FABRIKA_MANIFEST_VERSION =
|
|
6
|
+
export const FABRIKA_MANIFEST_VERSION = 3
|
|
7
|
+
export const ZEROPS_SOURCE_DESCRIPTOR_MAX_BYTES = 256 * 1024
|
|
7
8
|
export const ZEROPS_SERVICE_HOSTNAME_PATTERN = /^[a-z0-9]{1,25}$/
|
|
9
|
+
const SHA256_PATTERN = /^[0-9a-f]{64}$/
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* The one reference an app may declare for the namespace-owned PostgreSQL service.
|
|
@@ -22,6 +24,12 @@ export interface FabrikaImportDocument {
|
|
|
22
24
|
readonly services: ReadonlyArray<{ readonly [key: string]: JsonValue }>
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
export interface ZeropsArtifactSourceDescriptor {
|
|
28
|
+
readonly path: 'zerops.yaml'
|
|
29
|
+
readonly contents: string
|
|
30
|
+
readonly sha256: string
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
export interface FabrikaManifest {
|
|
26
34
|
manifestVersion: typeof FABRIKA_MANIFEST_VERSION
|
|
27
35
|
app: {
|
|
@@ -35,6 +43,8 @@ export interface FabrikaManifest {
|
|
|
35
43
|
}
|
|
36
44
|
target: {
|
|
37
45
|
platform: 'zerops'
|
|
46
|
+
/** The exact repository-root build descriptor used for this artifact. */
|
|
47
|
+
sourceDescriptor: ZeropsArtifactSourceDescriptor
|
|
38
48
|
/** The canonical document used both for resource claims and the Zerops API payload. */
|
|
39
49
|
importDocument: FabrikaImportDocument
|
|
40
50
|
deployService: string
|
|
@@ -264,6 +274,50 @@ const parseImportDocument = (value: unknown): FabrikaImportDocument => {
|
|
|
264
274
|
return project === undefined ? { services } : { project, services }
|
|
265
275
|
}
|
|
266
276
|
|
|
277
|
+
const parseSourceDescriptor = (value: unknown): ZeropsArtifactSourceDescriptor => {
|
|
278
|
+
if (value === undefined) {
|
|
279
|
+
throw new Error('invalid fabrika manifest Zerops source descriptor')
|
|
280
|
+
}
|
|
281
|
+
const raw = jsonObject(value, 'Zerops source descriptor')
|
|
282
|
+
const unknownKeys = Object.keys(raw).filter((key) => key !== 'path' && key !== 'contents' && key !== 'sha256')
|
|
283
|
+
if (unknownKeys.length > 0) {
|
|
284
|
+
throw new Error(`invalid fabrika manifest Zerops source descriptor field \`${unknownKeys[0]}\``)
|
|
285
|
+
}
|
|
286
|
+
const contents = raw['contents']
|
|
287
|
+
const sha256 = raw['sha256']
|
|
288
|
+
if (
|
|
289
|
+
raw['path'] !== 'zerops.yaml'
|
|
290
|
+
|| typeof contents !== 'string'
|
|
291
|
+
|| contents === ''
|
|
292
|
+
|| new TextEncoder().encode(contents).byteLength > ZEROPS_SOURCE_DESCRIPTOR_MAX_BYTES
|
|
293
|
+
|| typeof sha256 !== 'string'
|
|
294
|
+
|| !SHA256_PATTERN.test(sha256)
|
|
295
|
+
) {
|
|
296
|
+
throw new Error('invalid fabrika manifest Zerops source descriptor')
|
|
297
|
+
}
|
|
298
|
+
return { path: 'zerops.yaml', contents, sha256 }
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export const createZeropsArtifactSourceDescriptor = async (contents: string): Promise<ZeropsArtifactSourceDescriptor> => {
|
|
302
|
+
const bytes = new TextEncoder().encode(contents)
|
|
303
|
+
if (bytes.byteLength === 0) {
|
|
304
|
+
throw new Error('Zerops source descriptor must not be empty')
|
|
305
|
+
}
|
|
306
|
+
if (bytes.byteLength > ZEROPS_SOURCE_DESCRIPTOR_MAX_BYTES) {
|
|
307
|
+
throw new Error(`Zerops source descriptor exceeds ${ZEROPS_SOURCE_DESCRIPTOR_MAX_BYTES} bytes`)
|
|
308
|
+
}
|
|
309
|
+
const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', bytes))
|
|
310
|
+
const sha256 = Array.from(digest, (byte) => byte.toString(16).padStart(2, '0')).join('')
|
|
311
|
+
return { path: 'zerops.yaml', contents, sha256 }
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export const verifyZeropsArtifactSourceDescriptor = async (descriptor: ZeropsArtifactSourceDescriptor): Promise<void> => {
|
|
315
|
+
const actual = await createZeropsArtifactSourceDescriptor(descriptor.contents)
|
|
316
|
+
if (actual.sha256 !== descriptor.sha256) {
|
|
317
|
+
throw new Error('Zerops artifact source descriptor digest mismatch')
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
267
321
|
export const manifestServiceHostnames = (manifest: FabrikaManifest): string[] =>
|
|
268
322
|
manifest.target.importDocument.services.map((service) => {
|
|
269
323
|
const hostname = service['hostname']
|
|
@@ -312,6 +366,7 @@ export function parseFabrikaManifest(value: unknown, expected: ManifestExpectati
|
|
|
312
366
|
}
|
|
313
367
|
|
|
314
368
|
const rawTarget = prop(value, 'target')
|
|
369
|
+
const sourceDescriptor = parseSourceDescriptor(prop(rawTarget, 'sourceDescriptor'))
|
|
315
370
|
const rawImportDocument = prop(rawTarget, 'importDocument')
|
|
316
371
|
const deployService = nonEmptyString(rawTarget, 'deployService')
|
|
317
372
|
if (
|
|
@@ -372,6 +427,7 @@ export function parseFabrikaManifest(value: unknown, expected: ManifestExpectati
|
|
|
372
427
|
},
|
|
373
428
|
target: {
|
|
374
429
|
platform: 'zerops',
|
|
430
|
+
sourceDescriptor,
|
|
375
431
|
importDocument,
|
|
376
432
|
deployService,
|
|
377
433
|
...(zeropsSetup !== null ? { zeropsSetup } : {}),
|
|
@@ -381,7 +437,7 @@ export function parseFabrikaManifest(value: unknown, expected: ManifestExpectati
|
|
|
381
437
|
}
|
|
382
438
|
}
|
|
383
439
|
|
|
384
|
-
export function compileFabrikaManifest(config: ZeropsAppConfig, env: string): FabrikaManifest {
|
|
440
|
+
export function compileFabrikaManifest(config: ZeropsAppConfig, env: string, sourceDescriptor: ZeropsArtifactSourceDescriptor): FabrikaManifest {
|
|
385
441
|
if (config.target.services === undefined) {
|
|
386
442
|
throw new Error('fabrika app build requires a source Zerops config')
|
|
387
443
|
}
|
|
@@ -414,6 +470,7 @@ export function compileFabrikaManifest(config: ZeropsAppConfig, env: string): Fa
|
|
|
414
470
|
},
|
|
415
471
|
target: {
|
|
416
472
|
platform: 'zerops',
|
|
473
|
+
sourceDescriptor,
|
|
417
474
|
importDocument: parseImportDocument(document),
|
|
418
475
|
deployService,
|
|
419
476
|
...(config.target.zeropsSetup !== undefined ? { zeropsSetup: config.target.zeropsSetup } : {}),
|
|
@@ -436,7 +493,7 @@ export function compileFabrikaManifest(config: ZeropsAppConfig, env: string): Fa
|
|
|
436
493
|
|
|
437
494
|
/** Provider artifact codec. Its envelope version and manifest payload version advance independently. */
|
|
438
495
|
export const zeropsArtifactCodec: ProviderCodec<FabrikaManifest> = {
|
|
439
|
-
version:
|
|
496
|
+
version: 3,
|
|
440
497
|
encode: jsonValue,
|
|
441
498
|
decode: (payload) => parseFabrikaManifest(payload),
|
|
442
499
|
}
|
package/src/provider.ts
CHANGED
|
@@ -10,12 +10,27 @@
|
|
|
10
10
|
// cancel the build it started rather than leaving it running.
|
|
11
11
|
|
|
12
12
|
import { createProvider, type ProviderDeploySession, type ProviderModule, type TypedProviderRun } from '@fabrika/provider-contract'
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
ZEROPS_ACTIVE,
|
|
15
|
+
ZEROPS_PROCESS_FINISHED,
|
|
16
|
+
ZEROPS_PROCESS_TERMINAL,
|
|
17
|
+
ZEROPS_TERMINAL,
|
|
18
|
+
ZeropsApiError,
|
|
19
|
+
type ZeropsAppVersion,
|
|
20
|
+
type ZeropsLogAccess,
|
|
21
|
+
type ZeropsProcess,
|
|
22
|
+
} from './api'
|
|
14
23
|
import { zeropsTargetCodec } from './codec'
|
|
15
|
-
import { defaultZeropsCollaborators, type ZeropsCollaboratorFactory, type ZeropsCollaborators } from './collaborators'
|
|
16
|
-
import {
|
|
24
|
+
import { defaultSleep, defaultZeropsCollaborators, type ZeropsCollaboratorFactory, type ZeropsCollaborators } from './collaborators'
|
|
25
|
+
import {
|
|
26
|
+
type FabrikaManifest,
|
|
27
|
+
manifestServiceHostnames,
|
|
28
|
+
renderFabrikaImportYaml,
|
|
29
|
+
verifyZeropsArtifactSourceDescriptor,
|
|
30
|
+
zeropsArtifactCodec,
|
|
31
|
+
} from './manifest'
|
|
17
32
|
import { buildZeropsPlan, resolveDeployHostname, type ZeropsJobSpec, type ZeropsPlan } from './plan'
|
|
18
|
-
import type { ZeropsRuntimeTarget } from './types'
|
|
33
|
+
import type { ZeropsRunState, ZeropsRuntimeSource, ZeropsRuntimeTarget } from './types'
|
|
19
34
|
|
|
20
35
|
export const CANCELLED = 'deploy cancelled'
|
|
21
36
|
|
|
@@ -27,6 +42,12 @@ const POLL_INTERVAL_MS = 3000
|
|
|
27
42
|
/** Give up after this long. Zerops' own pipeline limit is 1 hour; we allow a little slack past it. */
|
|
28
43
|
const POLL_TIMEOUT_MS = 70 * 60 * 1000
|
|
29
44
|
|
|
45
|
+
/** Delay before deciding whether an ambiguous build trigger remained pre-trigger. */
|
|
46
|
+
const BUILD_TRIGGER_CONSISTENCY_MS = 10_000
|
|
47
|
+
|
|
48
|
+
/** Source cancellation cannot hold Zerops cleanup hostage. */
|
|
49
|
+
const SOURCE_CANCEL_TIMEOUT_MS = 5000
|
|
50
|
+
|
|
30
51
|
/** Abandon the step if the run was cancelled. Checked before every real mutation and each poll iteration. */
|
|
31
52
|
const assertRunning = (signal: AbortSignal): void => {
|
|
32
53
|
if (signal.aborted) {
|
|
@@ -74,7 +95,52 @@ interface StepEnv {
|
|
|
74
95
|
signal: AbortSignal
|
|
75
96
|
dryRun: boolean
|
|
76
97
|
/** Set by `trigger-deploy`, read by `await-deploy` — the only state that crosses a step boundary. */
|
|
77
|
-
state: { appVersionId?: string }
|
|
98
|
+
state: { appVersionId?: string; processId?: string }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const runState = (
|
|
102
|
+
appVersionId: string,
|
|
103
|
+
phase: Exclude<ZeropsRunState['phase'], 'build_triggered'>,
|
|
104
|
+
): Exclude<ZeropsRunState, { phase: 'build_triggered' }> => ({ appVersionId, phase })
|
|
105
|
+
|
|
106
|
+
const buildTriggeredState = (
|
|
107
|
+
appVersionId: string,
|
|
108
|
+
processId?: string,
|
|
109
|
+
): Extract<ZeropsRunState, { phase: 'build_triggered' }> => ({
|
|
110
|
+
appVersionId,
|
|
111
|
+
phase: 'build_triggered',
|
|
112
|
+
...(processId === undefined ? {} : { processId }),
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
const requireSource = (env: StepEnv): { runtime: ZeropsRuntimeSource; client: NonNullable<ZeropsCollaborators['source']> } => {
|
|
116
|
+
const runtime = env.run.target.source
|
|
117
|
+
const client = env.zerops.source
|
|
118
|
+
if (runtime === undefined || client === undefined) {
|
|
119
|
+
throw new Error('zerops: upload-backed deploy requires source coordinates and a source client')
|
|
120
|
+
}
|
|
121
|
+
return { runtime, client }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const cancelSourceBestEffort = async (env: StepEnv, source: ZeropsRuntimeSource, appVersionId: string): Promise<void> => {
|
|
125
|
+
const client = env.zerops.source
|
|
126
|
+
if (client === undefined) return
|
|
127
|
+
const sourceController = new AbortController()
|
|
128
|
+
const timeoutController = new AbortController()
|
|
129
|
+
const cancel = Promise.resolve()
|
|
130
|
+
.then(() => client.cancel({ runId: source.runId, appVersionId, signal: sourceController.signal }))
|
|
131
|
+
.catch(() => {})
|
|
132
|
+
const timeout = Promise.resolve()
|
|
133
|
+
.then(() => (env.zerops.sourceCancelSleep ?? defaultSleep)(SOURCE_CANCEL_TIMEOUT_MS, timeoutController.signal))
|
|
134
|
+
.then(() => sourceController.abort())
|
|
135
|
+
.catch(() => {})
|
|
136
|
+
await Promise.race([cancel, timeout])
|
|
137
|
+
timeoutController.abort()
|
|
138
|
+
sourceController.abort()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const cleanupPreTriggerVersion = async (env: StepEnv, source: ZeropsRuntimeSource, appVersionId: string): Promise<void> => {
|
|
142
|
+
await cancelSourceBestEffort(env, source, appVersionId)
|
|
143
|
+
await env.zerops.api.deleteAppVersion({ appVersionId, signal: AbortSignal.timeout(5000) })
|
|
78
144
|
}
|
|
79
145
|
|
|
80
146
|
/**
|
|
@@ -83,7 +149,7 @@ interface StepEnv {
|
|
|
83
149
|
* Deliberately FAILURE-TOLERANT: the log service's protocol is the one shape in this driver nobody could
|
|
84
150
|
* verify against a document (see `ZeropsApi.readBuildLog`). An unverified endpoint must never be able to
|
|
85
151
|
* fail a deploy, so a broken guess degrades to "no lines relayed" and the run still succeeds or fails on
|
|
86
|
-
*
|
|
152
|
+
* the platform's process and app-version statuses.
|
|
87
153
|
*
|
|
88
154
|
* Relay is pull-based (ADR-0003), so each poll re-reads a window that overlaps the last one; `seen` keeps
|
|
89
155
|
* the run log from repeating itself. Two genuinely identical lines with no timestamp collapse into one —
|
|
@@ -119,13 +185,14 @@ const openLog = async (env: StepEnv): Promise<ZeropsLogAccess | null> => {
|
|
|
119
185
|
}
|
|
120
186
|
|
|
121
187
|
/**
|
|
122
|
-
* Poll `/app-version` until the version reaches a terminal state, relaying the
|
|
188
|
+
* Poll the trigger process and `/app-version` until the version reaches a terminal state, relaying the
|
|
189
|
+
* build log as it goes. The process is optional because the trigger response may omit its id.
|
|
123
190
|
*
|
|
124
191
|
* A run cancelled here asks Zerops to cancel the BUILD too. That is not tidiness: the platform is running
|
|
125
192
|
* the work, so abandoning the poll loop without telling it leaves a build container burning through the
|
|
126
193
|
* pipeline's hour.
|
|
127
194
|
*/
|
|
128
|
-
const awaitVersion = async (env: StepEnv, appVersionId: string): Promise<void> => {
|
|
195
|
+
const awaitVersion = async (env: StepEnv, appVersionId: string, processId: string | undefined): Promise<void> => {
|
|
129
196
|
const { zerops, signal, log } = env
|
|
130
197
|
const access = await openLog(env)
|
|
131
198
|
const seen = new Set<string>()
|
|
@@ -138,13 +205,30 @@ const awaitVersion = async (env: StepEnv, appVersionId: string): Promise<void> =
|
|
|
138
205
|
await zerops.api.cancelBuild({ appVersionId, signal: AbortSignal.timeout(5000) }).catch(() => {})
|
|
139
206
|
throw new Error(CANCELLED)
|
|
140
207
|
}
|
|
141
|
-
|
|
208
|
+
let process: ZeropsProcess | undefined
|
|
209
|
+
let version: ZeropsAppVersion
|
|
210
|
+
try {
|
|
211
|
+
process = processId === undefined ? undefined : await zerops.api.getProcess({ processId, signal })
|
|
212
|
+
version = await zerops.api.getAppVersion({ appVersionId, signal })
|
|
213
|
+
} catch {
|
|
214
|
+
log(` ${appVersionId}: status observation unavailable; retrying`)
|
|
215
|
+
if (Date.now() > deadline) {
|
|
216
|
+
throw new Error(`zerops: timed out after ${Math.round(POLL_TIMEOUT_MS / 60000)}m waiting for app-version ${appVersionId}`)
|
|
217
|
+
}
|
|
218
|
+
await zerops.sleep(POLL_INTERVAL_MS, signal)
|
|
219
|
+
continue
|
|
220
|
+
}
|
|
142
221
|
if (version.status !== previous) {
|
|
143
222
|
log(` ${appVersionId}: ${version.status ?? 'unknown'}`)
|
|
144
223
|
previous = version.status
|
|
145
224
|
}
|
|
146
225
|
await relayLog(env, access, seen)
|
|
147
226
|
|
|
227
|
+
if (process?.status !== undefined && ZEROPS_PROCESS_TERMINAL.has(process.status) && process.status !== ZEROPS_PROCESS_FINISHED) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
`zerops: pipeline process ${process.id} is ${process.status} while app-version ${appVersionId} is ${version.status ?? 'in an unknown state'}`,
|
|
230
|
+
)
|
|
231
|
+
}
|
|
148
232
|
if (version.status !== undefined && ZEROPS_TERMINAL.has(version.status)) {
|
|
149
233
|
if (version.status === ZEROPS_ACTIVE) {
|
|
150
234
|
return
|
|
@@ -180,37 +264,93 @@ const runStep = async (spec: ZeropsJobSpec, env: StepEnv): Promise<void> => {
|
|
|
180
264
|
}
|
|
181
265
|
|
|
182
266
|
case 'trigger-deploy': {
|
|
183
|
-
const source = target.buildFromGit === undefined ? "the service's configured Git integration" : target.buildFromGit
|
|
184
267
|
if (dryRun) {
|
|
185
|
-
log(` [dry-run] would
|
|
268
|
+
log(` [dry-run] would create an app version, upload the resolved repository snapshot, and trigger build+deploy for service ${target.serviceId}`)
|
|
186
269
|
return
|
|
187
270
|
}
|
|
188
271
|
assertRunning(signal)
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
buildFromGit: target.buildFromGit,
|
|
193
|
-
zeropsSetup,
|
|
194
|
-
signal,
|
|
195
|
-
})
|
|
196
|
-
// The trigger response's app-version id is optional on the wire, so resolve the version we just
|
|
197
|
-
// created rather than depending on it — and fail loudly if there is none, since awaiting a
|
|
198
|
-
// version we cannot name would silently watch someone else's deploy.
|
|
199
|
-
const version = process?.appVersionId !== undefined
|
|
200
|
-
? { id: process.appVersionId }
|
|
201
|
-
: await zerops.api.latestAppVersion({ serviceId: target.serviceId, signal })
|
|
202
|
-
if (version === null || version.id === '') {
|
|
203
|
-
throw new Error(`zerops: pipeline triggered for service ${target.serviceId} but no app-version could be resolved`)
|
|
204
|
-
}
|
|
272
|
+
const source = requireSource(env)
|
|
273
|
+
await verifyZeropsArtifactSourceDescriptor(artifact.target.sourceDescriptor)
|
|
274
|
+
const version = await zerops.api.createAppVersion({ serviceId: target.serviceId, name: source.runtime.runId, signal })
|
|
205
275
|
state.appVersionId = version.id
|
|
206
|
-
|
|
207
|
-
|
|
276
|
+
let buildTriggerRequested = false
|
|
277
|
+
try {
|
|
278
|
+
await run.events.externalId(version.id, runState(version.id, 'version_created'))
|
|
279
|
+
const uploaded = await source.client.upload({
|
|
280
|
+
runId: source.runtime.runId,
|
|
281
|
+
appVersionId: version.id,
|
|
282
|
+
repository: source.runtime.repository,
|
|
283
|
+
commitSha: source.runtime.commitSha,
|
|
284
|
+
...(source.runtime.githubInstallationId === undefined
|
|
285
|
+
? {}
|
|
286
|
+
: { githubInstallationId: source.runtime.githubInstallationId }),
|
|
287
|
+
uploadUrl: version.uploadUrl,
|
|
288
|
+
descriptor: {
|
|
289
|
+
path: artifact.target.sourceDescriptor.path,
|
|
290
|
+
sha256: artifact.target.sourceDescriptor.sha256,
|
|
291
|
+
},
|
|
292
|
+
signal,
|
|
293
|
+
})
|
|
294
|
+
if (
|
|
295
|
+
uploaded.runId !== source.runtime.runId
|
|
296
|
+
|| uploaded.appVersionId !== version.id
|
|
297
|
+
|| uploaded.commitSha !== source.runtime.commitSha
|
|
298
|
+
|| uploaded.descriptorSha256 !== artifact.target.sourceDescriptor.sha256
|
|
299
|
+
) {
|
|
300
|
+
throw new Error('zerops: source upload returned different coordinates')
|
|
301
|
+
}
|
|
302
|
+
await run.events.checkpoint(runState(version.id, 'source_uploaded'))
|
|
303
|
+
await verifyZeropsArtifactSourceDescriptor(artifact.target.sourceDescriptor)
|
|
304
|
+
await run.events.checkpoint(runState(version.id, 'build_trigger_requested'))
|
|
305
|
+
buildTriggerRequested = true
|
|
306
|
+
let process: ZeropsProcess
|
|
307
|
+
try {
|
|
308
|
+
process = await zerops.api.buildAndDeployAppVersion({
|
|
309
|
+
appVersionId: version.id,
|
|
310
|
+
zeropsYaml: artifact.target.sourceDescriptor.contents,
|
|
311
|
+
...(artifact.target.zeropsSetup === undefined ? {} : { zeropsYamlSetup: artifact.target.zeropsSetup }),
|
|
312
|
+
signal,
|
|
313
|
+
})
|
|
314
|
+
} catch (error) {
|
|
315
|
+
if (error instanceof ZeropsApiError && error.status >= 400 && error.status < 500) {
|
|
316
|
+
await cleanupPreTriggerVersion(env, source.runtime, version.id)
|
|
317
|
+
throw error
|
|
318
|
+
}
|
|
319
|
+
let observed: ZeropsAppVersion | undefined
|
|
320
|
+
try {
|
|
321
|
+
await zerops.sleep(BUILD_TRIGGER_CONSISTENCY_MS, signal)
|
|
322
|
+
observed = await zerops.api.getAppVersion({ appVersionId: version.id, signal })
|
|
323
|
+
} catch {
|
|
324
|
+
log(` ${version.id}: build-trigger observation unavailable; continuing to poll`)
|
|
325
|
+
return
|
|
326
|
+
}
|
|
327
|
+
if (observed.id === version.id && observed.status === 'UPLOADING') {
|
|
328
|
+
await cleanupPreTriggerVersion(env, source.runtime, version.id)
|
|
329
|
+
throw error
|
|
330
|
+
}
|
|
331
|
+
if (observed.id === version.id && observed.status !== undefined) {
|
|
332
|
+
await run.events.checkpoint(buildTriggeredState(version.id)).catch(() => {})
|
|
333
|
+
log(` recovered accepted build trigger for app-version ${version.id}`)
|
|
334
|
+
} else {
|
|
335
|
+
log(` ${version.id}: build-trigger state is not known yet; continuing to poll`)
|
|
336
|
+
}
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
state.processId = process.id
|
|
340
|
+
await run.events.checkpoint(buildTriggeredState(version.id, process.id)).catch(() => {})
|
|
341
|
+
log(` triggered: app-version ${version.id} (build+deploy is ONE platform-side operation)`)
|
|
342
|
+
} catch (error) {
|
|
343
|
+
if (!buildTriggerRequested) {
|
|
344
|
+
await cleanupPreTriggerVersion(env, source.runtime, version.id)
|
|
345
|
+
}
|
|
346
|
+
throw error
|
|
347
|
+
}
|
|
208
348
|
return
|
|
209
349
|
}
|
|
210
350
|
|
|
211
351
|
case 'await-deploy': {
|
|
212
352
|
if (dryRun) {
|
|
213
|
-
log(' [dry-run] would poll /app-version until it is ACTIVE
|
|
353
|
+
log(' [dry-run] would poll the pipeline process when available and /app-version until it is ACTIVE, relaying the build log')
|
|
214
354
|
return
|
|
215
355
|
}
|
|
216
356
|
const appVersionId = state.appVersionId
|
|
@@ -218,7 +358,7 @@ const runStep = async (spec: ZeropsJobSpec, env: StepEnv): Promise<void> => {
|
|
|
218
358
|
throw new Error('zerops: await-deploy has no app-version to watch (trigger-deploy did not run)')
|
|
219
359
|
}
|
|
220
360
|
assertRunning(signal)
|
|
221
|
-
await awaitVersion(env, appVersionId)
|
|
361
|
+
await awaitVersion(env, appVersionId, state.processId)
|
|
222
362
|
return
|
|
223
363
|
}
|
|
224
364
|
|