@wyxos/zephyr 0.3.4 → 0.4.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/README.md +71 -0
- package/bin/zephyr.mjs +15 -23
- package/package.json +1 -1
- package/src/application/configuration/select-deployment-target.mjs +94 -9
- package/src/application/deploy/build-remote-deployment-plan.mjs +68 -20
- package/src/application/deploy/execute-remote-deployment-plan.mjs +2 -2
- package/src/application/deploy/resolve-pending-snapshot.mjs +21 -1
- package/src/application/deploy/run-deployment.mjs +114 -29
- package/src/application/release/release-node-package.mjs +64 -17
- package/src/application/release/release-packagist-package.mjs +54 -19
- package/src/cli/options.mjs +122 -0
- package/src/config/project.mjs +32 -1
- package/src/config/servers.mjs +32 -2
- package/src/dependency-scanner.mjs +11 -1
- package/src/deploy/locks.mjs +40 -24
- package/src/main.mjs +199 -71
- package/src/project/bootstrap.mjs +10 -1
- package/src/release/shared.mjs +15 -5
- package/src/release-node.mjs +27 -17
- package/src/release-packagist.mjs +26 -15
- package/src/runtime/app-context.mjs +33 -6
- package/src/runtime/errors.mjs +46 -0
- package/src/runtime/local-command.mjs +12 -3
- package/src/runtime/prompt.mjs +40 -2
- package/src/utils/output.mjs +45 -0
package/README.md
CHANGED
|
@@ -43,6 +43,15 @@ zephyr
|
|
|
43
43
|
# Deploy an app and bump the local npm package version first
|
|
44
44
|
zephyr minor
|
|
45
45
|
|
|
46
|
+
# Deploy a configured app non-interactively
|
|
47
|
+
zephyr --non-interactive --preset wyxos-release --maintenance off
|
|
48
|
+
|
|
49
|
+
# Resume a pending non-interactive deployment
|
|
50
|
+
zephyr --non-interactive --preset wyxos-release --resume-pending --maintenance off
|
|
51
|
+
|
|
52
|
+
# Emit NDJSON events for automation or agent tooling
|
|
53
|
+
zephyr --non-interactive --json --preset wyxos-release --maintenance on
|
|
54
|
+
|
|
46
55
|
# Release a Node/Vue package (defaults to a patch bump)
|
|
47
56
|
zephyr --type node
|
|
48
57
|
|
|
@@ -55,6 +64,67 @@ zephyr --type packagist patch
|
|
|
55
64
|
|
|
56
65
|
When `--type node` or `--type vue` is used without a bump argument, Zephyr defaults to `patch`.
|
|
57
66
|
|
|
67
|
+
## Interactive and Non-Interactive Modes
|
|
68
|
+
|
|
69
|
+
Interactive mode remains the default and is the best fit for first-time setup, config repair, and one-off deployments.
|
|
70
|
+
|
|
71
|
+
Non-interactive mode is strict and is intended for already-configured projects:
|
|
72
|
+
|
|
73
|
+
- `--non-interactive` fails instead of prompting
|
|
74
|
+
- app deployments require `--preset <name>`
|
|
75
|
+
- Laravel app deployments require `--maintenance on|off` unless resuming a saved snapshot that already contains the choice
|
|
76
|
+
- pending deployment snapshots require either `--resume-pending` or `--discard-pending`
|
|
77
|
+
- stale remote locks are never auto-removed in non-interactive mode
|
|
78
|
+
- `--json` is only supported together with `--non-interactive`
|
|
79
|
+
|
|
80
|
+
If Zephyr would normally prompt to:
|
|
81
|
+
|
|
82
|
+
- create or repair config
|
|
83
|
+
- save a preset
|
|
84
|
+
- install the `release` script
|
|
85
|
+
- confirm local path dependency updates
|
|
86
|
+
- resolve a stale remote lock
|
|
87
|
+
|
|
88
|
+
then non-interactive mode stops immediately with a clear error instead.
|
|
89
|
+
|
|
90
|
+
## AI Agents and Automation
|
|
91
|
+
|
|
92
|
+
Zephyr can be used safely by Codex, CI jobs, or other automation once configuration is already in place.
|
|
93
|
+
|
|
94
|
+
Recommended pattern for app deployments:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
zephyr --non-interactive --json --preset wyxos-release --maintenance off
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Recommended pattern for package releases:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
zephyr --type node --non-interactive --json minor
|
|
104
|
+
zephyr --type packagist --non-interactive --json patch
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
In `--json` mode Zephyr emits NDJSON events on `stdout` with a stable shape:
|
|
108
|
+
|
|
109
|
+
- `run_started`
|
|
110
|
+
- `log`
|
|
111
|
+
- `prompt_required`
|
|
112
|
+
- `run_completed`
|
|
113
|
+
- `run_failed`
|
|
114
|
+
|
|
115
|
+
Each event includes:
|
|
116
|
+
|
|
117
|
+
- `event`
|
|
118
|
+
- `timestamp`
|
|
119
|
+
- `workflow`
|
|
120
|
+
- `message`
|
|
121
|
+
- `level` where relevant
|
|
122
|
+
- `data`
|
|
123
|
+
|
|
124
|
+
`run_failed` also includes a stable `code` field for automation checks.
|
|
125
|
+
|
|
126
|
+
In `--json` mode, Zephyr reserves `stdout` for NDJSON events and routes inherited local command output to `stderr` so agent parsers do not get corrupted.
|
|
127
|
+
|
|
58
128
|
On a first run inside a project with `package.json`, Zephyr can:
|
|
59
129
|
- add `.zephyr/` to `.gitignore`
|
|
60
130
|
- add a `release` script that runs `npx @wyxos/zephyr@latest`
|
|
@@ -79,6 +149,7 @@ npm run release
|
|
|
79
149
|
- `npm run release` is the recommended app/package entrypoint once the release script has been installed.
|
|
80
150
|
- For `--type node` workflows, Zephyr runs your project's `lint` script when present.
|
|
81
151
|
- For `--type node` workflows, Zephyr runs `test:run` or `test` when present.
|
|
152
|
+
- For non-interactive app deploys, use a saved preset name instead of relying on prompt fallback.
|
|
82
153
|
|
|
83
154
|
## Features
|
|
84
155
|
|
package/bin/zephyr.mjs
CHANGED
|
@@ -1,30 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import process from 'node:process'
|
|
3
|
-
import { Command } from 'commander'
|
|
4
|
-
import chalk from 'chalk'
|
|
5
|
-
import { main } from '../src/main.mjs'
|
|
6
|
-
import { createChalkLogger } from '../src/utils/output.mjs'
|
|
7
3
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
program
|
|
13
|
-
.name('zephyr')
|
|
14
|
-
.description('A streamlined deployment tool for web applications with intelligent Laravel project detection')
|
|
15
|
-
.option('--type <type>', 'Workflow type (node|vue|packagist). Omit for normal app deployments.')
|
|
16
|
-
.argument(
|
|
17
|
-
'[version]',
|
|
18
|
-
'Version or npm bump type for deployments (e.g. 1.2.3, patch, minor, major). --type node/vue/packagist workflows accept bump types only and default to patch.'
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
program.parse(process.argv)
|
|
22
|
-
const options = program.opts()
|
|
23
|
-
const versionArg = program.args[0] ?? null
|
|
4
|
+
import {parseCliOptions} from '../src/cli/options.mjs'
|
|
5
|
+
import {main} from '../src/main.mjs'
|
|
6
|
+
import {writeStderrLine} from '../src/utils/output.mjs'
|
|
24
7
|
|
|
8
|
+
let options
|
|
25
9
|
try {
|
|
26
|
-
|
|
10
|
+
options = parseCliOptions()
|
|
27
11
|
} catch (error) {
|
|
28
|
-
|
|
29
|
-
|
|
12
|
+
writeStderrLine(error.message)
|
|
13
|
+
process.exitCode = 1
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (options) {
|
|
17
|
+
try {
|
|
18
|
+
await main(options)
|
|
19
|
+
} catch {
|
|
20
|
+
process.exitCode = 1
|
|
21
|
+
}
|
|
30
22
|
}
|
package/package.json
CHANGED
|
@@ -1,22 +1,82 @@
|
|
|
1
1
|
import {writeStdoutLine} from '../../utils/output.mjs'
|
|
2
2
|
import {loadServers} from '../../config/servers.mjs'
|
|
3
3
|
import {loadProjectConfig, removePreset, saveProjectConfig} from '../../config/project.mjs'
|
|
4
|
+
import {ZephyrError} from '../../runtime/errors.mjs'
|
|
5
|
+
|
|
6
|
+
function findPresetByName(projectConfig, presetName) {
|
|
7
|
+
const presets = projectConfig?.presets ?? []
|
|
8
|
+
return presets.find((entry) => entry?.name === presetName) ?? null
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function resolvePresetNonInteractive(projectConfig, servers, preset, presetName) {
|
|
12
|
+
if (!preset) {
|
|
13
|
+
throw new ZephyrError(
|
|
14
|
+
`Zephyr cannot run non-interactively because preset "${presetName}" was not found in .zephyr/config.json.`,
|
|
15
|
+
{code: 'ZEPHYR_PRESET_NOT_FOUND'}
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!preset.appId) {
|
|
20
|
+
throw new ZephyrError(
|
|
21
|
+
`Zephyr cannot run non-interactively because preset "${preset.name || presetName}" uses a legacy or invalid format. Rerun interactively once to repair .zephyr/config.json.`,
|
|
22
|
+
{code: 'ZEPHYR_PRESET_REPAIR_REQUIRED'}
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const apps = projectConfig?.apps ?? []
|
|
27
|
+
const appConfig = apps.find((app) => app.id === preset.appId)
|
|
28
|
+
if (!appConfig) {
|
|
29
|
+
throw new ZephyrError(
|
|
30
|
+
`Zephyr cannot run non-interactively because preset "${preset.name || presetName}" references an application that no longer exists.`,
|
|
31
|
+
{code: 'ZEPHYR_PRESET_INVALID'}
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const server = servers.find((entry) => entry.id === appConfig.serverId || entry.serverName === appConfig.serverName)
|
|
36
|
+
if (!server) {
|
|
37
|
+
throw new ZephyrError(
|
|
38
|
+
`Zephyr cannot run non-interactively because preset "${preset.name || presetName}" references a server that no longer exists.`,
|
|
39
|
+
{code: 'ZEPHYR_PRESET_INVALID'}
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
server,
|
|
45
|
+
appConfig,
|
|
46
|
+
branch: preset.branch || appConfig.branch
|
|
47
|
+
}
|
|
48
|
+
}
|
|
4
49
|
|
|
5
50
|
export async function selectDeploymentTarget(rootDir, {
|
|
6
51
|
configurationService,
|
|
7
52
|
runPrompt,
|
|
8
53
|
logProcessing,
|
|
9
54
|
logSuccess,
|
|
10
|
-
logWarning
|
|
55
|
+
logWarning,
|
|
56
|
+
emitEvent,
|
|
57
|
+
executionMode = {}
|
|
11
58
|
} = {}) {
|
|
12
|
-
const
|
|
13
|
-
const
|
|
59
|
+
const nonInteractive = executionMode?.interactive === false
|
|
60
|
+
const servers = await loadServers({
|
|
61
|
+
logSuccess,
|
|
62
|
+
logWarning,
|
|
63
|
+
strict: nonInteractive,
|
|
64
|
+
allowMigration: !nonInteractive
|
|
65
|
+
})
|
|
66
|
+
const projectConfig = await loadProjectConfig(rootDir, servers, {
|
|
67
|
+
logSuccess,
|
|
68
|
+
logWarning,
|
|
69
|
+
strict: nonInteractive,
|
|
70
|
+
allowMigration: !nonInteractive
|
|
71
|
+
})
|
|
14
72
|
|
|
15
73
|
let server = null
|
|
16
74
|
let appConfig = null
|
|
17
75
|
let isCreatingNewPreset = false
|
|
18
76
|
|
|
19
|
-
const preset =
|
|
77
|
+
const preset = nonInteractive
|
|
78
|
+
? findPresetByName(projectConfig, executionMode.presetName)
|
|
79
|
+
: await configurationService.selectPreset(projectConfig, servers)
|
|
20
80
|
|
|
21
81
|
const removeInvalidPreset = async () => {
|
|
22
82
|
if (!preset || preset === 'create') {
|
|
@@ -34,7 +94,15 @@ export async function selectDeploymentTarget(rootDir, {
|
|
|
34
94
|
isCreatingNewPreset = true
|
|
35
95
|
}
|
|
36
96
|
|
|
37
|
-
if (
|
|
97
|
+
if (nonInteractive) {
|
|
98
|
+
const resolved = resolvePresetNonInteractive(projectConfig, servers, preset, executionMode.presetName)
|
|
99
|
+
server = resolved.server
|
|
100
|
+
appConfig = resolved.appConfig
|
|
101
|
+
appConfig = {
|
|
102
|
+
...appConfig,
|
|
103
|
+
branch: resolved.branch
|
|
104
|
+
}
|
|
105
|
+
} else if (preset === 'create') {
|
|
38
106
|
isCreatingNewPreset = true
|
|
39
107
|
server = await configurationService.selectServer(servers)
|
|
40
108
|
appConfig = await configurationService.selectApp(projectConfig, server, rootDir)
|
|
@@ -103,7 +171,16 @@ export async function selectDeploymentTarget(rootDir, {
|
|
|
103
171
|
appConfig = await configurationService.selectApp(projectConfig, server, rootDir)
|
|
104
172
|
}
|
|
105
173
|
|
|
106
|
-
|
|
174
|
+
if (nonInteractive && (!appConfig?.sshUser || !appConfig?.sshKey)) {
|
|
175
|
+
throw new ZephyrError(
|
|
176
|
+
`Zephyr cannot run non-interactively because preset "${preset?.name || executionMode.presetName}" is missing SSH details.`,
|
|
177
|
+
{code: 'ZEPHYR_SSH_DETAILS_REQUIRED'}
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const updated = nonInteractive
|
|
182
|
+
? false
|
|
183
|
+
: await configurationService.ensureSshDetails(appConfig, rootDir)
|
|
107
184
|
|
|
108
185
|
if (updated) {
|
|
109
186
|
await saveProjectConfig(rootDir, projectConfig)
|
|
@@ -119,10 +196,18 @@ export async function selectDeploymentTarget(rootDir, {
|
|
|
119
196
|
sshKey: appConfig.sshKey
|
|
120
197
|
}
|
|
121
198
|
|
|
122
|
-
|
|
123
|
-
|
|
199
|
+
if (typeof emitEvent === 'function' && executionMode?.json) {
|
|
200
|
+
emitEvent('log', {
|
|
201
|
+
level: 'processing',
|
|
202
|
+
message: 'Selected deployment target.',
|
|
203
|
+
data: {deploymentConfig}
|
|
204
|
+
})
|
|
205
|
+
} else {
|
|
206
|
+
logProcessing?.('\nSelected deployment target:')
|
|
207
|
+
writeStdoutLine(JSON.stringify(deploymentConfig, null, 2))
|
|
208
|
+
}
|
|
124
209
|
|
|
125
|
-
if (isCreatingNewPreset || !preset) {
|
|
210
|
+
if (!nonInteractive && (isCreatingNewPreset || !preset)) {
|
|
126
211
|
const {presetName} = await runPrompt([
|
|
127
212
|
{
|
|
128
213
|
type: 'input',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {findPhpBinary} from '../../infrastructure/php/version.mjs'
|
|
2
|
+
import {ZephyrError} from '../../runtime/errors.mjs'
|
|
2
3
|
import {planLaravelDeploymentTasks} from './plan-laravel-deployment-tasks.mjs'
|
|
3
4
|
|
|
4
5
|
const PRERENDERED_MAINTENANCE_VIEW = 'errors::503'
|
|
@@ -241,7 +242,8 @@ function createMaintenanceModePlan({
|
|
|
241
242
|
async function resolveMaintenanceMode({
|
|
242
243
|
snapshot,
|
|
243
244
|
remoteIsLaravel,
|
|
244
|
-
runPrompt
|
|
245
|
+
runPrompt,
|
|
246
|
+
executionMode = {}
|
|
245
247
|
} = {}) {
|
|
246
248
|
if (!remoteIsLaravel) {
|
|
247
249
|
return false
|
|
@@ -251,6 +253,17 @@ async function resolveMaintenanceMode({
|
|
|
251
253
|
return snapshot.maintenanceModeEnabled
|
|
252
254
|
}
|
|
253
255
|
|
|
256
|
+
if (executionMode?.interactive === false) {
|
|
257
|
+
if (typeof executionMode.maintenanceMode !== 'boolean') {
|
|
258
|
+
throw new ZephyrError(
|
|
259
|
+
'Zephyr cannot run this Laravel deployment non-interactively without an explicit maintenance-mode decision. Pass --maintenance on or --maintenance off.',
|
|
260
|
+
{code: 'ZEPHYR_MAINTENANCE_FLAG_REQUIRED'}
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return executionMode.maintenanceMode
|
|
265
|
+
}
|
|
266
|
+
|
|
254
267
|
if (typeof runPrompt !== 'function') {
|
|
255
268
|
return false
|
|
256
269
|
}
|
|
@@ -325,10 +338,43 @@ async function resolveMaintenanceModePlan({
|
|
|
325
338
|
})
|
|
326
339
|
}
|
|
327
340
|
|
|
341
|
+
export async function resolveRemoteDeploymentState({
|
|
342
|
+
snapshot,
|
|
343
|
+
executionMode = {},
|
|
344
|
+
ssh,
|
|
345
|
+
remoteCwd,
|
|
346
|
+
runPrompt,
|
|
347
|
+
logSuccess,
|
|
348
|
+
logWarning
|
|
349
|
+
} = {}) {
|
|
350
|
+
const remoteIsLaravel = await detectRemoteLaravelProject(ssh, remoteCwd)
|
|
351
|
+
|
|
352
|
+
if (remoteIsLaravel) {
|
|
353
|
+
logSuccess?.('Laravel project detected.')
|
|
354
|
+
} else {
|
|
355
|
+
logWarning?.('Laravel project not detected; skipping Laravel-specific maintenance tasks.')
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const maintenanceModeEnabled = await resolveMaintenanceMode({
|
|
359
|
+
snapshot,
|
|
360
|
+
remoteIsLaravel,
|
|
361
|
+
runPrompt,
|
|
362
|
+
executionMode
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
remoteIsLaravel,
|
|
367
|
+
maintenanceModeEnabled
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
328
371
|
export async function buildRemoteDeploymentPlan({
|
|
329
372
|
config,
|
|
330
373
|
snapshot = null,
|
|
331
374
|
requiredPhpVersion = null,
|
|
375
|
+
executionMode = {},
|
|
376
|
+
remoteIsLaravel = null,
|
|
377
|
+
maintenanceModeEnabled = null,
|
|
332
378
|
ssh,
|
|
333
379
|
remoteCwd,
|
|
334
380
|
executeRemote,
|
|
@@ -337,18 +383,26 @@ export async function buildRemoteDeploymentPlan({
|
|
|
337
383
|
logWarning,
|
|
338
384
|
runPrompt
|
|
339
385
|
} = {}) {
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
386
|
+
const remoteState = typeof remoteIsLaravel === 'boolean' &&
|
|
387
|
+
(remoteIsLaravel === false || typeof maintenanceModeEnabled === 'boolean')
|
|
388
|
+
? {
|
|
389
|
+
remoteIsLaravel,
|
|
390
|
+
maintenanceModeEnabled: remoteIsLaravel ? maintenanceModeEnabled : false
|
|
391
|
+
}
|
|
392
|
+
: await resolveRemoteDeploymentState({
|
|
393
|
+
snapshot,
|
|
394
|
+
executionMode,
|
|
395
|
+
ssh,
|
|
396
|
+
remoteCwd,
|
|
397
|
+
runPrompt,
|
|
398
|
+
logSuccess,
|
|
399
|
+
logWarning
|
|
400
|
+
})
|
|
347
401
|
|
|
348
402
|
const changedFiles = await collectChangedFiles({
|
|
349
403
|
config,
|
|
350
404
|
snapshot,
|
|
351
|
-
remoteIsLaravel,
|
|
405
|
+
remoteIsLaravel: remoteState.remoteIsLaravel,
|
|
352
406
|
executeRemote,
|
|
353
407
|
logProcessing
|
|
354
408
|
})
|
|
@@ -356,7 +410,7 @@ export async function buildRemoteDeploymentPlan({
|
|
|
356
410
|
const horizonConfigured = await detectHorizonConfiguration({
|
|
357
411
|
ssh,
|
|
358
412
|
remoteCwd,
|
|
359
|
-
remoteIsLaravel,
|
|
413
|
+
remoteIsLaravel: remoteState.remoteIsLaravel,
|
|
360
414
|
changedFiles
|
|
361
415
|
})
|
|
362
416
|
|
|
@@ -368,17 +422,11 @@ export async function buildRemoteDeploymentPlan({
|
|
|
368
422
|
logWarning
|
|
369
423
|
})
|
|
370
424
|
|
|
371
|
-
const maintenanceModeEnabled = await resolveMaintenanceMode({
|
|
372
|
-
snapshot,
|
|
373
|
-
remoteIsLaravel,
|
|
374
|
-
runPrompt
|
|
375
|
-
})
|
|
376
|
-
|
|
377
425
|
const maintenanceModePlan = await resolveMaintenanceModePlan({
|
|
378
426
|
snapshot,
|
|
379
|
-
remoteIsLaravel,
|
|
427
|
+
remoteIsLaravel: remoteState.remoteIsLaravel,
|
|
380
428
|
remoteCwd,
|
|
381
|
-
maintenanceModeEnabled,
|
|
429
|
+
maintenanceModeEnabled: remoteState.maintenanceModeEnabled,
|
|
382
430
|
phpCommand,
|
|
383
431
|
ssh,
|
|
384
432
|
executeRemote,
|
|
@@ -388,7 +436,7 @@ export async function buildRemoteDeploymentPlan({
|
|
|
388
436
|
|
|
389
437
|
const steps = planLaravelDeploymentTasks({
|
|
390
438
|
branch: config.branch,
|
|
391
|
-
isLaravel: remoteIsLaravel,
|
|
439
|
+
isLaravel: remoteState.remoteIsLaravel,
|
|
392
440
|
changedFiles,
|
|
393
441
|
horizonConfigured,
|
|
394
442
|
phpCommand,
|
|
@@ -423,7 +471,7 @@ export async function buildRemoteDeploymentPlan({
|
|
|
423
471
|
}
|
|
424
472
|
|
|
425
473
|
return {
|
|
426
|
-
remoteIsLaravel,
|
|
474
|
+
remoteIsLaravel: remoteState.remoteIsLaravel,
|
|
427
475
|
changedFiles,
|
|
428
476
|
horizonConfigured,
|
|
429
477
|
phpCommand,
|
|
@@ -46,7 +46,7 @@ export async function executeRemoteDeploymentPlan({
|
|
|
46
46
|
usefulSteps,
|
|
47
47
|
pendingSnapshot = null,
|
|
48
48
|
logProcessing,
|
|
49
|
-
executionState =
|
|
49
|
+
executionState = undefined
|
|
50
50
|
} = {}) {
|
|
51
51
|
if (usefulSteps && pendingSnapshot) {
|
|
52
52
|
await persistPendingSnapshot(rootDir, pendingSnapshot, executeRemote)
|
|
@@ -73,4 +73,4 @@ export async function executeRemoteDeploymentPlan({
|
|
|
73
73
|
await clearPendingTasksSnapshot(rootDir)
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
|
-
}
|
|
76
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import {clearPendingTasksSnapshot, loadPendingTasksSnapshot} from '../../deploy/snapshots.mjs'
|
|
2
|
+
import {ZephyrError} from '../../runtime/errors.mjs'
|
|
2
3
|
|
|
3
4
|
export async function resolvePendingSnapshot(rootDir, deploymentConfig, {
|
|
4
5
|
runPrompt,
|
|
5
6
|
logProcessing,
|
|
6
|
-
logWarning
|
|
7
|
+
logWarning,
|
|
8
|
+
executionMode = {}
|
|
7
9
|
} = {}) {
|
|
8
10
|
const existingSnapshot = await loadPendingTasksSnapshot(rootDir)
|
|
9
11
|
|
|
@@ -25,6 +27,24 @@ export async function resolvePendingSnapshot(rootDir, deploymentConfig, {
|
|
|
25
27
|
messageLines.push(`Tasks: ${existingSnapshot.taskLabels.join(', ')}`)
|
|
26
28
|
}
|
|
27
29
|
|
|
30
|
+
if (executionMode?.interactive === false) {
|
|
31
|
+
if (!executionMode.resumePending && !executionMode.discardPending) {
|
|
32
|
+
throw new ZephyrError(
|
|
33
|
+
'Zephyr found a pending deployment snapshot, but non-interactive mode requires either --resume-pending or --discard-pending.',
|
|
34
|
+
{code: 'ZEPHYR_PENDING_SNAPSHOT_ACTION_REQUIRED'}
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (executionMode.resumePending) {
|
|
39
|
+
logProcessing?.('Resuming deployment using saved task snapshot...')
|
|
40
|
+
return existingSnapshot
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
await clearPendingTasksSnapshot(rootDir)
|
|
44
|
+
logWarning?.('Discarded pending deployment snapshot.')
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
|
|
28
48
|
const {resumePendingTasks} = await runPrompt([
|
|
29
49
|
{
|
|
30
50
|
type: 'confirm',
|