@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
|
@@ -12,7 +12,7 @@ import {createRemoteExecutor} from '../../deploy/remote-exec.mjs'
|
|
|
12
12
|
import {resolveSshKeyPath} from '../../ssh/keys.mjs'
|
|
13
13
|
import {cleanupOldLogs, closeLogFile, getLogFilePath, writeToLogFile} from '../../utils/log-file.mjs'
|
|
14
14
|
import {resolveRemotePath} from '../../utils/remote-path.mjs'
|
|
15
|
-
import {buildRemoteDeploymentPlan} from './build-remote-deployment-plan.mjs'
|
|
15
|
+
import {buildRemoteDeploymentPlan, resolveRemoteDeploymentState} from './build-remote-deployment-plan.mjs'
|
|
16
16
|
import {executeRemoteDeploymentPlan} from './execute-remote-deployment-plan.mjs'
|
|
17
17
|
import {prepareLocalDeployment} from './prepare-local-deployment.mjs'
|
|
18
18
|
|
|
@@ -21,12 +21,45 @@ async function resolveRemoteHome(ssh, sshUser) {
|
|
|
21
21
|
return remoteHomeResult.stdout.trim() || `/home/${sshUser}`
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
async function connectToRemoteDeploymentTarget({
|
|
25
|
+
config,
|
|
26
|
+
createSshClient,
|
|
27
|
+
sshUser,
|
|
28
|
+
privateKey,
|
|
29
|
+
remoteCwd = null,
|
|
30
|
+
logProcessing,
|
|
31
|
+
message
|
|
32
|
+
} = {}) {
|
|
33
|
+
const ssh = createSshClient()
|
|
34
|
+
|
|
35
|
+
logProcessing?.(`\n${message ?? `Connecting to ${config.serverIp} as ${sshUser}...`}`)
|
|
36
|
+
|
|
37
|
+
await ssh.connect({
|
|
38
|
+
host: config.serverIp,
|
|
39
|
+
username: sshUser,
|
|
40
|
+
privateKey
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
if (remoteCwd) {
|
|
44
|
+
return {ssh, remoteCwd}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const remoteHome = await resolveRemoteHome(ssh, sshUser)
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
ssh,
|
|
51
|
+
remoteCwd: resolveRemotePath(config.projectPath, remoteHome)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
24
55
|
async function maybeRecoverLaravelMaintenanceMode({
|
|
25
56
|
remotePlan,
|
|
26
57
|
executionState,
|
|
27
58
|
executeRemote,
|
|
28
59
|
runPrompt,
|
|
29
|
-
|
|
60
|
+
logProcessing,
|
|
61
|
+
logWarning,
|
|
62
|
+
executionMode = {}
|
|
30
63
|
} = {}) {
|
|
31
64
|
if (!remotePlan?.remoteIsLaravel || !remotePlan?.maintenanceModeEnabled) {
|
|
32
65
|
return
|
|
@@ -36,12 +69,27 @@ async function maybeRecoverLaravelMaintenanceMode({
|
|
|
36
69
|
return
|
|
37
70
|
}
|
|
38
71
|
|
|
39
|
-
if (typeof
|
|
72
|
+
if (typeof executeRemote !== 'function') {
|
|
40
73
|
logWarning?.('Deployment failed while Laravel maintenance mode may still be enabled.')
|
|
41
74
|
return
|
|
42
75
|
}
|
|
43
76
|
|
|
44
77
|
try {
|
|
78
|
+
if (executionMode?.interactive === false) {
|
|
79
|
+
logProcessing?.('Deployment failed after Laravel maintenance mode was enabled. Running `artisan up` automatically...')
|
|
80
|
+
await executeRemote(
|
|
81
|
+
'Disable Laravel maintenance mode',
|
|
82
|
+
remotePlan.maintenanceUpCommand ?? `${remotePlan.phpCommand} artisan up`
|
|
83
|
+
)
|
|
84
|
+
executionState.exitedMaintenanceMode = true
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (typeof runPrompt !== 'function') {
|
|
89
|
+
logWarning?.('Deployment failed while Laravel maintenance mode may still be enabled.')
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
45
93
|
const answers = await runPrompt([
|
|
46
94
|
{
|
|
47
95
|
type: 'confirm',
|
|
@@ -82,51 +130,79 @@ export async function runDeployment(config, options = {}) {
|
|
|
82
130
|
logError,
|
|
83
131
|
runPrompt,
|
|
84
132
|
createSshClient,
|
|
85
|
-
runCommand
|
|
133
|
+
runCommand,
|
|
134
|
+
executionMode
|
|
86
135
|
} = context
|
|
87
136
|
|
|
88
137
|
await cleanupOldLogs(rootDir)
|
|
89
138
|
|
|
90
|
-
const {requiredPhpVersion} = await prepareLocalDeployment(config, {
|
|
91
|
-
snapshot,
|
|
92
|
-
rootDir,
|
|
93
|
-
versionArg,
|
|
94
|
-
runPrompt,
|
|
95
|
-
runCommand,
|
|
96
|
-
runCommandCapture: context.runCommandCapture,
|
|
97
|
-
logProcessing,
|
|
98
|
-
logSuccess,
|
|
99
|
-
logWarning
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
const ssh = createSshClient()
|
|
103
139
|
const sshUser = config.sshUser || os.userInfo().username
|
|
104
140
|
const privateKeyPath = await resolveSshKeyPath(config.sshKey)
|
|
105
141
|
const privateKey = await fs.readFile(privateKeyPath, 'utf8')
|
|
142
|
+
let ssh = null
|
|
106
143
|
let remoteCwd = null
|
|
107
144
|
let executeRemote = null
|
|
108
145
|
let remotePlan = null
|
|
146
|
+
let remoteState = null
|
|
109
147
|
const executionState = {
|
|
110
148
|
enteredMaintenanceMode: false,
|
|
111
149
|
exitedMaintenanceMode: false
|
|
112
150
|
}
|
|
113
151
|
|
|
114
|
-
logProcessing(`\nConnecting to ${config.serverIp} as ${sshUser}...`)
|
|
115
|
-
|
|
116
152
|
let lockAcquired = false
|
|
117
153
|
|
|
118
154
|
try {
|
|
119
|
-
await
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
155
|
+
({ssh, remoteCwd} = await connectToRemoteDeploymentTarget({
|
|
156
|
+
config,
|
|
157
|
+
createSshClient,
|
|
158
|
+
sshUser,
|
|
159
|
+
privateKey,
|
|
160
|
+
logProcessing,
|
|
161
|
+
message: `Connecting to ${config.serverIp} as ${sshUser} to inspect remote deployment state...`
|
|
162
|
+
}))
|
|
163
|
+
|
|
164
|
+
remoteState = await resolveRemoteDeploymentState({
|
|
165
|
+
snapshot,
|
|
166
|
+
executionMode,
|
|
167
|
+
ssh,
|
|
168
|
+
remoteCwd,
|
|
169
|
+
runPrompt,
|
|
170
|
+
logSuccess,
|
|
171
|
+
logWarning
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
// Reconnect after local checks so long-running validation does not hold a stale SSH session open.
|
|
175
|
+
await ssh.dispose()
|
|
176
|
+
ssh = null
|
|
177
|
+
|
|
178
|
+
const {requiredPhpVersion} = await prepareLocalDeployment(config, {
|
|
179
|
+
snapshot,
|
|
180
|
+
rootDir,
|
|
181
|
+
versionArg,
|
|
182
|
+
runPrompt,
|
|
183
|
+
runCommand,
|
|
184
|
+
runCommandCapture: context.runCommandCapture,
|
|
185
|
+
logProcessing,
|
|
186
|
+
logSuccess,
|
|
187
|
+
logWarning
|
|
123
188
|
})
|
|
124
189
|
|
|
125
|
-
|
|
126
|
-
|
|
190
|
+
;({ssh} = await connectToRemoteDeploymentTarget({
|
|
191
|
+
config,
|
|
192
|
+
createSshClient,
|
|
193
|
+
sshUser,
|
|
194
|
+
privateKey,
|
|
195
|
+
remoteCwd,
|
|
196
|
+
logProcessing,
|
|
197
|
+
message: `Reconnecting to ${config.serverIp} as ${sshUser}...`
|
|
198
|
+
}))
|
|
127
199
|
|
|
128
200
|
logProcessing('Connection established. Acquiring deployment lock on server...')
|
|
129
|
-
await acquireRemoteLock(ssh, remoteCwd, rootDir, {
|
|
201
|
+
await acquireRemoteLock(ssh, remoteCwd, rootDir, {
|
|
202
|
+
runPrompt,
|
|
203
|
+
logWarning,
|
|
204
|
+
interactive: executionMode?.interactive !== false
|
|
205
|
+
})
|
|
130
206
|
lockAcquired = true
|
|
131
207
|
logProcessing(`Lock acquired. Running deployment commands in ${remoteCwd}...`)
|
|
132
208
|
|
|
@@ -145,6 +221,9 @@ export async function runDeployment(config, options = {}) {
|
|
|
145
221
|
snapshot,
|
|
146
222
|
rootDir,
|
|
147
223
|
requiredPhpVersion,
|
|
224
|
+
executionMode,
|
|
225
|
+
remoteIsLaravel: remoteState?.remoteIsLaravel,
|
|
226
|
+
maintenanceModeEnabled: remoteState?.maintenanceModeEnabled,
|
|
148
227
|
ssh,
|
|
149
228
|
remoteCwd,
|
|
150
229
|
executeRemote,
|
|
@@ -179,12 +258,18 @@ export async function runDeployment(config, options = {}) {
|
|
|
179
258
|
executionState,
|
|
180
259
|
executeRemote,
|
|
181
260
|
runPrompt,
|
|
182
|
-
|
|
261
|
+
logProcessing,
|
|
262
|
+
logWarning,
|
|
263
|
+
executionMode
|
|
183
264
|
})
|
|
184
265
|
|
|
185
266
|
if (lockAcquired && ssh && remoteCwd) {
|
|
186
267
|
try {
|
|
187
|
-
await compareLocksAndPrompt(rootDir, ssh, remoteCwd, {
|
|
268
|
+
await compareLocksAndPrompt(rootDir, ssh, remoteCwd, {
|
|
269
|
+
runPrompt,
|
|
270
|
+
logWarning,
|
|
271
|
+
interactive: executionMode?.interactive !== false
|
|
272
|
+
})
|
|
188
273
|
} catch {
|
|
189
274
|
// Ignore lock comparison errors during error handling
|
|
190
275
|
}
|
|
@@ -206,4 +291,4 @@ export async function runDeployment(config, options = {}) {
|
|
|
206
291
|
ssh.dispose()
|
|
207
292
|
}
|
|
208
293
|
}
|
|
209
|
-
}
|
|
294
|
+
}
|
|
@@ -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}.`)
|
|
@@ -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
|
|
|
@@ -50,7 +50,13 @@ async function hasArtisan(rootDir = process.cwd()) {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
async function runLint(skipLint, rootDir = process.cwd(), {
|
|
53
|
+
async function runLint(skipLint, rootDir = process.cwd(), {
|
|
54
|
+
logStep,
|
|
55
|
+
logSuccess,
|
|
56
|
+
logWarning,
|
|
57
|
+
runCommand = runReleaseCommand,
|
|
58
|
+
progressWriter = process.stdout
|
|
59
|
+
} = {}) {
|
|
54
60
|
if (skipLint) {
|
|
55
61
|
logWarning?.('Skipping lint because --skip-lint flag was provided.')
|
|
56
62
|
return
|
|
@@ -67,9 +73,9 @@ async function runLint(skipLint, rootDir = process.cwd(), {logStep, logSuccess,
|
|
|
67
73
|
|
|
68
74
|
let dotInterval = null
|
|
69
75
|
try {
|
|
70
|
-
|
|
76
|
+
progressWriter.write(' ')
|
|
71
77
|
dotInterval = setInterval(() => {
|
|
72
|
-
|
|
78
|
+
progressWriter.write('.')
|
|
73
79
|
}, 200)
|
|
74
80
|
|
|
75
81
|
await runCommand('php', [pintPath], {capture: true, cwd: rootDir})
|
|
@@ -78,14 +84,14 @@ async function runLint(skipLint, rootDir = process.cwd(), {logStep, logSuccess,
|
|
|
78
84
|
clearInterval(dotInterval)
|
|
79
85
|
dotInterval = null
|
|
80
86
|
}
|
|
81
|
-
|
|
87
|
+
progressWriter.write('\n')
|
|
82
88
|
logSuccess?.('Lint passed.')
|
|
83
89
|
} catch (error) {
|
|
84
90
|
if (dotInterval) {
|
|
85
91
|
clearInterval(dotInterval)
|
|
86
92
|
dotInterval = null
|
|
87
93
|
}
|
|
88
|
-
|
|
94
|
+
progressWriter.write('\n')
|
|
89
95
|
if (error.stdout) {
|
|
90
96
|
writeStderr(error.stdout)
|
|
91
97
|
}
|
|
@@ -96,7 +102,13 @@ async function runLint(skipLint, rootDir = process.cwd(), {logStep, logSuccess,
|
|
|
96
102
|
}
|
|
97
103
|
}
|
|
98
104
|
|
|
99
|
-
async function runTests(skipTests, composer, rootDir = process.cwd(), {
|
|
105
|
+
async function runTests(skipTests, composer, rootDir = process.cwd(), {
|
|
106
|
+
logStep,
|
|
107
|
+
logSuccess,
|
|
108
|
+
logWarning,
|
|
109
|
+
runCommand = runReleaseCommand,
|
|
110
|
+
progressWriter = process.stdout
|
|
111
|
+
} = {}) {
|
|
100
112
|
if (skipTests) {
|
|
101
113
|
logWarning?.('Skipping tests because --skip-tests flag was provided.')
|
|
102
114
|
return
|
|
@@ -114,9 +126,9 @@ async function runTests(skipTests, composer, rootDir = process.cwd(), {logStep,
|
|
|
114
126
|
|
|
115
127
|
let dotInterval = null
|
|
116
128
|
try {
|
|
117
|
-
|
|
129
|
+
progressWriter.write(' ')
|
|
118
130
|
dotInterval = setInterval(() => {
|
|
119
|
-
|
|
131
|
+
progressWriter.write('.')
|
|
120
132
|
}, 200)
|
|
121
133
|
|
|
122
134
|
if (hasArtisanFile) {
|
|
@@ -129,14 +141,14 @@ async function runTests(skipTests, composer, rootDir = process.cwd(), {logStep,
|
|
|
129
141
|
clearInterval(dotInterval)
|
|
130
142
|
dotInterval = null
|
|
131
143
|
}
|
|
132
|
-
|
|
144
|
+
progressWriter.write('\n')
|
|
133
145
|
logSuccess?.('Tests passed.')
|
|
134
146
|
} catch (error) {
|
|
135
147
|
if (dotInterval) {
|
|
136
148
|
clearInterval(dotInterval)
|
|
137
149
|
dotInterval = null
|
|
138
150
|
}
|
|
139
|
-
|
|
151
|
+
progressWriter.write('\n')
|
|
140
152
|
if (error.stdout) {
|
|
141
153
|
writeStderr(error.stdout)
|
|
142
154
|
}
|
|
@@ -147,7 +159,11 @@ async function runTests(skipTests, composer, rootDir = process.cwd(), {logStep,
|
|
|
147
159
|
}
|
|
148
160
|
}
|
|
149
161
|
|
|
150
|
-
async function bumpVersion(releaseType, rootDir = process.cwd(), {
|
|
162
|
+
async function bumpVersion(releaseType, rootDir = process.cwd(), {
|
|
163
|
+
logStep,
|
|
164
|
+
logSuccess,
|
|
165
|
+
runCommand = runReleaseCommand
|
|
166
|
+
} = {}) {
|
|
151
167
|
logStep?.('Bumping composer version...')
|
|
152
168
|
|
|
153
169
|
const composer = await readComposer(rootDir)
|
|
@@ -179,7 +195,11 @@ async function bumpVersion(releaseType, rootDir = process.cwd(), {logStep, logSu
|
|
|
179
195
|
return {...composer, version: newVersion}
|
|
180
196
|
}
|
|
181
197
|
|
|
182
|
-
async function pushChanges(rootDir = process.cwd(), {
|
|
198
|
+
async function pushChanges(rootDir = process.cwd(), {
|
|
199
|
+
logStep,
|
|
200
|
+
logSuccess,
|
|
201
|
+
runCommand = runReleaseCommand
|
|
202
|
+
} = {}) {
|
|
183
203
|
logStep?.('Pushing commits to origin...')
|
|
184
204
|
await runCommand('git', ['push'], {cwd: rootDir})
|
|
185
205
|
|
|
@@ -196,8 +216,19 @@ export async function releasePackagistPackage({
|
|
|
196
216
|
rootDir = process.cwd(),
|
|
197
217
|
logStep,
|
|
198
218
|
logSuccess,
|
|
199
|
-
logWarning
|
|
219
|
+
logWarning,
|
|
220
|
+
runPrompt,
|
|
221
|
+
runCommandImpl,
|
|
222
|
+
runCommandCaptureImpl,
|
|
223
|
+
interactive = true,
|
|
224
|
+
progressWriter = process.stdout
|
|
200
225
|
} = {}) {
|
|
226
|
+
const runCommand = (command, args, options = {}) => runReleaseCommand(command, args, {
|
|
227
|
+
...options,
|
|
228
|
+
runCommandImpl,
|
|
229
|
+
runCommandCaptureImpl
|
|
230
|
+
})
|
|
231
|
+
|
|
201
232
|
logStep?.('Reading composer metadata...')
|
|
202
233
|
const composer = await readComposer(rootDir)
|
|
203
234
|
|
|
@@ -206,17 +237,21 @@ export async function releasePackagistPackage({
|
|
|
206
237
|
}
|
|
207
238
|
|
|
208
239
|
logStep?.('Validating dependencies...')
|
|
209
|
-
await validateReleaseDependencies(rootDir, {
|
|
240
|
+
await validateReleaseDependencies(rootDir, {
|
|
241
|
+
prompt: runPrompt,
|
|
242
|
+
logSuccess,
|
|
243
|
+
interactive
|
|
244
|
+
})
|
|
210
245
|
|
|
211
246
|
logStep?.('Checking working tree status...')
|
|
212
247
|
await ensureCleanWorkingTree(rootDir, {runCommand})
|
|
213
248
|
await ensureReleaseBranchReady({rootDir, branchMethod: 'show-current', logStep, logWarning})
|
|
214
249
|
|
|
215
|
-
await runLint(skipLint, rootDir, {logStep, logSuccess, logWarning})
|
|
216
|
-
await runTests(skipTests, composer, rootDir, {logStep, logSuccess, logWarning})
|
|
250
|
+
await runLint(skipLint, rootDir, {logStep, logSuccess, logWarning, runCommand, progressWriter})
|
|
251
|
+
await runTests(skipTests, composer, rootDir, {logStep, logSuccess, logWarning, runCommand, progressWriter})
|
|
217
252
|
|
|
218
|
-
const updatedComposer = await bumpVersion(releaseType, rootDir, {logStep, logSuccess})
|
|
219
|
-
await pushChanges(rootDir, {logStep, logSuccess})
|
|
253
|
+
const updatedComposer = await bumpVersion(releaseType, rootDir, {logStep, logSuccess, runCommand})
|
|
254
|
+
await pushChanges(rootDir, {logStep, logSuccess, runCommand})
|
|
220
255
|
|
|
221
256
|
logSuccess?.(`Release workflow completed for ${composer.name}@${updatedComposer.version}.`)
|
|
222
257
|
logStep?.('Note: Packagist will automatically detect the new git tag and update the package.')
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import process from 'node:process'
|
|
2
|
+
|
|
3
|
+
import {Command} from 'commander'
|
|
4
|
+
|
|
5
|
+
import {InvalidCliOptionsError} from '../runtime/errors.mjs'
|
|
6
|
+
|
|
7
|
+
const WORKFLOW_TYPES = new Set(['node', 'vue', 'packagist'])
|
|
8
|
+
function normalizeMaintenanceMode(value) {
|
|
9
|
+
if (value == null) {
|
|
10
|
+
return null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (value === 'on') {
|
|
14
|
+
return true
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (value === 'off') {
|
|
18
|
+
return false
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
throw new InvalidCliOptionsError('Invalid value for --maintenance. Use "on" or "off".')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function parseCliOptions(args = process.argv.slice(2)) {
|
|
25
|
+
const program = new Command()
|
|
26
|
+
|
|
27
|
+
program
|
|
28
|
+
.allowExcessArguments(false)
|
|
29
|
+
.allowUnknownOption(false)
|
|
30
|
+
.exitOverride()
|
|
31
|
+
.option('--type <type>', 'Workflow type (node|vue|packagist). Omit for normal app deployments.')
|
|
32
|
+
.option('--non-interactive', 'Fail instead of prompting when Zephyr needs user input.')
|
|
33
|
+
.option('--json', 'Emit NDJSON events to stdout. Requires --non-interactive.')
|
|
34
|
+
.option('--preset <name>', 'Preset name to use for non-interactive app deployments.')
|
|
35
|
+
.option('--resume-pending', 'Resume a saved pending deployment snapshot without prompting.')
|
|
36
|
+
.option('--discard-pending', 'Discard a saved pending deployment snapshot without prompting.')
|
|
37
|
+
.option('--maintenance <mode>', 'Laravel maintenance mode policy for non-interactive app deploys (on|off).')
|
|
38
|
+
.option('--skip-tests', 'Skip test execution in package release workflows.')
|
|
39
|
+
.option('--skip-lint', 'Skip lint execution in package release workflows.')
|
|
40
|
+
.option('--skip-build', 'Skip build execution in node/vue release workflows.')
|
|
41
|
+
.option('--skip-deploy', 'Skip GitHub Pages deployment in node/vue release workflows.')
|
|
42
|
+
.argument(
|
|
43
|
+
'[version]',
|
|
44
|
+
'Version or npm bump type for deployments (e.g. 1.2.3, patch, minor, major).'
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
program.parse(args, {from: 'user'})
|
|
49
|
+
} catch (error) {
|
|
50
|
+
throw new InvalidCliOptionsError(error.message)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const options = program.opts()
|
|
54
|
+
const workflowType = options.type ?? null
|
|
55
|
+
|
|
56
|
+
if (workflowType && !WORKFLOW_TYPES.has(workflowType)) {
|
|
57
|
+
throw new InvalidCliOptionsError('Invalid value for --type. Use one of: node, vue, packagist.')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
workflowType,
|
|
62
|
+
versionArg: program.args[0] ?? null,
|
|
63
|
+
nonInteractive: Boolean(options.nonInteractive),
|
|
64
|
+
json: Boolean(options.json),
|
|
65
|
+
presetName: options.preset ?? null,
|
|
66
|
+
resumePending: Boolean(options.resumePending),
|
|
67
|
+
discardPending: Boolean(options.discardPending),
|
|
68
|
+
maintenanceMode: normalizeMaintenanceMode(options.maintenance),
|
|
69
|
+
skipTests: Boolean(options.skipTests),
|
|
70
|
+
skipLint: Boolean(options.skipLint),
|
|
71
|
+
skipBuild: Boolean(options.skipBuild),
|
|
72
|
+
skipDeploy: Boolean(options.skipDeploy)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function validateCliOptions(options = {}) {
|
|
77
|
+
const {
|
|
78
|
+
workflowType = null,
|
|
79
|
+
nonInteractive = false,
|
|
80
|
+
json = false,
|
|
81
|
+
presetName = null,
|
|
82
|
+
resumePending = false,
|
|
83
|
+
discardPending = false,
|
|
84
|
+
maintenanceMode = null,
|
|
85
|
+
skipTests = false,
|
|
86
|
+
skipLint = false,
|
|
87
|
+
skipBuild = false,
|
|
88
|
+
skipDeploy = false
|
|
89
|
+
} = options
|
|
90
|
+
|
|
91
|
+
if (json && !nonInteractive) {
|
|
92
|
+
throw new InvalidCliOptionsError('--json requires --non-interactive.')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (resumePending && discardPending) {
|
|
96
|
+
throw new InvalidCliOptionsError('Use either --resume-pending or --discard-pending, not both.')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const isPackageRelease = workflowType === 'node' || workflowType === 'vue' || workflowType === 'packagist'
|
|
100
|
+
|
|
101
|
+
if (isPackageRelease) {
|
|
102
|
+
if (presetName) {
|
|
103
|
+
throw new InvalidCliOptionsError('--preset is only valid for app deployments.')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (resumePending || discardPending) {
|
|
107
|
+
throw new InvalidCliOptionsError('--resume-pending and --discard-pending are only valid for app deployments.')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (maintenanceMode !== null) {
|
|
111
|
+
throw new InvalidCliOptionsError('--maintenance is only valid for app deployments.')
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
if (skipTests || skipLint || skipBuild || skipDeploy) {
|
|
115
|
+
throw new InvalidCliOptionsError('Release-only skip flags are not valid for app deployments.')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (nonInteractive && !presetName) {
|
|
119
|
+
throw new InvalidCliOptionsError('--non-interactive app deployments require --preset <name>.')
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|