@wyxos/zephyr 0.3.4 → 0.4.0
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 +17 -2
- 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 +36 -7
- 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
|
}
|
|
@@ -329,6 +342,7 @@ export async function buildRemoteDeploymentPlan({
|
|
|
329
342
|
config,
|
|
330
343
|
snapshot = null,
|
|
331
344
|
requiredPhpVersion = null,
|
|
345
|
+
executionMode = {},
|
|
332
346
|
ssh,
|
|
333
347
|
remoteCwd,
|
|
334
348
|
executeRemote,
|
|
@@ -371,7 +385,8 @@ export async function buildRemoteDeploymentPlan({
|
|
|
371
385
|
const maintenanceModeEnabled = await resolveMaintenanceMode({
|
|
372
386
|
snapshot,
|
|
373
387
|
remoteIsLaravel,
|
|
374
|
-
runPrompt
|
|
388
|
+
runPrompt,
|
|
389
|
+
executionMode
|
|
375
390
|
})
|
|
376
391
|
|
|
377
392
|
const maintenanceModePlan = await resolveMaintenanceModePlan({
|
|
@@ -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',
|
|
@@ -26,7 +26,9 @@ async function maybeRecoverLaravelMaintenanceMode({
|
|
|
26
26
|
executionState,
|
|
27
27
|
executeRemote,
|
|
28
28
|
runPrompt,
|
|
29
|
-
|
|
29
|
+
logProcessing,
|
|
30
|
+
logWarning,
|
|
31
|
+
executionMode = {}
|
|
30
32
|
} = {}) {
|
|
31
33
|
if (!remotePlan?.remoteIsLaravel || !remotePlan?.maintenanceModeEnabled) {
|
|
32
34
|
return
|
|
@@ -36,12 +38,27 @@ async function maybeRecoverLaravelMaintenanceMode({
|
|
|
36
38
|
return
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
if (typeof
|
|
41
|
+
if (typeof executeRemote !== 'function') {
|
|
40
42
|
logWarning?.('Deployment failed while Laravel maintenance mode may still be enabled.')
|
|
41
43
|
return
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
try {
|
|
47
|
+
if (executionMode?.interactive === false) {
|
|
48
|
+
logProcessing?.('Deployment failed after Laravel maintenance mode was enabled. Running `artisan up` automatically...')
|
|
49
|
+
await executeRemote(
|
|
50
|
+
'Disable Laravel maintenance mode',
|
|
51
|
+
remotePlan.maintenanceUpCommand ?? `${remotePlan.phpCommand} artisan up`
|
|
52
|
+
)
|
|
53
|
+
executionState.exitedMaintenanceMode = true
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (typeof runPrompt !== 'function') {
|
|
58
|
+
logWarning?.('Deployment failed while Laravel maintenance mode may still be enabled.')
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
45
62
|
const answers = await runPrompt([
|
|
46
63
|
{
|
|
47
64
|
type: 'confirm',
|
|
@@ -82,7 +99,8 @@ export async function runDeployment(config, options = {}) {
|
|
|
82
99
|
logError,
|
|
83
100
|
runPrompt,
|
|
84
101
|
createSshClient,
|
|
85
|
-
runCommand
|
|
102
|
+
runCommand,
|
|
103
|
+
executionMode
|
|
86
104
|
} = context
|
|
87
105
|
|
|
88
106
|
await cleanupOldLogs(rootDir)
|
|
@@ -126,7 +144,11 @@ export async function runDeployment(config, options = {}) {
|
|
|
126
144
|
remoteCwd = resolveRemotePath(config.projectPath, remoteHome)
|
|
127
145
|
|
|
128
146
|
logProcessing('Connection established. Acquiring deployment lock on server...')
|
|
129
|
-
await acquireRemoteLock(ssh, remoteCwd, rootDir, {
|
|
147
|
+
await acquireRemoteLock(ssh, remoteCwd, rootDir, {
|
|
148
|
+
runPrompt,
|
|
149
|
+
logWarning,
|
|
150
|
+
interactive: executionMode?.interactive !== false
|
|
151
|
+
})
|
|
130
152
|
lockAcquired = true
|
|
131
153
|
logProcessing(`Lock acquired. Running deployment commands in ${remoteCwd}...`)
|
|
132
154
|
|
|
@@ -145,6 +167,7 @@ export async function runDeployment(config, options = {}) {
|
|
|
145
167
|
snapshot,
|
|
146
168
|
rootDir,
|
|
147
169
|
requiredPhpVersion,
|
|
170
|
+
executionMode,
|
|
148
171
|
ssh,
|
|
149
172
|
remoteCwd,
|
|
150
173
|
executeRemote,
|
|
@@ -179,12 +202,18 @@ export async function runDeployment(config, options = {}) {
|
|
|
179
202
|
executionState,
|
|
180
203
|
executeRemote,
|
|
181
204
|
runPrompt,
|
|
182
|
-
|
|
205
|
+
logProcessing,
|
|
206
|
+
logWarning,
|
|
207
|
+
executionMode
|
|
183
208
|
})
|
|
184
209
|
|
|
185
210
|
if (lockAcquired && ssh && remoteCwd) {
|
|
186
211
|
try {
|
|
187
|
-
await compareLocksAndPrompt(rootDir, ssh, remoteCwd, {
|
|
212
|
+
await compareLocksAndPrompt(rootDir, ssh, remoteCwd, {
|
|
213
|
+
runPrompt,
|
|
214
|
+
logWarning,
|
|
215
|
+
interactive: executionMode?.interactive !== false
|
|
216
|
+
})
|
|
188
217
|
} catch {
|
|
189
218
|
// Ignore lock comparison errors during error handling
|
|
190
219
|
}
|
|
@@ -206,4 +235,4 @@ export async function runDeployment(config, options = {}) {
|
|
|
206
235
|
ssh.dispose()
|
|
207
236
|
}
|
|
208
237
|
}
|
|
209
|
-
}
|
|
238
|
+
}
|
|
@@ -8,7 +8,7 @@ import {writeStderr} from '../../utils/output.mjs'
|
|
|
8
8
|
import {
|
|
9
9
|
ensureCleanWorkingTree,
|
|
10
10
|
ensureReleaseBranchReady,
|
|
11
|
-
runReleaseCommand
|
|
11
|
+
runReleaseCommand,
|
|
12
12
|
validateReleaseDependencies
|
|
13
13
|
} from '../../release/shared.mjs'
|
|
14
14
|
|
|
@@ -22,7 +22,12 @@ function hasScript(pkg, scriptName) {
|
|
|
22
22
|
return pkg?.scripts?.[scriptName] !== undefined
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
async function runLint(skipLint, pkg, rootDir = process.cwd(), {
|
|
25
|
+
async function runLint(skipLint, pkg, rootDir = process.cwd(), {
|
|
26
|
+
logStep,
|
|
27
|
+
logSuccess,
|
|
28
|
+
logWarning,
|
|
29
|
+
runCommand = runReleaseCommand
|
|
30
|
+
} = {}) {
|
|
26
31
|
if (skipLint) {
|
|
27
32
|
logWarning?.('Skipping lint because --skip-lint flag was provided.')
|
|
28
33
|
return
|
|
@@ -49,7 +54,12 @@ async function runLint(skipLint, pkg, rootDir = process.cwd(), {logStep, logSucc
|
|
|
49
54
|
}
|
|
50
55
|
}
|
|
51
56
|
|
|
52
|
-
async function runTests(skipTests, pkg, rootDir = process.cwd(), {
|
|
57
|
+
async function runTests(skipTests, pkg, rootDir = process.cwd(), {
|
|
58
|
+
logStep,
|
|
59
|
+
logSuccess,
|
|
60
|
+
logWarning,
|
|
61
|
+
runCommand = runReleaseCommand
|
|
62
|
+
} = {}) {
|
|
53
63
|
if (skipTests) {
|
|
54
64
|
logWarning?.('Skipping tests because --skip-tests flag was provided.')
|
|
55
65
|
return
|
|
@@ -91,7 +101,12 @@ async function runTests(skipTests, pkg, rootDir = process.cwd(), {logStep, logSu
|
|
|
91
101
|
}
|
|
92
102
|
}
|
|
93
103
|
|
|
94
|
-
async function runBuild(skipBuild, pkg, rootDir = process.cwd(), {
|
|
104
|
+
async function runBuild(skipBuild, pkg, rootDir = process.cwd(), {
|
|
105
|
+
logStep,
|
|
106
|
+
logSuccess,
|
|
107
|
+
logWarning,
|
|
108
|
+
runCommand = runReleaseCommand
|
|
109
|
+
} = {}) {
|
|
95
110
|
if (skipBuild) {
|
|
96
111
|
logWarning?.('Skipping build because --skip-build flag was provided.')
|
|
97
112
|
return
|
|
@@ -118,7 +133,12 @@ async function runBuild(skipBuild, pkg, rootDir = process.cwd(), {logStep, logSu
|
|
|
118
133
|
}
|
|
119
134
|
}
|
|
120
135
|
|
|
121
|
-
async function runLibBuild(skipBuild, pkg, rootDir = process.cwd(), {
|
|
136
|
+
async function runLibBuild(skipBuild, pkg, rootDir = process.cwd(), {
|
|
137
|
+
logStep,
|
|
138
|
+
logSuccess,
|
|
139
|
+
logWarning,
|
|
140
|
+
runCommand = runReleaseCommand
|
|
141
|
+
} = {}) {
|
|
122
142
|
if (skipBuild) {
|
|
123
143
|
logWarning?.('Skipping library build because --skip-build flag was provided.')
|
|
124
144
|
return false
|
|
@@ -160,7 +180,11 @@ async function runLibBuild(skipBuild, pkg, rootDir = process.cwd(), {logStep, lo
|
|
|
160
180
|
return hasLibChanges
|
|
161
181
|
}
|
|
162
182
|
|
|
163
|
-
async function bumpVersion(releaseType, rootDir = process.cwd(), {
|
|
183
|
+
async function bumpVersion(releaseType, rootDir = process.cwd(), {
|
|
184
|
+
logStep,
|
|
185
|
+
logSuccess,
|
|
186
|
+
runCommand = runReleaseCommand
|
|
187
|
+
} = {}) {
|
|
164
188
|
logStep?.('Bumping package version...')
|
|
165
189
|
|
|
166
190
|
const {stdout: statusBefore} = await runCommand('git', ['status', '--porcelain'], {capture: true, cwd: rootDir})
|
|
@@ -197,7 +221,11 @@ async function bumpVersion(releaseType, rootDir = process.cwd(), {logStep, logSu
|
|
|
197
221
|
return pkg
|
|
198
222
|
}
|
|
199
223
|
|
|
200
|
-
async function pushChanges(rootDir = process.cwd(), {
|
|
224
|
+
async function pushChanges(rootDir = process.cwd(), {
|
|
225
|
+
logStep,
|
|
226
|
+
logSuccess,
|
|
227
|
+
runCommand = runReleaseCommand
|
|
228
|
+
} = {}) {
|
|
201
229
|
logStep?.('Pushing commits and tags to origin...')
|
|
202
230
|
try {
|
|
203
231
|
await runCommand('git', ['push', '--follow-tags'], {capture: true, cwd: rootDir})
|
|
@@ -224,7 +252,12 @@ function extractDomainFromHomepage(homepage) {
|
|
|
224
252
|
}
|
|
225
253
|
}
|
|
226
254
|
|
|
227
|
-
async function deployGHPages(skipDeploy, pkg, rootDir = process.cwd(), {
|
|
255
|
+
async function deployGHPages(skipDeploy, pkg, rootDir = process.cwd(), {
|
|
256
|
+
logStep,
|
|
257
|
+
logSuccess,
|
|
258
|
+
logWarning,
|
|
259
|
+
runCommand = runReleaseCommand
|
|
260
|
+
} = {}) {
|
|
228
261
|
if (skipDeploy) {
|
|
229
262
|
logWarning?.('Skipping GitHub Pages deployment because --skip-deploy flag was provided.')
|
|
230
263
|
return
|
|
@@ -315,26 +348,40 @@ export async function releaseNodePackage({
|
|
|
315
348
|
rootDir = process.cwd(),
|
|
316
349
|
logStep,
|
|
317
350
|
logSuccess,
|
|
318
|
-
logWarning
|
|
351
|
+
logWarning,
|
|
352
|
+
runPrompt,
|
|
353
|
+
runCommandImpl,
|
|
354
|
+
runCommandCaptureImpl,
|
|
355
|
+
interactive = true
|
|
319
356
|
} = {}) {
|
|
357
|
+
const runCommand = (command, args, options = {}) => runReleaseCommand(command, args, {
|
|
358
|
+
...options,
|
|
359
|
+
runCommandImpl,
|
|
360
|
+
runCommandCaptureImpl
|
|
361
|
+
})
|
|
362
|
+
|
|
320
363
|
logStep?.('Reading package metadata...')
|
|
321
364
|
const pkg = await readPackage(rootDir)
|
|
322
365
|
|
|
323
366
|
logStep?.('Validating dependencies...')
|
|
324
|
-
await validateReleaseDependencies(rootDir, {
|
|
367
|
+
await validateReleaseDependencies(rootDir, {
|
|
368
|
+
prompt: runPrompt,
|
|
369
|
+
logSuccess,
|
|
370
|
+
interactive
|
|
371
|
+
})
|
|
325
372
|
|
|
326
373
|
logStep?.('Checking working tree status...')
|
|
327
374
|
await ensureCleanWorkingTree(rootDir, {runCommand})
|
|
328
375
|
await ensureReleaseBranchReady({rootDir, branchMethod: 'show-current', logStep, logWarning})
|
|
329
376
|
|
|
330
|
-
await runLint(skipLint, pkg, rootDir, {logStep, logSuccess, logWarning})
|
|
331
|
-
await runTests(skipTests, pkg, rootDir, {logStep, logSuccess, logWarning})
|
|
332
|
-
await runLibBuild(skipBuild, pkg, rootDir, {logStep, logSuccess, logWarning})
|
|
377
|
+
await runLint(skipLint, pkg, rootDir, {logStep, logSuccess, logWarning, runCommand})
|
|
378
|
+
await runTests(skipTests, pkg, rootDir, {logStep, logSuccess, logWarning, runCommand})
|
|
379
|
+
await runLibBuild(skipBuild, pkg, rootDir, {logStep, logSuccess, logWarning, runCommand})
|
|
333
380
|
|
|
334
|
-
const updatedPkg = await bumpVersion(releaseType, rootDir, {logStep, logSuccess})
|
|
335
|
-
await runBuild(skipBuild, updatedPkg, rootDir, {logStep, logSuccess, logWarning})
|
|
336
|
-
await pushChanges(rootDir, {logStep, logSuccess})
|
|
337
|
-
await deployGHPages(skipDeploy, updatedPkg, rootDir, {logStep, logSuccess, logWarning})
|
|
381
|
+
const updatedPkg = await bumpVersion(releaseType, rootDir, {logStep, logSuccess, runCommand})
|
|
382
|
+
await runBuild(skipBuild, updatedPkg, rootDir, {logStep, logSuccess, logWarning, runCommand})
|
|
383
|
+
await pushChanges(rootDir, {logStep, logSuccess, runCommand})
|
|
384
|
+
await deployGHPages(skipDeploy, updatedPkg, rootDir, {logStep, logSuccess, logWarning, runCommand})
|
|
338
385
|
|
|
339
386
|
logStep?.('Publishing will be handled by GitHub Actions via trusted publishing.')
|
|
340
387
|
logSuccess?.(`Release workflow completed for ${updatedPkg.name}@${updatedPkg.version}.`)
|