@wyxos/zephyr 0.3.3 → 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 +134 -3
- 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/src/deploy/locks.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'node:fs/promises'
|
|
|
2
2
|
import os from 'node:os'
|
|
3
3
|
import process from 'node:process'
|
|
4
4
|
|
|
5
|
+
import {ZephyrError} from '../runtime/errors.mjs'
|
|
5
6
|
import { PROJECT_LOCK_FILE, ensureDirectory, getLockFilePath, getProjectConfigDir } from '../utils/paths.mjs'
|
|
6
7
|
|
|
7
8
|
function createLockPayload() {
|
|
@@ -67,7 +68,26 @@ export async function readRemoteLock(ssh, remoteCwd) {
|
|
|
67
68
|
return null
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
|
|
71
|
+
function parseLockDetails(rawContent = '') {
|
|
72
|
+
try {
|
|
73
|
+
return JSON.parse(rawContent.trim())
|
|
74
|
+
} catch (_error) {
|
|
75
|
+
return { raw: rawContent.trim() }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function formatLockHolder(details = {}) {
|
|
80
|
+
const startedBy = details.user ? `${details.user}@${details.hostname ?? 'unknown'}` : 'unknown user'
|
|
81
|
+
const startedAt = details.startedAt ? ` at ${details.startedAt}` : ''
|
|
82
|
+
|
|
83
|
+
return { startedBy, startedAt }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function compareLocksAndPrompt(rootDir, ssh, remoteCwd, {
|
|
87
|
+
runPrompt,
|
|
88
|
+
logWarning,
|
|
89
|
+
interactive = true
|
|
90
|
+
} = {}) {
|
|
71
91
|
const localLock = await readLocalLock(rootDir)
|
|
72
92
|
const remoteLock = await readRemoteLock(ssh, remoteCwd)
|
|
73
93
|
|
|
@@ -79,8 +99,15 @@ export async function compareLocksAndPrompt(rootDir, ssh, remoteCwd, { runPrompt
|
|
|
79
99
|
const remoteKey = `${remoteLock.user}@${remoteLock.hostname}:${remoteLock.pid}:${remoteLock.startedAt}`
|
|
80
100
|
|
|
81
101
|
if (localKey === remoteKey) {
|
|
82
|
-
const startedBy
|
|
83
|
-
|
|
102
|
+
const { startedBy, startedAt } = formatLockHolder(remoteLock)
|
|
103
|
+
|
|
104
|
+
if (!interactive) {
|
|
105
|
+
throw new ZephyrError(
|
|
106
|
+
`Stale deployment lock detected on the server (started by ${startedBy}${startedAt}). Remove ${remoteCwd}/.zephyr/${PROJECT_LOCK_FILE} manually before rerunning with --non-interactive.`,
|
|
107
|
+
{code: 'ZEPHYR_STALE_REMOTE_LOCK'}
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
84
111
|
const { shouldRemove } = await runPrompt([
|
|
85
112
|
{
|
|
86
113
|
type: 'confirm',
|
|
@@ -103,7 +130,11 @@ export async function compareLocksAndPrompt(rootDir, ssh, remoteCwd, { runPrompt
|
|
|
103
130
|
return false
|
|
104
131
|
}
|
|
105
132
|
|
|
106
|
-
export async function acquireRemoteLock(ssh, remoteCwd, rootDir, {
|
|
133
|
+
export async function acquireRemoteLock(ssh, remoteCwd, rootDir, {
|
|
134
|
+
runPrompt,
|
|
135
|
+
logWarning,
|
|
136
|
+
interactive = true
|
|
137
|
+
} = {}) {
|
|
107
138
|
const lockPath = `.zephyr/${PROJECT_LOCK_FILE}`
|
|
108
139
|
const escapedLockPath = lockPath.replace(/'/g, "'\\''")
|
|
109
140
|
const checkCommand = `mkdir -p .zephyr && if [ -f '${escapedLockPath}' ]; then cat '${escapedLockPath}'; else echo "LOCK_NOT_FOUND"; fi`
|
|
@@ -113,31 +144,17 @@ export async function acquireRemoteLock(ssh, remoteCwd, rootDir, { runPrompt, lo
|
|
|
113
144
|
if (checkResult.stdout && checkResult.stdout.trim() !== 'LOCK_NOT_FOUND' && checkResult.stdout.trim() !== '') {
|
|
114
145
|
const localLock = await readLocalLock(rootDir)
|
|
115
146
|
if (localLock) {
|
|
116
|
-
const removed = await compareLocksAndPrompt(rootDir, ssh, remoteCwd, { runPrompt, logWarning })
|
|
147
|
+
const removed = await compareLocksAndPrompt(rootDir, ssh, remoteCwd, { runPrompt, logWarning, interactive })
|
|
117
148
|
if (!removed) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
details = JSON.parse(checkResult.stdout.trim())
|
|
121
|
-
} catch (_error) {
|
|
122
|
-
details = { raw: checkResult.stdout.trim() }
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const startedBy = details.user ? `${details.user}@${details.hostname ?? 'unknown'}` : 'unknown user'
|
|
126
|
-
const startedAt = details.startedAt ? ` at ${details.startedAt}` : ''
|
|
149
|
+
const details = parseLockDetails(checkResult.stdout.trim())
|
|
150
|
+
const { startedBy, startedAt } = formatLockHolder(details)
|
|
127
151
|
throw new Error(
|
|
128
152
|
`Another deployment is currently in progress on the server (started by ${startedBy}${startedAt}). Remove ${remoteCwd}/${lockPath} if you are sure it is stale.`
|
|
129
153
|
)
|
|
130
154
|
}
|
|
131
155
|
} else {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
details = JSON.parse(checkResult.stdout.trim())
|
|
135
|
-
} catch (_error) {
|
|
136
|
-
details = { raw: checkResult.stdout.trim() }
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const startedBy = details.user ? `${details.user}@${details.hostname ?? 'unknown'}` : 'unknown user'
|
|
140
|
-
const startedAt = details.startedAt ? ` at ${details.startedAt}` : ''
|
|
156
|
+
const details = parseLockDetails(checkResult.stdout.trim())
|
|
157
|
+
const { startedBy, startedAt } = formatLockHolder(details)
|
|
141
158
|
throw new Error(
|
|
142
159
|
`Another deployment is currently in progress on the server (started by ${startedBy}${startedAt}). Remove ${remoteCwd}/${lockPath} if you are sure it is stale.`
|
|
143
160
|
)
|
|
@@ -168,4 +185,3 @@ export async function releaseRemoteLock(ssh, remoteCwd, { logWarning } = {}) {
|
|
|
168
185
|
logWarning?.(`Failed to remove lock file: ${result.stderr}`)
|
|
169
186
|
}
|
|
170
187
|
}
|
|
171
|
-
|
package/src/main.mjs
CHANGED
|
@@ -3,10 +3,12 @@ import {createRequire} from 'node:module'
|
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import process from 'node:process'
|
|
5
5
|
|
|
6
|
+
import {validateCliOptions} from './cli/options.mjs'
|
|
6
7
|
import {releaseNode} from './release-node.mjs'
|
|
7
8
|
import {releasePackagist} from './release-packagist.mjs'
|
|
8
9
|
import {validateLocalDependencies} from './dependency-scanner.mjs'
|
|
9
10
|
import * as bootstrap from './project/bootstrap.mjs'
|
|
11
|
+
import {getErrorCode} from './runtime/errors.mjs'
|
|
10
12
|
import {PROJECT_CONFIG_DIR} from './utils/paths.mjs'
|
|
11
13
|
import {writeStderrLine} from './utils/output.mjs'
|
|
12
14
|
import {createAppContext} from './runtime/app-context.mjs'
|
|
@@ -20,97 +22,223 @@ const RELEASE_SCRIPT_COMMAND = 'npx @wyxos/zephyr@latest'
|
|
|
20
22
|
const require = createRequire(import.meta.url)
|
|
21
23
|
const {version: ZEPHYR_VERSION} = require('../package.json')
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
function normalizeMainOptions(firstArg = null, secondArg = null) {
|
|
26
|
+
if (firstArg && typeof firstArg === 'object' && !Array.isArray(firstArg)) {
|
|
27
|
+
return {
|
|
28
|
+
workflowType: firstArg.workflowType ?? firstArg.type ?? null,
|
|
29
|
+
versionArg: firstArg.versionArg ?? null,
|
|
30
|
+
nonInteractive: firstArg.nonInteractive === true,
|
|
31
|
+
json: firstArg.json === true,
|
|
32
|
+
presetName: firstArg.presetName ?? null,
|
|
33
|
+
resumePending: firstArg.resumePending === true,
|
|
34
|
+
discardPending: firstArg.discardPending === true,
|
|
35
|
+
maintenanceMode: firstArg.maintenanceMode ?? null,
|
|
36
|
+
skipTests: firstArg.skipTests === true,
|
|
37
|
+
skipLint: firstArg.skipLint === true,
|
|
38
|
+
skipBuild: firstArg.skipBuild === true,
|
|
39
|
+
skipDeploy: firstArg.skipDeploy === true,
|
|
40
|
+
context: firstArg.context ?? null
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
workflowType: firstArg ?? null,
|
|
46
|
+
versionArg: secondArg ?? null,
|
|
47
|
+
nonInteractive: false,
|
|
48
|
+
json: false,
|
|
49
|
+
presetName: null,
|
|
50
|
+
resumePending: false,
|
|
51
|
+
discardPending: false,
|
|
52
|
+
maintenanceMode: null,
|
|
53
|
+
skipTests: false,
|
|
54
|
+
skipLint: false,
|
|
55
|
+
skipBuild: false,
|
|
56
|
+
skipDeploy: false,
|
|
57
|
+
context: null
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function resolveWorkflowName(workflowType = null) {
|
|
62
|
+
if (workflowType === 'node' || workflowType === 'vue') {
|
|
63
|
+
return `release-${workflowType}`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (workflowType === 'packagist') {
|
|
67
|
+
return 'release-packagist'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return 'deploy'
|
|
71
|
+
}
|
|
33
72
|
|
|
34
73
|
async function runRemoteTasks(config, options = {}) {
|
|
35
74
|
return await runDeployment(config, {
|
|
36
75
|
...options,
|
|
37
|
-
context: options.context
|
|
76
|
+
context: options.context
|
|
38
77
|
})
|
|
39
78
|
}
|
|
40
79
|
|
|
41
|
-
async function main(
|
|
42
|
-
|
|
80
|
+
async function main(optionsOrWorkflowType = null, versionArg = null) {
|
|
81
|
+
const options = normalizeMainOptions(optionsOrWorkflowType, versionArg)
|
|
82
|
+
|
|
83
|
+
const executionMode = {
|
|
84
|
+
interactive: !options.nonInteractive,
|
|
85
|
+
json: options.json === true && options.nonInteractive === true,
|
|
86
|
+
workflow: resolveWorkflowName(options.workflowType),
|
|
87
|
+
presetName: options.presetName,
|
|
88
|
+
maintenanceMode: options.maintenanceMode,
|
|
89
|
+
resumePending: options.resumePending,
|
|
90
|
+
discardPending: options.discardPending
|
|
91
|
+
}
|
|
92
|
+
const appContext = options.context ?? createAppContext({executionMode})
|
|
93
|
+
const {
|
|
94
|
+
logProcessing,
|
|
95
|
+
logSuccess,
|
|
96
|
+
logWarning,
|
|
97
|
+
logError,
|
|
98
|
+
runPrompt,
|
|
99
|
+
runCommand,
|
|
100
|
+
emitEvent
|
|
101
|
+
} = appContext
|
|
102
|
+
const currentExecutionMode = appContext.executionMode ?? executionMode
|
|
103
|
+
const configurationService = createConfigurationService(appContext)
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
validateCliOptions(options)
|
|
107
|
+
|
|
108
|
+
if (currentExecutionMode.json) {
|
|
109
|
+
emitEvent?.('run_started', {
|
|
110
|
+
message: `Zephyr v${ZEPHYR_VERSION} starting`,
|
|
111
|
+
data: {
|
|
112
|
+
version: ZEPHYR_VERSION,
|
|
113
|
+
workflow: currentExecutionMode.workflow,
|
|
114
|
+
nonInteractive: currentExecutionMode.interactive === false,
|
|
115
|
+
presetName: currentExecutionMode.presetName,
|
|
116
|
+
maintenanceMode: currentExecutionMode.maintenanceMode,
|
|
117
|
+
resumePending: currentExecutionMode.resumePending,
|
|
118
|
+
discardPending: currentExecutionMode.discardPending
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
} else {
|
|
122
|
+
logProcessing(`Zephyr v${ZEPHYR_VERSION}`)
|
|
123
|
+
}
|
|
43
124
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
125
|
+
if (options.workflowType === 'node' || options.workflowType === 'vue') {
|
|
126
|
+
await releaseNode({
|
|
127
|
+
releaseType: options.versionArg,
|
|
128
|
+
skipTests: options.skipTests,
|
|
129
|
+
skipLint: options.skipLint,
|
|
130
|
+
skipBuild: options.skipBuild,
|
|
131
|
+
skipDeploy: options.skipDeploy,
|
|
132
|
+
context: appContext
|
|
133
|
+
})
|
|
134
|
+
emitEvent?.('run_completed', {
|
|
135
|
+
message: 'Zephyr workflow completed successfully.',
|
|
136
|
+
data: {
|
|
137
|
+
version: ZEPHYR_VERSION,
|
|
138
|
+
workflow: currentExecutionMode.workflow
|
|
139
|
+
}
|
|
140
|
+
})
|
|
47
141
|
return
|
|
48
|
-
} catch (error) {
|
|
49
|
-
logError('\nRelease failed:')
|
|
50
|
-
logError(error.message)
|
|
51
|
-
if (error.stack) {
|
|
52
|
-
writeStderrLine(error.stack)
|
|
53
|
-
}
|
|
54
|
-
process.exit(1)
|
|
55
142
|
}
|
|
56
|
-
}
|
|
57
143
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
144
|
+
if (options.workflowType === 'packagist') {
|
|
145
|
+
await releasePackagist({
|
|
146
|
+
releaseType: options.versionArg,
|
|
147
|
+
skipTests: options.skipTests,
|
|
148
|
+
skipLint: options.skipLint,
|
|
149
|
+
context: appContext
|
|
150
|
+
})
|
|
151
|
+
emitEvent?.('run_completed', {
|
|
152
|
+
message: 'Zephyr workflow completed successfully.',
|
|
153
|
+
data: {
|
|
154
|
+
version: ZEPHYR_VERSION,
|
|
155
|
+
workflow: currentExecutionMode.workflow
|
|
156
|
+
}
|
|
157
|
+
})
|
|
61
158
|
return
|
|
62
|
-
} catch (error) {
|
|
63
|
-
logError('\nRelease failed:')
|
|
64
|
-
logError(error.message)
|
|
65
|
-
if (error.stack) {
|
|
66
|
-
writeStderrLine(error.stack)
|
|
67
|
-
}
|
|
68
|
-
process.exit(1)
|
|
69
159
|
}
|
|
70
|
-
}
|
|
71
160
|
|
|
72
|
-
|
|
161
|
+
const rootDir = process.cwd()
|
|
73
162
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
163
|
+
await bootstrap.ensureGitignoreEntry(rootDir, {
|
|
164
|
+
projectConfigDir: PROJECT_CONFIG_DIR,
|
|
165
|
+
runCommand,
|
|
166
|
+
logSuccess,
|
|
167
|
+
logWarning
|
|
168
|
+
})
|
|
169
|
+
await bootstrap.ensureProjectReleaseScript(rootDir, {
|
|
170
|
+
runPrompt,
|
|
171
|
+
runCommand,
|
|
172
|
+
logSuccess,
|
|
173
|
+
logWarning,
|
|
174
|
+
interactive: currentExecutionMode.interactive,
|
|
175
|
+
releaseScriptName: RELEASE_SCRIPT_NAME,
|
|
176
|
+
releaseScriptCommand: RELEASE_SCRIPT_COMMAND
|
|
177
|
+
})
|
|
88
178
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
179
|
+
const packageJsonPath = path.join(rootDir, 'package.json')
|
|
180
|
+
const composerJsonPath = path.join(rootDir, 'composer.json')
|
|
181
|
+
const hasPackageJson = await fs.access(packageJsonPath).then(() => true).catch(() => false)
|
|
182
|
+
const hasComposerJson = await fs.access(composerJsonPath).then(() => true).catch(() => false)
|
|
93
183
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
184
|
+
if (hasPackageJson || hasComposerJson) {
|
|
185
|
+
logProcessing('Validating dependencies...')
|
|
186
|
+
await validateLocalDependencies(rootDir, runPrompt, logSuccess, {
|
|
187
|
+
interactive: currentExecutionMode.interactive
|
|
188
|
+
})
|
|
189
|
+
}
|
|
98
190
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
191
|
+
const {deploymentConfig} = await selectDeploymentTarget(rootDir, {
|
|
192
|
+
configurationService,
|
|
193
|
+
runPrompt,
|
|
194
|
+
logProcessing,
|
|
195
|
+
logSuccess,
|
|
196
|
+
logWarning,
|
|
197
|
+
emitEvent,
|
|
198
|
+
executionMode: currentExecutionMode
|
|
199
|
+
})
|
|
106
200
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
201
|
+
const snapshotToUse = await resolvePendingSnapshot(rootDir, deploymentConfig, {
|
|
202
|
+
runPrompt,
|
|
203
|
+
logProcessing,
|
|
204
|
+
logWarning,
|
|
205
|
+
executionMode: currentExecutionMode
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
await runRemoteTasks(deploymentConfig, {
|
|
209
|
+
rootDir,
|
|
210
|
+
snapshot: snapshotToUse,
|
|
211
|
+
versionArg: options.versionArg,
|
|
212
|
+
context: appContext
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
emitEvent?.('run_completed', {
|
|
216
|
+
message: 'Zephyr workflow completed successfully.',
|
|
217
|
+
data: {
|
|
218
|
+
version: ZEPHYR_VERSION,
|
|
219
|
+
workflow: currentExecutionMode.workflow
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
} catch (error) {
|
|
223
|
+
const errorCode = getErrorCode(error)
|
|
224
|
+
emitEvent?.('run_failed', {
|
|
225
|
+
message: error.message,
|
|
226
|
+
code: errorCode,
|
|
227
|
+
data: {
|
|
228
|
+
version: ZEPHYR_VERSION,
|
|
229
|
+
workflow: currentExecutionMode.workflow
|
|
230
|
+
}
|
|
231
|
+
})
|
|
112
232
|
|
|
113
|
-
|
|
233
|
+
if (!currentExecutionMode.json) {
|
|
234
|
+
logError(error.message)
|
|
235
|
+
if (errorCode === 'ZEPHYR_FAILURE' && error.stack) {
|
|
236
|
+
writeStderrLine(error.stack)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
throw error
|
|
241
|
+
}
|
|
114
242
|
}
|
|
115
243
|
|
|
116
244
|
export {main, runRemoteTasks}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs/promises'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
|
|
4
|
+
import {ZephyrError} from '../runtime/errors.mjs'
|
|
5
|
+
|
|
4
6
|
export async function ensureGitignoreEntry(rootDir, {
|
|
5
7
|
projectConfigDir = '.zephyr',
|
|
6
8
|
runCommand,
|
|
@@ -66,6 +68,7 @@ export async function ensureProjectReleaseScript(rootDir, {
|
|
|
66
68
|
runCommand,
|
|
67
69
|
logSuccess,
|
|
68
70
|
logWarning,
|
|
71
|
+
interactive = true,
|
|
69
72
|
releaseScriptName = 'release',
|
|
70
73
|
releaseScriptCommand = 'npx @wyxos/zephyr@latest'
|
|
71
74
|
} = {}) {
|
|
@@ -96,6 +99,13 @@ export async function ensureProjectReleaseScript(rootDir, {
|
|
|
96
99
|
return false
|
|
97
100
|
}
|
|
98
101
|
|
|
102
|
+
if (!interactive) {
|
|
103
|
+
throw new ZephyrError(
|
|
104
|
+
'Zephyr cannot run non-interactively because package.json is missing the Zephyr release script. Add `"release": "npx @wyxos/zephyr@latest"` and rerun.',
|
|
105
|
+
{code: 'ZEPHYR_RELEASE_SCRIPT_REQUIRED'}
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
99
109
|
const { installReleaseScript } = await runPrompt([
|
|
100
110
|
{
|
|
101
111
|
type: 'confirm',
|
|
@@ -144,4 +154,3 @@ export async function ensureProjectReleaseScript(rootDir, {
|
|
|
144
154
|
|
|
145
155
|
return true
|
|
146
156
|
}
|
|
147
|
-
|
package/src/release/shared.mjs
CHANGED
|
@@ -65,14 +65,23 @@ export function parseReleaseArgs({
|
|
|
65
65
|
|
|
66
66
|
export async function runReleaseCommand(command, args, {
|
|
67
67
|
cwd = process.cwd(),
|
|
68
|
-
capture = false
|
|
68
|
+
capture = false,
|
|
69
|
+
runCommandImpl = runCommandBase,
|
|
70
|
+
runCommandCaptureImpl = runCommandCaptureBase
|
|
69
71
|
} = {}) {
|
|
70
72
|
if (capture) {
|
|
71
|
-
const
|
|
73
|
+
const captured = await runCommandCaptureImpl(command, args, { cwd })
|
|
74
|
+
|
|
75
|
+
if (typeof captured === 'string') {
|
|
76
|
+
return { stdout: captured.trim(), stderr: '' }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const stdout = captured?.stdout ?? ''
|
|
80
|
+
const stderr = captured?.stderr ?? ''
|
|
72
81
|
return { stdout: stdout.trim(), stderr: stderr.trim() }
|
|
73
82
|
}
|
|
74
83
|
|
|
75
|
-
await
|
|
84
|
+
await runCommandImpl(command, args, { cwd })
|
|
76
85
|
return undefined
|
|
77
86
|
}
|
|
78
87
|
|
|
@@ -91,9 +100,10 @@ export async function ensureCleanWorkingTree(rootDir = process.cwd(), {
|
|
|
91
100
|
|
|
92
101
|
export async function validateReleaseDependencies(rootDir = process.cwd(), {
|
|
93
102
|
prompt = (questions) => inquirer.prompt(questions),
|
|
94
|
-
logSuccess
|
|
103
|
+
logSuccess,
|
|
104
|
+
interactive = true
|
|
95
105
|
} = {}) {
|
|
96
|
-
await validateLocalDependencies(rootDir, prompt, logSuccess)
|
|
106
|
+
await validateLocalDependencies(rootDir, prompt, logSuccess, { interactive })
|
|
97
107
|
}
|
|
98
108
|
|
|
99
109
|
export async function ensureReleaseBranchReady({
|
package/src/release-node.mjs
CHANGED
|
@@ -1,27 +1,37 @@
|
|
|
1
1
|
import process from 'node:process'
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
parseReleaseArgs,
|
|
6
|
-
} from './release/shared.mjs'
|
|
2
|
+
import {createAppContext} from './runtime/app-context.mjs'
|
|
3
|
+
import {parseReleaseArgs} from './release/shared.mjs'
|
|
7
4
|
import {releaseNodePackage} from './application/release/release-node-package.mjs'
|
|
8
5
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
export async function releaseNode(options = {}) {
|
|
7
|
+
const parsed = options.releaseType
|
|
8
|
+
? options
|
|
9
|
+
: parseReleaseArgs({
|
|
10
|
+
booleanFlags: ['--skip-tests', '--skip-lint', '--skip-build', '--skip-deploy']
|
|
11
|
+
})
|
|
12
|
+
const rootDir = options.rootDir ?? process.cwd()
|
|
13
|
+
const context = options.context ?? createAppContext({
|
|
14
|
+
executionMode: {
|
|
15
|
+
interactive: true,
|
|
16
|
+
json: false,
|
|
17
|
+
workflow: 'release-node'
|
|
18
|
+
}
|
|
14
19
|
})
|
|
15
|
-
const
|
|
20
|
+
const {logProcessing: logStep, logSuccess, logWarning, runPrompt, runCommand, runCommandCapture, executionMode} = context
|
|
21
|
+
|
|
16
22
|
await releaseNodePackage({
|
|
17
|
-
releaseType,
|
|
18
|
-
skipTests,
|
|
19
|
-
skipLint,
|
|
20
|
-
skipBuild,
|
|
21
|
-
skipDeploy,
|
|
23
|
+
releaseType: parsed.releaseType,
|
|
24
|
+
skipTests: parsed.skipTests === true,
|
|
25
|
+
skipLint: parsed.skipLint === true,
|
|
26
|
+
skipBuild: parsed.skipBuild === true,
|
|
27
|
+
skipDeploy: parsed.skipDeploy === true,
|
|
22
28
|
rootDir,
|
|
23
29
|
logStep,
|
|
24
30
|
logSuccess,
|
|
25
|
-
logWarning
|
|
31
|
+
logWarning,
|
|
32
|
+
runPrompt,
|
|
33
|
+
runCommandImpl: runCommand,
|
|
34
|
+
runCommandCaptureImpl: runCommandCapture,
|
|
35
|
+
interactive: executionMode?.interactive !== false
|
|
26
36
|
})
|
|
27
37
|
}
|
|
@@ -1,25 +1,36 @@
|
|
|
1
1
|
import process from 'node:process'
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
parseReleaseArgs,
|
|
6
|
-
} from './release/shared.mjs'
|
|
2
|
+
import {createAppContext} from './runtime/app-context.mjs'
|
|
3
|
+
import {parseReleaseArgs} from './release/shared.mjs'
|
|
7
4
|
import {releasePackagistPackage} from './application/release/release-packagist-package.mjs'
|
|
8
5
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
export async function releasePackagist(options = {}) {
|
|
7
|
+
const parsed = options.releaseType
|
|
8
|
+
? options
|
|
9
|
+
: parseReleaseArgs({
|
|
10
|
+
booleanFlags: ['--skip-tests', '--skip-lint']
|
|
11
|
+
})
|
|
12
|
+
const rootDir = options.rootDir ?? process.cwd()
|
|
13
|
+
const context = options.context ?? createAppContext({
|
|
14
|
+
executionMode: {
|
|
15
|
+
interactive: true,
|
|
16
|
+
json: false,
|
|
17
|
+
workflow: 'release-packagist'
|
|
18
|
+
}
|
|
14
19
|
})
|
|
15
|
-
const
|
|
20
|
+
const {logProcessing: logStep, logSuccess, logWarning, runPrompt, runCommand, runCommandCapture, executionMode} = context
|
|
21
|
+
|
|
16
22
|
await releasePackagistPackage({
|
|
17
|
-
releaseType,
|
|
18
|
-
skipTests,
|
|
19
|
-
skipLint,
|
|
23
|
+
releaseType: parsed.releaseType,
|
|
24
|
+
skipTests: parsed.skipTests === true,
|
|
25
|
+
skipLint: parsed.skipLint === true,
|
|
20
26
|
rootDir,
|
|
21
27
|
logStep,
|
|
22
28
|
logSuccess,
|
|
23
|
-
logWarning
|
|
29
|
+
logWarning,
|
|
30
|
+
runPrompt,
|
|
31
|
+
runCommandImpl: runCommand,
|
|
32
|
+
runCommandCaptureImpl: runCommandCapture,
|
|
33
|
+
interactive: executionMode?.interactive !== false,
|
|
34
|
+
progressWriter: executionMode?.json ? process.stderr : process.stdout
|
|
24
35
|
})
|
|
25
36
|
}
|
|
@@ -2,7 +2,7 @@ import chalk from 'chalk'
|
|
|
2
2
|
import inquirer from 'inquirer'
|
|
3
3
|
import {NodeSSH} from 'node-ssh'
|
|
4
4
|
|
|
5
|
-
import {createChalkLogger} from '../utils/output.mjs'
|
|
5
|
+
import {createChalkLogger, createJsonEventEmitter, createJsonLogger} from '../utils/output.mjs'
|
|
6
6
|
import {runCommand as runCommandBase, runCommandCapture as runCommandCaptureBase} from '../utils/command.mjs'
|
|
7
7
|
import {createLocalCommandRunners} from './local-command.mjs'
|
|
8
8
|
import {createRunPrompt} from './prompt.mjs'
|
|
@@ -13,16 +13,41 @@ export function createAppContext({
|
|
|
13
13
|
inquirerInstance = inquirer,
|
|
14
14
|
NodeSSHClass = NodeSSH,
|
|
15
15
|
runCommandImpl = runCommandBase,
|
|
16
|
-
runCommandCaptureImpl = runCommandCaptureBase
|
|
16
|
+
runCommandCaptureImpl = runCommandCaptureBase,
|
|
17
|
+
executionMode = {}
|
|
17
18
|
} = {}) {
|
|
18
|
-
const
|
|
19
|
-
|
|
19
|
+
const normalizedExecutionMode = {
|
|
20
|
+
interactive: executionMode.interactive !== false,
|
|
21
|
+
json: executionMode.json === true,
|
|
22
|
+
workflow: executionMode.workflow ?? 'deploy',
|
|
23
|
+
presetName: executionMode.presetName ?? null,
|
|
24
|
+
maintenanceMode: executionMode.maintenanceMode ?? null,
|
|
25
|
+
resumePending: executionMode.resumePending === true,
|
|
26
|
+
discardPending: executionMode.discardPending === true
|
|
27
|
+
}
|
|
28
|
+
const emitEvent = normalizedExecutionMode.json
|
|
29
|
+
? createJsonEventEmitter({workflow: normalizedExecutionMode.workflow})
|
|
30
|
+
: null
|
|
31
|
+
const {logProcessing, logSuccess, logWarning, logError} = normalizedExecutionMode.json
|
|
32
|
+
? createJsonLogger({emitEvent})
|
|
33
|
+
: createChalkLogger(chalkInstance)
|
|
34
|
+
const runPrompt = createRunPrompt({
|
|
35
|
+
inquirer: inquirerInstance,
|
|
36
|
+
interactive: normalizedExecutionMode.interactive,
|
|
37
|
+
emitEvent,
|
|
38
|
+
workflow: normalizedExecutionMode.workflow
|
|
39
|
+
})
|
|
20
40
|
const createSshClient = createSshClientFactory({NodeSSH: NodeSSHClass})
|
|
21
41
|
const {runCommand, runCommandCapture} = createLocalCommandRunners({
|
|
22
42
|
runCommandBase: runCommandImpl,
|
|
23
43
|
runCommandCaptureBase: runCommandCaptureImpl
|
|
24
44
|
})
|
|
25
45
|
|
|
46
|
+
const runCommandWithMode = (command, args, options = {}) => runCommand(command, args, {
|
|
47
|
+
...options,
|
|
48
|
+
forwardStdoutToStderr: normalizedExecutionMode.json
|
|
49
|
+
})
|
|
50
|
+
|
|
26
51
|
return {
|
|
27
52
|
logProcessing,
|
|
28
53
|
logSuccess,
|
|
@@ -30,7 +55,9 @@ export function createAppContext({
|
|
|
30
55
|
logError,
|
|
31
56
|
runPrompt,
|
|
32
57
|
createSshClient,
|
|
33
|
-
runCommand,
|
|
34
|
-
runCommandCapture
|
|
58
|
+
runCommand: runCommandWithMode,
|
|
59
|
+
runCommandCapture,
|
|
60
|
+
emitEvent,
|
|
61
|
+
executionMode: normalizedExecutionMode
|
|
35
62
|
}
|
|
36
63
|
}
|