@fabrika/provider-zerops 0.0.26 → 0.3.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 +18 -5
- package/src/api/client.ts +522 -0
- package/src/api/errors.ts +50 -0
- package/src/api/polling.ts +59 -0
- package/src/api/readers.ts +185 -0
- package/src/api/service-env-response.ts +34 -0
- package/src/api/types.ts +602 -0
- package/src/api.ts +29 -1378
- package/src/authoring.ts +7 -10
- package/src/cli-args.ts +0 -5
- package/src/cli.ts +3 -28
- package/src/collaborators.ts +2 -4
- package/src/compile.ts +75 -20
- package/src/control.ts +217 -447
- package/src/descriptor.ts +135 -0
- package/src/index.ts +24 -128
- package/src/installation/admin-options.ts +128 -0
- package/src/installation/admin.ts +123 -0
- package/src/installation/deploy-options.ts +155 -0
- package/src/installation/deploy.ts +668 -0
- package/src/installation/generated/platform-console-schema.ts +41 -0
- package/src/installation/generated/platform-proxy-manifest.ts +72 -0
- package/src/installation/hosts.ts +117 -0
- package/src/installation/index.ts +366 -0
- package/src/installation/init.ts +521 -0
- package/src/installation/install-collaborators.ts +28 -0
- package/src/installation/install-environment.ts +193 -0
- package/src/installation/install-options.ts +193 -0
- package/src/installation/install-project.ts +201 -0
- package/src/installation/install-provisioning.ts +185 -0
- package/src/installation/install.ts +286 -0
- package/src/installation/installation-source.ts +42 -0
- package/src/installation/log.ts +77 -0
- package/src/installation/machine-key.ts +65 -0
- package/src/installation/manifest.ts +95 -0
- package/src/installation/placement-options.ts +33 -0
- package/src/installation/platform-source.ts +106 -0
- package/src/installation/platform-upload.ts +34 -0
- package/src/installation/proxy-manifest.ts +113 -0
- package/src/installation/secrets.ts +79 -0
- package/src/installation/sidecar.ts +122 -0
- package/src/installation/source-options.ts +12 -0
- package/src/installation/templates/README.md +116 -0
- package/src/installation/templates/fabrika.ref +1 -0
- package/src/installation/templates/gitignore +3 -0
- package/src/installation/templates/platform.yml +144 -0
- package/src/installation/topology/artifacts.ts +141 -0
- package/src/installation/topology/index.ts +13 -0
- package/src/installation/topology/installation-schemas.ts +225 -0
- package/src/installation/topology/invariants.ts +86 -0
- package/src/installation/topology/setups.ts +442 -0
- package/src/installation/topology/topology.ts +501 -0
- package/src/installation/topology/validate.ts +26 -0
- package/src/installation/upgrade-options.ts +108 -0
- package/src/installation/upgrade-output.ts +31 -0
- package/src/installation/upgrade.ts +373 -0
- package/src/managed-env.ts +105 -0
- package/src/manifest.ts +208 -315
- package/src/namespace/claims.ts +77 -0
- package/src/namespace/codec.ts +179 -0
- package/src/namespace/constants.ts +30 -0
- package/src/namespace/errors.ts +60 -0
- package/src/namespace/manifest.ts +28 -0
- package/src/namespace/operator.ts +151 -0
- package/src/namespace/provisioning.ts +482 -0
- package/src/namespace/proxy-attempt.ts +220 -0
- package/src/namespace/proxy-source.ts +22 -0
- package/src/namespace/topology.ts +115 -0
- package/src/namespace/types.ts +114 -0
- package/src/namespace.ts +34 -1260
- package/src/plan.ts +21 -23
- package/src/provider.ts +399 -254
- package/src/service-references.ts +28 -0
- package/src/source/constants.ts +11 -0
- package/src/source/port.ts +49 -0
- package/src/source/types.ts +4 -0
- package/src/source/validation.ts +72 -0
- package/src/source.ts +2 -1220
- package/src/types.ts +8 -7
- package/src/version-attempt.ts +18 -0
- package/src/codec.ts +0 -108
- package/src/namespace-command.ts +0 -209
- package/src/source-connection.ts +0 -344
package/src/provider.ts
CHANGED
|
@@ -4,12 +4,20 @@
|
|
|
4
4
|
// driver's whole side-effect surface is one REST client plus the portable schema reconciler
|
|
5
5
|
// (`./collaborators.ts`). It never spawns a process and has no `runCommand` to be handed.
|
|
6
6
|
//
|
|
7
|
-
// The interesting step is `await-
|
|
8
|
-
// runs.
|
|
9
|
-
//
|
|
10
|
-
// cancel the build it started rather than leaving it running.
|
|
7
|
+
// The interesting step is `await-version`: fabrika does not RUN the build, it WATCHES one the platform
|
|
8
|
+
// runs. Recovery never resumes a deploy (ADR-0044): a version reserved but never triggered is deleted, and
|
|
9
|
+
// one whose build may have been requested is settled only by polling it.
|
|
11
10
|
|
|
12
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
type DeployStepResult,
|
|
13
|
+
followLeaseLoss,
|
|
14
|
+
isLeaseLoss,
|
|
15
|
+
type JsonValue,
|
|
16
|
+
ProviderInvocationAbort,
|
|
17
|
+
type ProviderManagedEnvironment,
|
|
18
|
+
type ProviderRunEvents,
|
|
19
|
+
runSteps,
|
|
20
|
+
} from '@fabrika/provider-contract'
|
|
13
21
|
import {
|
|
14
22
|
waitForProcess,
|
|
15
23
|
ZEROPS_ACTIVE,
|
|
@@ -21,68 +29,202 @@ import {
|
|
|
21
29
|
type ZeropsLogAccess,
|
|
22
30
|
type ZeropsProcess,
|
|
23
31
|
} from './api'
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
32
|
+
import { defaultZeropsCollaborators, type ZeropsCollaboratorFactory, type ZeropsCollaborators } from './collaborators'
|
|
33
|
+
import { assertZeropsInvariants, renderYaml } from './compile'
|
|
26
34
|
import {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
import type
|
|
35
|
+
assertAuthoredManagedDocument,
|
|
36
|
+
assertAuthoredManagedInputs,
|
|
37
|
+
type ManagedValues,
|
|
38
|
+
overlayRelease,
|
|
39
|
+
parseManagedValues,
|
|
40
|
+
SERVICE_MANAGED_KEYS,
|
|
41
|
+
} from './managed-env'
|
|
42
|
+
import { type FabrikaImportDocument, type FabrikaManifest, manifestServiceHostnames, verifyZeropsArtifactSourceDescriptor } from './manifest'
|
|
43
|
+
import { buildZeropsPlan, resolveDeployHostname, type ZeropsStep } from './plan'
|
|
44
|
+
import { allowedReferenceHosts, foreignReferenceHost } from './service-references'
|
|
45
|
+
import { isZeropsUploadDestination } from './source'
|
|
35
46
|
import type { ZeropsRunState, ZeropsRuntimeSource, ZeropsRuntimeTarget } from './types'
|
|
47
|
+
import { deleteVersion } from './version-attempt'
|
|
36
48
|
|
|
37
49
|
export const CANCELLED = 'deploy cancelled'
|
|
38
50
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
readonly
|
|
44
|
-
readonly
|
|
51
|
+
export interface ZeropsDeployRun {
|
|
52
|
+
readonly appId: string
|
|
53
|
+
readonly env: string
|
|
54
|
+
/** Every origin IAM may hand this app a session at; app-wide, because IAM's registry is keyed by app id. */
|
|
55
|
+
readonly returnOrigins?: readonly string[]
|
|
56
|
+
readonly secrets: Readonly<Record<string, string>>
|
|
57
|
+
readonly vars: Readonly<Record<string, string>>
|
|
58
|
+
/** Platform-owned runtime configuration, separate from operator-managed application vars. */
|
|
59
|
+
readonly managedEnvironment: ProviderManagedEnvironment
|
|
60
|
+
readonly target: ZeropsRuntimeTarget
|
|
61
|
+
readonly artifact: FabrikaManifest
|
|
62
|
+
readonly dryRun: boolean
|
|
63
|
+
readonly signal: AbortSignal
|
|
64
|
+
readonly events: ProviderRunEvents
|
|
45
65
|
}
|
|
46
66
|
|
|
47
|
-
export interface
|
|
48
|
-
|
|
49
|
-
|
|
67
|
+
export interface ZeropsDeployResult {
|
|
68
|
+
readonly appId: string
|
|
69
|
+
readonly env: string
|
|
70
|
+
readonly status: 'succeeded' | 'failed'
|
|
71
|
+
readonly steps: readonly DeployStepResult[]
|
|
50
72
|
}
|
|
51
73
|
|
|
52
|
-
/** How often `await-
|
|
74
|
+
/** How often `await-version` asks Zerops for the version's status. */
|
|
53
75
|
const POLL_INTERVAL_MS = 3000
|
|
54
76
|
|
|
55
|
-
/**
|
|
56
|
-
const
|
|
77
|
+
/** A run stops waiting for its version this long after it started. Zerops' own pipeline limit is 1 hour. */
|
|
78
|
+
export const ZEROPS_VERSION_DEADLINE_MS = 75 * 60 * 1000
|
|
57
79
|
|
|
58
|
-
|
|
59
|
-
const BUILD_TRIGGER_CONSISTENCY_MS = 10_000
|
|
60
|
-
|
|
61
|
-
/** Source cancellation cannot hold Zerops cleanup hostage. */
|
|
62
|
-
const SOURCE_CANCEL_TIMEOUT_MS = 5000
|
|
80
|
+
const RELEASE_KEY = 'FABRIKA_RELEASE'
|
|
63
81
|
|
|
64
82
|
/** Abandon the step if the run was cancelled. Checked before every real mutation and each poll iteration. */
|
|
65
83
|
const assertRunning = (signal: AbortSignal): void => {
|
|
66
84
|
if (signal.aborted) {
|
|
85
|
+
if (signal.reason instanceof ProviderInvocationAbort) throw signal.reason
|
|
67
86
|
throw new Error(CANCELLED)
|
|
68
87
|
}
|
|
69
88
|
}
|
|
70
89
|
|
|
71
90
|
const PLACEHOLDER = /\$\{([A-Z][A-Z0-9_]*)\}/g
|
|
72
91
|
|
|
73
|
-
/**
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
92
|
+
/** Fields that decide WHAT a service is: a placeholder there would let a value redefine the service. */
|
|
93
|
+
const STRUCTURAL_FIELDS: readonly string[] = ['hostname', 'type', 'envIsolation', 'override']
|
|
94
|
+
|
|
95
|
+
/** Rebuild the tree with every string leaf resolved — arrays and nested objects included. */
|
|
96
|
+
const substituteLeaves = (value: JsonValue, resolve: (text: string) => string): JsonValue => {
|
|
97
|
+
if (typeof value === 'string') {
|
|
98
|
+
return resolve(value)
|
|
99
|
+
}
|
|
100
|
+
if (Array.isArray(value)) {
|
|
101
|
+
return value.map((item) => substituteLeaves(item, resolve))
|
|
102
|
+
}
|
|
103
|
+
if (value !== null && typeof value === 'object') {
|
|
104
|
+
return substituteRecord(value, resolve)
|
|
105
|
+
}
|
|
106
|
+
return value
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const substituteRecord = (
|
|
110
|
+
record: { readonly [key: string]: JsonValue },
|
|
111
|
+
resolve: (text: string) => string,
|
|
112
|
+
): { [key: string]: JsonValue } => {
|
|
113
|
+
const substituted: { [key: string]: JsonValue } = {}
|
|
114
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
115
|
+
const planted = key.match(PLACEHOLDER)
|
|
116
|
+
if (planted !== null) {
|
|
117
|
+
throw new Error(`zerops: placeholder \`${String(planted[0])}\` stands in an import document key; a variable may only fill a value`)
|
|
118
|
+
}
|
|
119
|
+
// Defined rather than assigned: a literal `__proto__` key would otherwise set the prototype.
|
|
120
|
+
Object.defineProperty(substituted, key, {
|
|
121
|
+
value: substituteLeaves(entry, resolve),
|
|
122
|
+
enumerable: true,
|
|
123
|
+
writable: true,
|
|
124
|
+
configurable: true,
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
return substituted
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** A structural field that moved means a placeholder sat where only the compiler may write. */
|
|
131
|
+
const assertStructureUnchanged = (before: FabrikaImportDocument, after: FabrikaImportDocument): void => {
|
|
132
|
+
if (JSON.stringify(before.project) !== JSON.stringify(after.project)) {
|
|
133
|
+
throw new Error('zerops: variable substitution changed the project entry; a variable may not stand in the project fabrika creates')
|
|
134
|
+
}
|
|
135
|
+
for (const [index, service] of after.services.entries()) {
|
|
136
|
+
const original = before.services[index]
|
|
137
|
+
for (const field of STRUCTURAL_FIELDS) {
|
|
138
|
+
if (original?.[field] !== service[field]) {
|
|
139
|
+
throw new Error(`zerops: variable substitution changed service field \`${field}\`; a variable may not stand in a service's identity`)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Substitution is a single pass, so a placeholder left in the document came out of a variable's VALUE. */
|
|
146
|
+
const assertNoPlaceholderLeft = (document: FabrikaImportDocument): void => {
|
|
147
|
+
const refuse = (text: string): void => {
|
|
148
|
+
const left = text.match(PLACEHOLDER)
|
|
149
|
+
if (left !== null) {
|
|
150
|
+
throw new Error(`zerops: placeholder \`${String(left[0])}\` survived substitution; a variable value may not carry one`)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const visitRecord = (record: { readonly [key: string]: JsonValue }): void => {
|
|
154
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
155
|
+
refuse(key)
|
|
156
|
+
visit(entry)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const visit = (value: JsonValue): void => {
|
|
160
|
+
if (typeof value === 'string') {
|
|
161
|
+
refuse(value)
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
if (Array.isArray(value)) {
|
|
165
|
+
for (const item of value) visit(item)
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
if (value !== null && typeof value === 'object') {
|
|
169
|
+
visitRecord(value)
|
|
79
170
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
171
|
+
}
|
|
172
|
+
if (document.project !== undefined) {
|
|
173
|
+
visitRecord(document.project)
|
|
174
|
+
}
|
|
175
|
+
for (const service of document.services) {
|
|
176
|
+
visitRecord(service)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Resolve the manifest's declared variables INTO THE DOCUMENT — never into the rendered YAML, where a
|
|
182
|
+
* value carrying a quote or a newline would be read as structure and could add a service or turn
|
|
183
|
+
* `envIsolation` off. Only variables the manifest declares resolve; every other placeholder fails the
|
|
184
|
+
* deploy, and so does a value that names a service outside the app (Zerops resolves `${host_KEY}`
|
|
185
|
+
* regardless of isolation). The document is re-checked here because this is the last point it is an object.
|
|
186
|
+
*/
|
|
187
|
+
export function interpolateManifest(
|
|
188
|
+
document: FabrikaImportDocument,
|
|
189
|
+
declared: readonly string[],
|
|
190
|
+
values: Record<string, string>,
|
|
191
|
+
ownHostnames: readonly string[],
|
|
192
|
+
): FabrikaImportDocument {
|
|
193
|
+
assertAuthoredManagedDocument(document.services)
|
|
194
|
+
assertAuthoredManagedInputs({ variableNames: [...declared, ...Object.keys(values)], secretNames: [] })
|
|
195
|
+
const allowed = new Set(declared)
|
|
196
|
+
const hosts = allowedReferenceHosts(ownHostnames)
|
|
197
|
+
const resolve = (text: string): string => {
|
|
198
|
+
const substituted = text.replace(PLACEHOLDER, (_placeholder, name: string) => {
|
|
199
|
+
if (!allowed.has(name)) {
|
|
200
|
+
throw new Error(`zerops: manifest uses undeclared variable \`${name}\``)
|
|
201
|
+
}
|
|
202
|
+
const value = values[name]
|
|
203
|
+
if (value === undefined) {
|
|
204
|
+
throw new Error(`zerops: manifest variable \`${name}\` has no deploy-time value`)
|
|
205
|
+
}
|
|
206
|
+
const foreign = foreignReferenceHost(value, hosts)
|
|
207
|
+
if (foreign !== null) {
|
|
208
|
+
throw new Error(`zerops: manifest variable \`${name}\` references service \`${foreign}\`, which this app does not own`)
|
|
209
|
+
}
|
|
210
|
+
return value
|
|
211
|
+
})
|
|
212
|
+
// Adjacent values can spell a reference neither of them carries alone.
|
|
213
|
+
const foreign = substituted === text ? null : foreignReferenceHost(substituted, hosts)
|
|
214
|
+
if (foreign !== null) {
|
|
215
|
+
throw new Error(`zerops: a substituted value references service \`${foreign}\`, which this app does not own`)
|
|
83
216
|
}
|
|
84
|
-
return
|
|
85
|
-
}
|
|
217
|
+
return substituted
|
|
218
|
+
}
|
|
219
|
+
const services = document.services.map((service) => substituteRecord(service, resolve))
|
|
220
|
+
const substituted: FabrikaImportDocument = document.project === undefined
|
|
221
|
+
? { services }
|
|
222
|
+
: { project: substituteRecord(document.project, resolve), services }
|
|
223
|
+
assertZeropsInvariants(substituted)
|
|
224
|
+
assertStructureUnchanged(document, substituted)
|
|
225
|
+
assertNoPlaceholderLeft(substituted)
|
|
226
|
+
assertAuthoredManagedDocument(substituted.services)
|
|
227
|
+
return substituted
|
|
86
228
|
}
|
|
87
229
|
|
|
88
230
|
/** Human-readable failure for a version that finished in a non-`ACTIVE` state. */
|
|
@@ -96,36 +238,32 @@ const terminalFailure = (version: ZeropsAppVersion): string => {
|
|
|
96
238
|
return `Zerops pipeline failed: app-version ${version.id} is ${version.status ?? 'in an unknown state'}`
|
|
97
239
|
}
|
|
98
240
|
|
|
241
|
+
/**
|
|
242
|
+
* The attempt this invocation is making. `appVersionId` is set once a version is reserved and `triggered`
|
|
243
|
+
* just before the build request is sent; together they decide whether a failure may delete the version.
|
|
244
|
+
*/
|
|
245
|
+
interface Attempt {
|
|
246
|
+
appVersionId?: string
|
|
247
|
+
uploadUrl?: string
|
|
248
|
+
processId?: string
|
|
249
|
+
triggered: boolean
|
|
250
|
+
}
|
|
251
|
+
|
|
99
252
|
/** Everything one step's executor needs, bundled so the per-kind handlers stay small. */
|
|
100
253
|
interface StepEnv {
|
|
101
|
-
run:
|
|
254
|
+
run: ZeropsDeployRun
|
|
102
255
|
zerops: ZeropsCollaborators
|
|
103
256
|
/** The compiled import document + its YAML text, materialized once in `open()`. */
|
|
104
257
|
compiled: { yaml: string; hostnames: string[] }
|
|
105
|
-
/** Hostname of the service the app's code deploys to. */
|
|
106
|
-
deployHostname: string
|
|
107
258
|
log: (line: string) => void
|
|
108
259
|
signal: AbortSignal
|
|
109
260
|
dryRun: boolean
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
261
|
+
attempt: Attempt
|
|
262
|
+
managedValues: ManagedValues
|
|
263
|
+
/** `await-version` gives up at this instant. */
|
|
264
|
+
deadlineMs: number
|
|
113
265
|
}
|
|
114
266
|
|
|
115
|
-
const runState = (
|
|
116
|
-
appVersionId: string,
|
|
117
|
-
phase: Exclude<ZeropsRunState['phase'], 'build_triggered'>,
|
|
118
|
-
): Exclude<ZeropsRunState, { phase: 'build_triggered' }> => ({ appVersionId, phase })
|
|
119
|
-
|
|
120
|
-
const buildTriggeredState = (
|
|
121
|
-
appVersionId: string,
|
|
122
|
-
processId?: string,
|
|
123
|
-
): Extract<ZeropsRunState, { phase: 'build_triggered' }> => ({
|
|
124
|
-
appVersionId,
|
|
125
|
-
phase: 'build_triggered',
|
|
126
|
-
...(processId === undefined ? {} : { processId }),
|
|
127
|
-
})
|
|
128
|
-
|
|
129
267
|
const requireSource = (env: StepEnv): { runtime: ZeropsRuntimeSource; client: NonNullable<ZeropsCollaborators['source']> } => {
|
|
130
268
|
const runtime = env.run.target.source
|
|
131
269
|
const client = env.zerops.source
|
|
@@ -135,38 +273,15 @@ const requireSource = (env: StepEnv): { runtime: ZeropsRuntimeSource; client: No
|
|
|
135
273
|
return { runtime, client }
|
|
136
274
|
}
|
|
137
275
|
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
if (
|
|
141
|
-
|
|
142
|
-
const timeoutController = new AbortController()
|
|
143
|
-
const cancel = Promise.resolve()
|
|
144
|
-
.then(() => client.cancel({ runId: source.runId, appVersionId, signal: sourceController.signal }))
|
|
145
|
-
.catch(() => {})
|
|
146
|
-
const timeout = Promise.resolve()
|
|
147
|
-
.then(() => (env.zerops.sourceCancelSleep ?? defaultSleep)(SOURCE_CANCEL_TIMEOUT_MS, timeoutController.signal))
|
|
148
|
-
.then(() => sourceController.abort())
|
|
149
|
-
.catch(() => {})
|
|
150
|
-
await Promise.race([cancel, timeout])
|
|
151
|
-
timeoutController.abort()
|
|
152
|
-
sourceController.abort()
|
|
276
|
+
const requireVersion = (env: StepEnv, step: string): string => {
|
|
277
|
+
const appVersionId = env.attempt.appVersionId
|
|
278
|
+
if (appVersionId === undefined) throw new Error(`zerops: ${step} has no reserved app-version (reserve-version did not run)`)
|
|
279
|
+
return appVersionId
|
|
153
280
|
}
|
|
154
281
|
|
|
155
|
-
/**
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
* BEST EFFORT ON PURPOSE. Every caller runs this while already failing, so a cleanup that throws would
|
|
159
|
-
* replace the reason the deploy failed with the reason the cleanup did — which is what happened live,
|
|
160
|
-
* where a real `trigger-deploy` failure surfaced as `delete app-version failed (400)` and the cause was
|
|
161
|
-
* gone. The leftover version is visible on the platform; the lost diagnosis was not.
|
|
162
|
-
*/
|
|
163
|
-
const cleanupPreTriggerVersion = async (env: StepEnv, source: ZeropsRuntimeSource, appVersionId: string): Promise<void> => {
|
|
164
|
-
await cancelSourceBestEffort(env, source, appVersionId)
|
|
165
|
-
try {
|
|
166
|
-
await env.zerops.api.deleteAppVersion({ appVersionId, signal: AbortSignal.timeout(5000) })
|
|
167
|
-
} catch (error) {
|
|
168
|
-
env.log(` [warn] app-version ${appVersionId} was left behind: ${error instanceof Error ? error.message : 'unknown error'}`)
|
|
169
|
-
}
|
|
282
|
+
/** Best-effort: the caller is already giving up, so a failed cancellation must not replace the reason. */
|
|
283
|
+
const cancelBuildQuietly = async (env: StepEnv, appVersionId: string): Promise<void> => {
|
|
284
|
+
await env.zerops.api.cancelBuild({ appVersionId, signal: AbortSignal.timeout(5000) }).catch(() => {})
|
|
170
285
|
}
|
|
171
286
|
|
|
172
287
|
/**
|
|
@@ -232,22 +347,28 @@ const openLog = async (env: StepEnv): Promise<ZeropsLogAccess | null> => {
|
|
|
232
347
|
* Poll the trigger process and `/app-version` until the version reaches a terminal state, relaying the
|
|
233
348
|
* build log as it goes. The process is optional because the trigger response may omit its id.
|
|
234
349
|
*
|
|
235
|
-
* A
|
|
236
|
-
*
|
|
237
|
-
*
|
|
350
|
+
* A cancelled run asks Zerops to cancel the BUILD too: the platform is running the work, so abandoning the
|
|
351
|
+
* poll loop without telling it leaves a build container burning through the pipeline's hour. A lost lease
|
|
352
|
+
* gives no such permission (ADR-0044), so it stops without touching the build.
|
|
238
353
|
*/
|
|
239
354
|
const awaitVersion = async (env: StepEnv, appVersionId: string, processId: string | undefined): Promise<void> => {
|
|
240
355
|
const { zerops, signal, log } = env
|
|
241
356
|
const access = await openLog(env)
|
|
242
357
|
const seen = new Set<string>()
|
|
243
|
-
const deadline = Date.now() + POLL_TIMEOUT_MS
|
|
244
358
|
let previous: string | undefined
|
|
359
|
+
const timedOut = async (): Promise<never> => {
|
|
360
|
+
await cancelBuildQuietly(env, appVersionId)
|
|
361
|
+
throw new Error(`zerops: gave up waiting for app-version ${appVersionId} at the run deadline`)
|
|
362
|
+
}
|
|
245
363
|
|
|
246
364
|
for (;;) {
|
|
365
|
+
// Only a boundary that still holds the lease may cancel the build; a lost lease rejects it as lease_lost.
|
|
366
|
+
const crossed: unknown = await env.run.events.boundary().then(() => null, (error: unknown) => error)
|
|
367
|
+
if (isLeaseLoss(crossed)) throw crossed
|
|
247
368
|
if (signal.aborted) {
|
|
248
|
-
|
|
249
|
-
await
|
|
250
|
-
throw new
|
|
369
|
+
if (isLeaseLoss(signal.reason)) throw signal.reason
|
|
370
|
+
await cancelBuildQuietly(env, appVersionId)
|
|
371
|
+
throw new ProviderInvocationAbort('cancel_requested')
|
|
251
372
|
}
|
|
252
373
|
let process: ZeropsProcess | undefined
|
|
253
374
|
let version: ZeropsAppVersion
|
|
@@ -256,12 +377,18 @@ const awaitVersion = async (env: StepEnv, appVersionId: string, processId: strin
|
|
|
256
377
|
version = await zerops.api.getAppVersion({ appVersionId, signal })
|
|
257
378
|
} catch {
|
|
258
379
|
log(` ${appVersionId}: status observation unavailable; retrying`)
|
|
259
|
-
if (Date.now() >
|
|
260
|
-
throw new Error(`zerops: timed out after ${Math.round(POLL_TIMEOUT_MS / 60000)}m waiting for app-version ${appVersionId}`)
|
|
261
|
-
}
|
|
380
|
+
if (Date.now() > env.deadlineMs) await timedOut()
|
|
262
381
|
await zerops.sleep(POLL_INTERVAL_MS, signal)
|
|
263
382
|
continue
|
|
264
383
|
}
|
|
384
|
+
if (signal.aborted) continue
|
|
385
|
+
if (
|
|
386
|
+
version.id !== appVersionId || (version.serviceStackId !== undefined && version.serviceStackId !== env.run.target.serviceId)
|
|
387
|
+
|| (processId !== undefined && process?.id !== processId)
|
|
388
|
+
|| (process?.appVersionId !== undefined && process.appVersionId !== appVersionId)
|
|
389
|
+
) {
|
|
390
|
+
throw new Error('zerops: status observation has different coordinates')
|
|
391
|
+
}
|
|
265
392
|
if (version.status !== previous) {
|
|
266
393
|
log(` ${appVersionId}: ${version.status ?? 'unknown'}`)
|
|
267
394
|
previous = version.status
|
|
@@ -279,17 +406,52 @@ const awaitVersion = async (env: StepEnv, appVersionId: string, processId: strin
|
|
|
279
406
|
}
|
|
280
407
|
throw new Error(terminalFailure(version))
|
|
281
408
|
}
|
|
282
|
-
if (Date.now() >
|
|
283
|
-
throw new Error(`zerops: timed out after ${Math.round(POLL_TIMEOUT_MS / 60000)}m waiting for app-version ${appVersionId}`)
|
|
284
|
-
}
|
|
409
|
+
if (Date.now() > env.deadlineMs) await timedOut()
|
|
285
410
|
await zerops.sleep(POLL_INTERVAL_MS, signal)
|
|
286
411
|
}
|
|
287
412
|
}
|
|
288
413
|
|
|
289
|
-
/**
|
|
290
|
-
const
|
|
291
|
-
const {
|
|
414
|
+
/** Write the five service-level managed keys; `FABRIKA_RELEASE` is never a service variable (ADR-0044). */
|
|
415
|
+
const writeServiceEnv = async (env: StepEnv): Promise<void> => {
|
|
416
|
+
const { zerops, log, signal, managedValues } = env
|
|
417
|
+
const serviceId = env.run.target.serviceId
|
|
418
|
+
if (env.dryRun) {
|
|
419
|
+
const written = SERVICE_MANAGED_KEYS.filter((key) => managedValues[key] !== null)
|
|
420
|
+
const removed = SERVICE_MANAGED_KEYS.filter((key) => managedValues[key] === null)
|
|
421
|
+
log(` [dry-run] would write service variables on ${serviceId}: ${written.join(', ') || '(none)'}`)
|
|
422
|
+
log(` [dry-run] would remove service variables on ${serviceId} if present: ${[...removed, RELEASE_KEY].join(', ')}`)
|
|
423
|
+
return
|
|
424
|
+
}
|
|
425
|
+
assertRunning(signal)
|
|
426
|
+
// One read: every write below waits for its own user-data sync, so the build request never meets `userDataSyncRunning`.
|
|
427
|
+
const records = await zerops.api.listServiceEnv({ serviceId, signal })
|
|
428
|
+
for (const key of SERVICE_MANAGED_KEYS) {
|
|
429
|
+
const value = managedValues[key]
|
|
430
|
+
const existing = records.find((record) => record.key === key)
|
|
431
|
+
assertRunning(signal)
|
|
432
|
+
if (value !== null && existing?.content !== value) {
|
|
433
|
+
await zerops.api.putServiceEnv({ serviceId, key, value, signal })
|
|
434
|
+
log(` ${key}: updated`)
|
|
435
|
+
} else if (value === null && existing !== undefined) {
|
|
436
|
+
await zerops.api.deleteServiceEnv({ envId: existing.id, signal })
|
|
437
|
+
log(` ${key}: removed`)
|
|
438
|
+
} else {
|
|
439
|
+
log(` ${key}: unchanged`)
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const strayRelease = records.find((record) => record.key === RELEASE_KEY)
|
|
443
|
+
if (strayRelease !== undefined) {
|
|
444
|
+
assertRunning(signal)
|
|
445
|
+
await zerops.api.deleteServiceEnv({ envId: strayRelease.id, signal })
|
|
446
|
+
log(` ${RELEASE_KEY}: removed`)
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** Run one step's effect. Resolves on success, throws on failure (caught + recorded by `runSteps`). */
|
|
451
|
+
const runStep = async (spec: ZeropsStep, env: StepEnv): Promise<void> => {
|
|
452
|
+
const { run, zerops, compiled, log, signal, dryRun, attempt } = env
|
|
292
453
|
const { target, artifact } = run
|
|
454
|
+
await run.events.boundary()
|
|
293
455
|
|
|
294
456
|
switch (spec.kind) {
|
|
295
457
|
case 'apply-import': {
|
|
@@ -318,117 +480,109 @@ const runStep = async (spec: ZeropsJobSpec, env: StepEnv): Promise<void> => {
|
|
|
318
480
|
return
|
|
319
481
|
}
|
|
320
482
|
|
|
321
|
-
case '
|
|
483
|
+
case 'write-service-env': {
|
|
484
|
+
await writeServiceEnv(env)
|
|
485
|
+
return
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
case 'reserve-version': {
|
|
322
489
|
if (dryRun) {
|
|
323
|
-
log(` [dry-run] would
|
|
490
|
+
log(` [dry-run] would reserve an app version on service ${target.serviceId}`)
|
|
324
491
|
return
|
|
325
492
|
}
|
|
326
|
-
assertRunning(signal)
|
|
327
493
|
const source = requireSource(env)
|
|
328
494
|
await verifyZeropsArtifactSourceDescriptor(artifact.target.sourceDescriptor)
|
|
495
|
+
assertRunning(signal)
|
|
329
496
|
const version = await zerops.api.createAppVersion({ serviceId: target.serviceId, name: source.runtime.runId, signal })
|
|
330
|
-
|
|
331
|
-
|
|
497
|
+
attempt.appVersionId = version.id
|
|
498
|
+
attempt.uploadUrl = version.uploadUrl
|
|
499
|
+
const checkpoint: ZeropsRunState = { appVersionId: version.id, triggered: false }
|
|
500
|
+
await run.events.externalId(version.id, { ...checkpoint })
|
|
501
|
+
log(` reserved app-version ${version.id}`)
|
|
502
|
+
return
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
case 'upload-source': {
|
|
506
|
+
if (dryRun) {
|
|
507
|
+
log(' [dry-run] would upload the resolved repository snapshot to the reserved app version')
|
|
508
|
+
return
|
|
509
|
+
}
|
|
510
|
+
const source = requireSource(env)
|
|
511
|
+
const appVersionId = requireVersion(env, 'upload-source')
|
|
512
|
+
const uploadUrl = attempt.uploadUrl
|
|
513
|
+
if (uploadUrl === undefined) throw new Error('zerops: upload-source has no upload URL')
|
|
514
|
+
// The upload URL carries its own credential; it is checked here and never put in an error.
|
|
515
|
+
if (!isZeropsUploadDestination(uploadUrl)) throw new Error('zerops: the reserved app version has an unexpected upload destination')
|
|
516
|
+
assertRunning(signal)
|
|
517
|
+
const binding = source.runtime.privateBinding
|
|
518
|
+
const uploaded = await source.client.upload({
|
|
519
|
+
repository: source.runtime.repository,
|
|
520
|
+
commitSha: source.runtime.commitSha,
|
|
521
|
+
descriptorSha256: artifact.target.sourceDescriptor.sha256,
|
|
522
|
+
archive: {
|
|
523
|
+
kind: 'application',
|
|
524
|
+
...(binding === undefined ? {} : { privateBinding: { connectionId: binding.connectionId, installationId: binding.installationId } }),
|
|
525
|
+
},
|
|
526
|
+
uploadUrl,
|
|
527
|
+
signal,
|
|
528
|
+
})
|
|
529
|
+
if (
|
|
530
|
+
uploaded.commitSha !== source.runtime.commitSha
|
|
531
|
+
|| uploaded.descriptorSha256 !== artifact.target.sourceDescriptor.sha256
|
|
532
|
+
) {
|
|
533
|
+
throw new Error('zerops: source upload returned different coordinates')
|
|
534
|
+
}
|
|
535
|
+
log(` uploaded ${source.runtime.commitSha} to ${appVersionId}`)
|
|
536
|
+
return
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
case 'trigger-build': {
|
|
540
|
+
if (dryRun) {
|
|
541
|
+
log(' [dry-run] would trigger build+deploy with FABRIKA_RELEASE written into the execution descriptor')
|
|
542
|
+
return
|
|
543
|
+
}
|
|
544
|
+
const appVersionId = requireVersion(env, 'trigger-build')
|
|
545
|
+
await verifyZeropsArtifactSourceDescriptor(artifact.target.sourceDescriptor)
|
|
546
|
+
const descriptor = overlayRelease(artifact.target.sourceDescriptor.contents, artifact.target.zeropsSetup, env.managedValues.FABRIKA_RELEASE)
|
|
547
|
+
assertRunning(signal)
|
|
548
|
+
// Recorded BEFORE the request: from here on the version is settled only by polling it (ADR-0044).
|
|
549
|
+
const checkpoint: ZeropsRunState = { appVersionId, triggered: true }
|
|
550
|
+
await run.events.checkpoint({ ...checkpoint })
|
|
551
|
+
attempt.triggered = true
|
|
552
|
+
// A cancel from here on still sends the build, so `cancel()` cancels a real build instead of
|
|
553
|
+
// polling a version that would sit in UPLOADING until the deadline. Only a lost lease stops it.
|
|
554
|
+
const buildSignal = followLeaseLoss(signal)
|
|
555
|
+
buildSignal.throwIfAborted()
|
|
556
|
+
let process: ZeropsProcess
|
|
332
557
|
try {
|
|
333
|
-
await
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
...(source.runtime.githubInstallationId === undefined
|
|
340
|
-
? {}
|
|
341
|
-
: { githubInstallationId: source.runtime.githubInstallationId }),
|
|
342
|
-
uploadUrl: version.uploadUrl,
|
|
343
|
-
descriptor: {
|
|
344
|
-
path: artifact.target.sourceDescriptor.path,
|
|
345
|
-
sha256: artifact.target.sourceDescriptor.sha256,
|
|
346
|
-
},
|
|
347
|
-
signal,
|
|
348
|
-
}
|
|
349
|
-
const sourceTransport = env.sourceTransport
|
|
350
|
-
const binding = sourceTransport?.bindingForRun(source.runtime.runId)
|
|
351
|
-
if (binding !== undefined && source.runtime.githubInstallationId !== binding.installationId) {
|
|
352
|
-
throw new Error('zerops: source transport binding has different installation coordinates')
|
|
353
|
-
}
|
|
354
|
-
let uploaded: ZeropsSourceUploadResult
|
|
355
|
-
if (binding === undefined) {
|
|
356
|
-
uploaded = await source.client.upload(uploadInput)
|
|
357
|
-
} else {
|
|
358
|
-
if (sourceTransport === undefined) throw new Error('zerops: keyed source transport is unavailable')
|
|
359
|
-
uploaded = await sourceTransport.uploadV2({
|
|
360
|
-
...uploadInput,
|
|
361
|
-
privateBinding: { connectionId: binding.connectionId, installationId: binding.installationId },
|
|
362
|
-
})
|
|
363
|
-
}
|
|
364
|
-
if (
|
|
365
|
-
uploaded.runId !== source.runtime.runId
|
|
366
|
-
|| uploaded.appVersionId !== version.id
|
|
367
|
-
|| uploaded.commitSha !== source.runtime.commitSha
|
|
368
|
-
|| uploaded.descriptorSha256 !== artifact.target.sourceDescriptor.sha256
|
|
369
|
-
) {
|
|
370
|
-
throw new Error('zerops: source upload returned different coordinates')
|
|
371
|
-
}
|
|
372
|
-
await run.events.checkpoint(runState(version.id, 'source_uploaded'))
|
|
373
|
-
await verifyZeropsArtifactSourceDescriptor(artifact.target.sourceDescriptor)
|
|
374
|
-
await run.events.checkpoint(runState(version.id, 'build_trigger_requested'))
|
|
375
|
-
buildTriggerRequested = true
|
|
376
|
-
let process: ZeropsProcess
|
|
377
|
-
try {
|
|
378
|
-
process = await zerops.api.buildAndDeployAppVersion({
|
|
379
|
-
appVersionId: version.id,
|
|
380
|
-
zeropsYaml: artifact.target.sourceDescriptor.contents,
|
|
381
|
-
...(artifact.target.zeropsSetup === undefined ? {} : { zeropsYamlSetup: artifact.target.zeropsSetup }),
|
|
382
|
-
signal,
|
|
383
|
-
})
|
|
384
|
-
} catch (error) {
|
|
385
|
-
if (error instanceof ZeropsApiError && error.status >= 400 && error.status < 500) {
|
|
386
|
-
await cleanupPreTriggerVersion(env, source.runtime, version.id)
|
|
387
|
-
throw error
|
|
388
|
-
}
|
|
389
|
-
let observed: ZeropsAppVersion | undefined
|
|
390
|
-
try {
|
|
391
|
-
await zerops.sleep(BUILD_TRIGGER_CONSISTENCY_MS, signal)
|
|
392
|
-
observed = await zerops.api.getAppVersion({ appVersionId: version.id, signal })
|
|
393
|
-
} catch {
|
|
394
|
-
log(` ${version.id}: build-trigger observation unavailable; continuing to poll`)
|
|
395
|
-
return
|
|
396
|
-
}
|
|
397
|
-
if (observed.id === version.id && observed.status === 'UPLOADING') {
|
|
398
|
-
await cleanupPreTriggerVersion(env, source.runtime, version.id)
|
|
399
|
-
throw error
|
|
400
|
-
}
|
|
401
|
-
if (observed.id === version.id && observed.status !== undefined) {
|
|
402
|
-
await run.events.checkpoint(buildTriggeredState(version.id)).catch(() => {})
|
|
403
|
-
log(` recovered accepted build trigger for app-version ${version.id}`)
|
|
404
|
-
} else {
|
|
405
|
-
log(` ${version.id}: build-trigger state is not known yet; continuing to poll`)
|
|
406
|
-
}
|
|
407
|
-
return
|
|
408
|
-
}
|
|
409
|
-
state.processId = process.id
|
|
410
|
-
await run.events.checkpoint(buildTriggeredState(version.id, process.id)).catch(() => {})
|
|
411
|
-
log(` triggered: app-version ${version.id} (build+deploy is ONE platform-side operation)`)
|
|
558
|
+
process = await zerops.api.buildAndDeployAppVersion({
|
|
559
|
+
appVersionId,
|
|
560
|
+
zeropsYaml: descriptor.contents,
|
|
561
|
+
zeropsYamlSetup: descriptor.setup,
|
|
562
|
+
signal: buildSignal,
|
|
563
|
+
})
|
|
412
564
|
} catch (error) {
|
|
413
|
-
|
|
414
|
-
|
|
565
|
+
buildSignal.throwIfAborted()
|
|
566
|
+
// A 408 says the request timed out, not that the platform refused it.
|
|
567
|
+
if (error instanceof ZeropsApiError && error.status >= 400 && error.status < 500 && error.status !== 408) {
|
|
568
|
+
throw new Error(`zerops: build refused (${error.code === '' ? error.status : error.code})`)
|
|
415
569
|
}
|
|
416
|
-
|
|
570
|
+
log(' build request outcome unknown; polling the version')
|
|
571
|
+
return
|
|
417
572
|
}
|
|
573
|
+
attempt.processId = process.id
|
|
574
|
+
log(` triggered app-version ${appVersionId}`)
|
|
418
575
|
return
|
|
419
576
|
}
|
|
420
577
|
|
|
421
|
-
case 'await-
|
|
578
|
+
case 'await-version': {
|
|
422
579
|
if (dryRun) {
|
|
423
580
|
log(' [dry-run] would poll the pipeline process when available and /app-version until it is ACTIVE, relaying the build log')
|
|
424
581
|
return
|
|
425
582
|
}
|
|
426
|
-
const appVersionId =
|
|
427
|
-
if (appVersionId === undefined) {
|
|
428
|
-
throw new Error('zerops: await-deploy has no app-version to watch (trigger-deploy did not run)')
|
|
429
|
-
}
|
|
583
|
+
const appVersionId = requireVersion(env, 'await-version')
|
|
430
584
|
assertRunning(signal)
|
|
431
|
-
await awaitVersion(env, appVersionId,
|
|
585
|
+
await awaitVersion(env, appVersionId, attempt.processId)
|
|
432
586
|
return
|
|
433
587
|
}
|
|
434
588
|
|
|
@@ -463,61 +617,52 @@ const runStep = async (spec: ZeropsJobSpec, env: StepEnv): Promise<void> => {
|
|
|
463
617
|
}
|
|
464
618
|
|
|
465
619
|
/**
|
|
466
|
-
* Build a Zerops
|
|
620
|
+
* Build a Zerops deployer over a specific collaborator FACTORY. Production passes the real one; tests pass
|
|
467
621
|
* `() => fakes`, which is the whole testability property — one seam, all of this driver's effects.
|
|
468
622
|
*
|
|
469
623
|
* A factory rather than a bundle because the Zerops client is authenticated with a token that lives on the
|
|
470
624
|
* run's target, so it cannot be constructed once at module load the way Cloudflare's bundle is.
|
|
471
625
|
*/
|
|
472
|
-
export
|
|
473
|
-
|
|
474
|
-
/** Construct an independently testable Zerops provider against one collaborator factory. */
|
|
475
|
-
export const createZeropsProvider = (
|
|
626
|
+
export const createZeropsDeployer = (
|
|
476
627
|
collaborators: ZeropsCollaboratorFactory = defaultZeropsCollaborators,
|
|
477
|
-
|
|
478
|
-
):
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
},
|
|
519
|
-
})
|
|
520
|
-
},
|
|
521
|
-
})
|
|
522
|
-
|
|
523
|
-
export const zeropsProvider: ZeropsProvider = createZeropsProvider()
|
|
628
|
+
) =>
|
|
629
|
+
async (run: ZeropsDeployRun): Promise<ZeropsDeployResult> => {
|
|
630
|
+
if (run.artifact.app.id !== run.appId) {
|
|
631
|
+
throw new Error(`zerops: artifact app drift: expected \`${run.appId}\`, got \`${run.artifact.app.id}\``)
|
|
632
|
+
}
|
|
633
|
+
if (run.artifact.app.env !== run.env) {
|
|
634
|
+
throw new Error(`zerops: artifact environment drift: expected \`${run.env}\`, got \`${run.artifact.app.env}\``)
|
|
635
|
+
}
|
|
636
|
+
assertAuthoredManagedInputs({ variableNames: Object.keys(run.vars), secretNames: Object.keys(run.secrets) })
|
|
637
|
+
const managedValues = parseManagedValues(run.managedEnvironment)
|
|
638
|
+
const hostnames = manifestServiceHostnames(run.artifact)
|
|
639
|
+
const document = interpolateManifest(run.artifact.target.importDocument, run.artifact.app.pipeline.vars, run.vars, hostnames)
|
|
640
|
+
const compiled = { yaml: renderYaml(document), hostnames }
|
|
641
|
+
resolveDeployHostname(run.artifact)
|
|
642
|
+
const zerops = collaborators(run.target)
|
|
643
|
+
const env: StepEnv = {
|
|
644
|
+
run,
|
|
645
|
+
zerops,
|
|
646
|
+
compiled,
|
|
647
|
+
log: run.events.log,
|
|
648
|
+
signal: run.signal,
|
|
649
|
+
dryRun: run.dryRun,
|
|
650
|
+
attempt: { triggered: false },
|
|
651
|
+
managedValues,
|
|
652
|
+
deadlineMs: Date.now() + ZEROPS_VERSION_DEADLINE_MS,
|
|
653
|
+
}
|
|
654
|
+
const steps = buildZeropsPlan(run.artifact, run.target)
|
|
655
|
+
const options = { appId: run.appId, env: run.env, dryRun: run.dryRun, signal: run.signal, log: run.events.log }
|
|
656
|
+
const result = await runSteps(steps, options, (step) => runStep(step, env))
|
|
657
|
+
const { appVersionId, triggered } = env.attempt
|
|
658
|
+
// A cancelled run leaves the version to `cancel()`; a lost lease leaves it to the next owner.
|
|
659
|
+
if (result.status === 'failed' && appVersionId !== undefined && !triggered && !run.signal.aborted) {
|
|
660
|
+
try {
|
|
661
|
+
await deleteVersion(zerops.api, appVersionId, run.signal)
|
|
662
|
+
run.events.log(` deleted app-version ${appVersionId}`)
|
|
663
|
+
} catch {
|
|
664
|
+
run.events.log(` [warn] app-version ${appVersionId} could not be deleted; it was never built`)
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return { appId: run.appId, env: run.env, ...result }
|
|
668
|
+
}
|