@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/src/config/project.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs/promises'
|
|
2
2
|
|
|
3
|
+
import {ZephyrError} from '../runtime/errors.mjs'
|
|
3
4
|
import { ensureDirectory, getProjectConfigDir, getProjectConfigPath } from '../utils/paths.mjs'
|
|
4
5
|
import { generateId } from '../utils/id.mjs'
|
|
5
6
|
|
|
@@ -69,7 +70,12 @@ export function migratePresets(presets, apps) {
|
|
|
69
70
|
return { presets: migrated, needsMigration }
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
export async function loadProjectConfig(rootDir, servers = [], {
|
|
73
|
+
export async function loadProjectConfig(rootDir, servers = [], {
|
|
74
|
+
logSuccess,
|
|
75
|
+
logWarning,
|
|
76
|
+
strict = false,
|
|
77
|
+
allowMigration = true
|
|
78
|
+
} = {}) {
|
|
73
79
|
const configPath = getProjectConfigPath(rootDir)
|
|
74
80
|
|
|
75
81
|
try {
|
|
@@ -82,6 +88,13 @@ export async function loadProjectConfig(rootDir, servers = [], { logSuccess, log
|
|
|
82
88
|
const { presets: migratedPresets, needsMigration: presetsNeedMigration } = migratePresets(presets, migratedApps)
|
|
83
89
|
|
|
84
90
|
if (appsNeedMigration || presetsNeedMigration) {
|
|
91
|
+
if (!allowMigration) {
|
|
92
|
+
throw new ZephyrError(
|
|
93
|
+
'Zephyr cannot run non-interactively because .zephyr/config.json needs migration. Rerun interactively once to update the config.',
|
|
94
|
+
{code: 'ZEPHYR_PROJECT_CONFIG_MIGRATION_REQUIRED'}
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
85
98
|
await saveProjectConfig(rootDir, {
|
|
86
99
|
apps: migratedApps,
|
|
87
100
|
presets: migratedPresets
|
|
@@ -92,9 +105,27 @@ export async function loadProjectConfig(rootDir, servers = [], { logSuccess, log
|
|
|
92
105
|
return { apps: migratedApps, presets: migratedPresets }
|
|
93
106
|
} catch (error) {
|
|
94
107
|
if (error.code === 'ENOENT') {
|
|
108
|
+
if (strict) {
|
|
109
|
+
throw new ZephyrError(
|
|
110
|
+
'Zephyr cannot run non-interactively because .zephyr/config.json does not exist. Run an interactive deployment first to create it.',
|
|
111
|
+
{code: 'ZEPHYR_PROJECT_CONFIG_MISSING'}
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
95
115
|
return { apps: [], presets: [] }
|
|
96
116
|
}
|
|
97
117
|
|
|
118
|
+
if (error instanceof ZephyrError) {
|
|
119
|
+
throw error
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (strict) {
|
|
123
|
+
throw new ZephyrError(
|
|
124
|
+
'Zephyr cannot run non-interactively because .zephyr/config.json could not be read.',
|
|
125
|
+
{code: 'ZEPHYR_PROJECT_CONFIG_INVALID', cause: error}
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
98
129
|
logWarning?.('Failed to read .zephyr/config.json, starting with an empty list of apps.')
|
|
99
130
|
return { apps: [], presets: [] }
|
|
100
131
|
}
|
package/src/config/servers.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'node:fs/promises'
|
|
|
2
2
|
import os from 'node:os'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
|
|
5
|
+
import {ZephyrError} from '../runtime/errors.mjs'
|
|
5
6
|
import { ensureDirectory } from '../utils/paths.mjs'
|
|
6
7
|
import { generateId } from '../utils/id.mjs'
|
|
7
8
|
|
|
@@ -25,7 +26,12 @@ export function migrateServers(servers) {
|
|
|
25
26
|
return { servers: migrated, needsMigration }
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
export async function loadServers({
|
|
29
|
+
export async function loadServers({
|
|
30
|
+
logSuccess,
|
|
31
|
+
logWarning,
|
|
32
|
+
strict = false,
|
|
33
|
+
allowMigration = true
|
|
34
|
+
} = {}) {
|
|
29
35
|
try {
|
|
30
36
|
const raw = await fs.readFile(SERVERS_FILE, 'utf8')
|
|
31
37
|
const data = JSON.parse(raw)
|
|
@@ -34,6 +40,13 @@ export async function loadServers({ logSuccess, logWarning } = {}) {
|
|
|
34
40
|
const { servers: migrated, needsMigration } = migrateServers(servers)
|
|
35
41
|
|
|
36
42
|
if (needsMigration) {
|
|
43
|
+
if (!allowMigration) {
|
|
44
|
+
throw new ZephyrError(
|
|
45
|
+
'Zephyr cannot run non-interactively because ~/.config/zephyr/servers.json needs migration. Rerun interactively once to update the config.',
|
|
46
|
+
{code: 'ZEPHYR_SERVERS_CONFIG_MIGRATION_REQUIRED'}
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
37
50
|
await saveServers(migrated)
|
|
38
51
|
logSuccess?.('Migrated servers configuration to use unique IDs.')
|
|
39
52
|
}
|
|
@@ -41,9 +54,27 @@ export async function loadServers({ logSuccess, logWarning } = {}) {
|
|
|
41
54
|
return migrated
|
|
42
55
|
} catch (error) {
|
|
43
56
|
if (error.code === 'ENOENT') {
|
|
57
|
+
if (strict) {
|
|
58
|
+
throw new ZephyrError(
|
|
59
|
+
'Zephyr cannot run non-interactively because ~/.config/zephyr/servers.json does not exist. Run an interactive deployment first to create it.',
|
|
60
|
+
{code: 'ZEPHYR_SERVERS_CONFIG_MISSING'}
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
44
64
|
return []
|
|
45
65
|
}
|
|
46
66
|
|
|
67
|
+
if (error instanceof ZephyrError) {
|
|
68
|
+
throw error
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (strict) {
|
|
72
|
+
throw new ZephyrError(
|
|
73
|
+
'Zephyr cannot run non-interactively because ~/.config/zephyr/servers.json could not be read.',
|
|
74
|
+
{code: 'ZEPHYR_SERVERS_CONFIG_INVALID', cause: error}
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
47
78
|
logWarning?.('Failed to read servers.json, starting with an empty list.')
|
|
48
79
|
return []
|
|
49
80
|
}
|
|
@@ -54,4 +85,3 @@ export async function saveServers(servers) {
|
|
|
54
85
|
const payload = JSON.stringify(servers, null, 2)
|
|
55
86
|
await fs.writeFile(SERVERS_FILE, `${payload}\n`)
|
|
56
87
|
}
|
|
57
|
-
|
|
@@ -2,6 +2,7 @@ import { readFile, writeFile } from 'node:fs/promises'
|
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import process from 'node:process'
|
|
4
4
|
import chalk from 'chalk'
|
|
5
|
+
import {ZephyrError} from './runtime/errors.mjs'
|
|
5
6
|
import { runCommand as runCommandBase, runCommandCapture as runCommandCaptureBase } from './utils/command.mjs'
|
|
6
7
|
|
|
7
8
|
function isLocalPathOutsideRepo(depPath, rootDir) {
|
|
@@ -262,7 +263,9 @@ async function commitDependencyUpdates(rootDir, updatedFiles, promptFn, logFn) {
|
|
|
262
263
|
return true
|
|
263
264
|
}
|
|
264
265
|
|
|
265
|
-
async function validateLocalDependencies(rootDir, promptFn, logFn = null
|
|
266
|
+
async function validateLocalDependencies(rootDir, promptFn, logFn = null, {
|
|
267
|
+
interactive = true
|
|
268
|
+
} = {}) {
|
|
266
269
|
const packageDeps = await scanPackageJsonDependencies(rootDir)
|
|
267
270
|
const composerDeps = await scanComposerJsonDependencies(rootDir)
|
|
268
271
|
|
|
@@ -308,6 +311,13 @@ async function validateLocalDependencies(rootDir, promptFn, logFn = null) {
|
|
|
308
311
|
const countText = allDeps.length === 1 ? 'dependency' : 'dependencies'
|
|
309
312
|
const promptMessage = `Found ${countColored} local file ${countText} pointing outside the repository:\n\n${messages.join('\n\n')}\n\nUpdate to latest version?`
|
|
310
313
|
|
|
314
|
+
if (!interactive) {
|
|
315
|
+
throw new ZephyrError(
|
|
316
|
+
'Zephyr cannot run non-interactively because local file dependencies point outside the repository and require confirmation to update.',
|
|
317
|
+
{code: 'ZEPHYR_DEPENDENCY_UPDATE_REQUIRED'}
|
|
318
|
+
)
|
|
319
|
+
}
|
|
320
|
+
|
|
311
321
|
// Prompt user
|
|
312
322
|
const { shouldUpdate } = await promptFn([
|
|
313
323
|
{
|
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({
|