@wyxos/zephyr 0.6.0 → 0.7.4
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 +27 -3
- package/package.json +1 -1
- package/src/application/configuration/preset-selection.mjs +0 -6
- package/src/application/configuration/select-deployment-target.mjs +108 -71
- package/src/application/deploy/build-remote-deployment-plan.mjs +13 -1
- package/src/application/deploy/plan-laravel-deployment-tasks.mjs +23 -2
- package/src/application/deploy/prepare-local-deployment.mjs +18 -9
- package/src/application/deploy/run-deployment.mjs +5 -1
- package/src/application/deploy/run-local-deployment-checks.mjs +38 -2
- package/src/application/release/release-node-package.mjs +68 -12
- package/src/application/release/release-packagist-package.mjs +56 -12
- package/src/cli/options.mjs +28 -1
- package/src/config/preset-options.mjs +89 -0
- package/src/config/project.mjs +45 -10
- package/src/deploy/local-repo.mjs +29 -16
- package/src/deploy/preflight.mjs +57 -3
- package/src/main.mjs +54 -4
- package/src/release/commit-message.mjs +8 -239
- package/src/release-node.mjs +8 -1
- package/src/release-packagist.mjs +10 -3
- package/src/runtime/app-context.mjs +10 -1
|
@@ -3,6 +3,7 @@ import {readFile} from 'node:fs/promises'
|
|
|
3
3
|
import fs from 'node:fs'
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
import process from 'node:process'
|
|
6
|
+
import semver from 'semver'
|
|
6
7
|
|
|
7
8
|
import {writeStderr} from '../../utils/output.mjs'
|
|
8
9
|
import {
|
|
@@ -20,6 +21,44 @@ async function readPackage(rootDir = process.cwd()) {
|
|
|
20
21
|
return JSON.parse(raw)
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
function ensureValidPackageVersion(pkg) {
|
|
25
|
+
const version = pkg?.version
|
|
26
|
+
|
|
27
|
+
if (!version) {
|
|
28
|
+
throw new Error('package.json does not have a version field. Add a valid semver version before using --skip-versioning.')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!semver.valid(version)) {
|
|
32
|
+
throw new Error(`Invalid current version "${version}" in package.json. Must be a valid semver.`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return version
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function ensureReleaseTagMissing(version, rootDir = process.cwd(), {
|
|
39
|
+
runCommand = runReleaseCommand
|
|
40
|
+
} = {}) {
|
|
41
|
+
const tagName = `v${version}`
|
|
42
|
+
const {stdout} = await runCommand('git', ['tag', '-l', tagName], {capture: true, cwd: rootDir})
|
|
43
|
+
|
|
44
|
+
if (stdout.trim() === tagName) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`Release tag ${tagName} already exists. Remove the existing tag or update package.json before using --skip-versioning.`
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return tagName
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function createReleaseTag(version, rootDir = process.cwd(), {
|
|
54
|
+
logStep,
|
|
55
|
+
runCommand = runReleaseCommand
|
|
56
|
+
} = {}) {
|
|
57
|
+
const tagName = `v${version}`
|
|
58
|
+
logStep?.(`Creating release tag ${tagName}...`)
|
|
59
|
+
await runCommand('git', ['tag', '-a', tagName, '-m', tagName], {capture: true, cwd: rootDir})
|
|
60
|
+
}
|
|
61
|
+
|
|
23
62
|
function hasScript(pkg, scriptName) {
|
|
24
63
|
return pkg?.scripts?.[scriptName] !== undefined
|
|
25
64
|
}
|
|
@@ -354,6 +393,7 @@ export async function releaseNodePackage({
|
|
|
354
393
|
skipGitHooks = false,
|
|
355
394
|
skipTests = false,
|
|
356
395
|
skipLint = false,
|
|
396
|
+
skipVersioning = false,
|
|
357
397
|
skipBuild = false,
|
|
358
398
|
skipDeploy = false,
|
|
359
399
|
rootDir = process.cwd(),
|
|
@@ -373,6 +413,7 @@ export async function releaseNodePackage({
|
|
|
373
413
|
|
|
374
414
|
logStep?.('Reading package metadata...')
|
|
375
415
|
const pkg = await readPackage(rootDir)
|
|
416
|
+
const currentVersion = skipVersioning ? ensureValidPackageVersion(pkg) : null
|
|
376
417
|
|
|
377
418
|
logStep?.('Validating dependencies...')
|
|
378
419
|
await validateReleaseDependencies(rootDir, {
|
|
@@ -393,24 +434,39 @@ export async function releaseNodePackage({
|
|
|
393
434
|
skipGitHooks
|
|
394
435
|
})
|
|
395
436
|
await ensureReleaseBranchReady({rootDir, branchMethod: 'show-current', logStep, logWarning})
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
currentVersion
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
437
|
+
|
|
438
|
+
if (skipVersioning) {
|
|
439
|
+
await ensureReleaseTagMissing(currentVersion, rootDir, {runCommand})
|
|
440
|
+
logWarning?.(
|
|
441
|
+
`Skipping package.json version update because --skip-versioning flag was provided. Releasing current version ${currentVersion}.`
|
|
442
|
+
)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const resolvedReleaseType = skipVersioning
|
|
446
|
+
? null
|
|
447
|
+
: await resolveReleaseType({
|
|
448
|
+
releaseType,
|
|
449
|
+
currentVersion: pkg.version,
|
|
450
|
+
packageName: pkg.name,
|
|
451
|
+
rootDir,
|
|
452
|
+
interactive,
|
|
453
|
+
runPrompt,
|
|
454
|
+
runCommand,
|
|
455
|
+
logStep,
|
|
456
|
+
logWarning
|
|
457
|
+
})
|
|
407
458
|
|
|
408
459
|
await runLint(skipLint, pkg, rootDir, {logStep, logSuccess, logWarning, runCommand})
|
|
409
460
|
await runTests(skipTests, pkg, rootDir, {logStep, logSuccess, logWarning, runCommand})
|
|
410
461
|
await runLibBuild(skipBuild, pkg, rootDir, {logStep, logSuccess, logWarning, runCommand, skipGitHooks})
|
|
411
462
|
|
|
412
|
-
const updatedPkg =
|
|
463
|
+
const updatedPkg = skipVersioning
|
|
464
|
+
? {...pkg, version: currentVersion}
|
|
465
|
+
: await bumpVersion(resolvedReleaseType, rootDir, {logStep, logSuccess, runCommand, skipGitHooks})
|
|
413
466
|
await runBuild(skipBuild, updatedPkg, rootDir, {logStep, logSuccess, logWarning, runCommand})
|
|
467
|
+
if (skipVersioning) {
|
|
468
|
+
await createReleaseTag(updatedPkg.version, rootDir, {logStep, runCommand})
|
|
469
|
+
}
|
|
414
470
|
await pushChanges(rootDir, {logStep, logSuccess, runCommand, skipGitHooks})
|
|
415
471
|
await deployGHPages(skipDeploy, updatedPkg, rootDir, {logStep, logSuccess, logWarning, runCommand, skipGitHooks})
|
|
416
472
|
|
|
@@ -20,6 +20,30 @@ async function readComposer(rootDir = process.cwd()) {
|
|
|
20
20
|
return JSON.parse(raw)
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
async function ensureReleaseTagMissing(version, rootDir = process.cwd(), {
|
|
24
|
+
runCommand = runReleaseCommand
|
|
25
|
+
} = {}) {
|
|
26
|
+
const tagName = `v${version}`
|
|
27
|
+
const {stdout} = await runCommand('git', ['tag', '-l', tagName], {capture: true, cwd: rootDir})
|
|
28
|
+
|
|
29
|
+
if (stdout.trim() === tagName) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Release tag ${tagName} already exists. Remove the existing tag or update composer.json before using --skip-versioning.`
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return tagName
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function createReleaseTag(version, rootDir = process.cwd(), {
|
|
39
|
+
logStep,
|
|
40
|
+
runCommand = runReleaseCommand
|
|
41
|
+
} = {}) {
|
|
42
|
+
const tagName = `v${version}`
|
|
43
|
+
logStep?.(`Creating git tag ${tagName}...`)
|
|
44
|
+
await runCommand('git', ['tag', tagName], {cwd: rootDir})
|
|
45
|
+
}
|
|
46
|
+
|
|
23
47
|
async function writeComposer(rootDir, composer, composerPath = null) {
|
|
24
48
|
const pathToUse = composerPath || join(rootDir, 'composer.json')
|
|
25
49
|
const content = JSON.stringify(composer, null, 2) + '\n'
|
|
@@ -218,6 +242,7 @@ export async function releasePackagistPackage({
|
|
|
218
242
|
skipGitHooks = false,
|
|
219
243
|
skipTests = false,
|
|
220
244
|
skipLint = false,
|
|
245
|
+
skipVersioning = false,
|
|
221
246
|
rootDir = process.cwd(),
|
|
222
247
|
logStep,
|
|
223
248
|
logSuccess,
|
|
@@ -241,6 +266,10 @@ export async function releasePackagistPackage({
|
|
|
241
266
|
throw new Error('composer.json does not have a version field. Add "version": "0.0.0" to composer.json.')
|
|
242
267
|
}
|
|
243
268
|
|
|
269
|
+
if (skipVersioning && !semver.valid(composer.version)) {
|
|
270
|
+
throw new Error(`Invalid current version "${composer.version}" in composer.json. Must be a valid semver.`)
|
|
271
|
+
}
|
|
272
|
+
|
|
244
273
|
logStep?.('Validating dependencies...')
|
|
245
274
|
await validateReleaseDependencies(rootDir, {
|
|
246
275
|
prompt: runPrompt,
|
|
@@ -260,22 +289,37 @@ export async function releasePackagistPackage({
|
|
|
260
289
|
skipGitHooks
|
|
261
290
|
})
|
|
262
291
|
await ensureReleaseBranchReady({rootDir, branchMethod: 'show-current', logStep, logWarning})
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
292
|
+
|
|
293
|
+
if (skipVersioning) {
|
|
294
|
+
await ensureReleaseTagMissing(composer.version, rootDir, {runCommand})
|
|
295
|
+
logWarning?.(
|
|
296
|
+
`Skipping composer.json version update because --skip-versioning flag was provided. Releasing current version ${composer.version}.`
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const resolvedReleaseType = skipVersioning
|
|
301
|
+
? null
|
|
302
|
+
: await resolveReleaseType({
|
|
303
|
+
releaseType,
|
|
304
|
+
currentVersion: composer.version,
|
|
305
|
+
packageName: composer.name,
|
|
306
|
+
rootDir,
|
|
307
|
+
interactive,
|
|
308
|
+
runPrompt,
|
|
309
|
+
runCommand,
|
|
310
|
+
logStep,
|
|
311
|
+
logWarning
|
|
312
|
+
})
|
|
274
313
|
|
|
275
314
|
await runLint(skipLint, rootDir, {logStep, logSuccess, logWarning, runCommand, progressWriter})
|
|
276
315
|
await runTests(skipTests, composer, rootDir, {logStep, logSuccess, logWarning, runCommand, progressWriter})
|
|
277
316
|
|
|
278
|
-
const updatedComposer =
|
|
317
|
+
const updatedComposer = skipVersioning
|
|
318
|
+
? {...composer}
|
|
319
|
+
: await bumpVersion(resolvedReleaseType, rootDir, {logStep, logSuccess, runCommand, skipGitHooks})
|
|
320
|
+
if (skipVersioning) {
|
|
321
|
+
await createReleaseTag(updatedComposer.version, rootDir, {logStep, runCommand})
|
|
322
|
+
}
|
|
279
323
|
await pushChanges(rootDir, {logStep, logSuccess, runCommand, skipGitHooks})
|
|
280
324
|
|
|
281
325
|
logSuccess?.(`Release workflow completed for ${composer.name}@${updatedComposer.version}.`)
|
package/src/cli/options.mjs
CHANGED
|
@@ -5,6 +5,11 @@ import {Command} from 'commander'
|
|
|
5
5
|
import {InvalidCliOptionsError} from '../runtime/errors.mjs'
|
|
6
6
|
|
|
7
7
|
const WORKFLOW_TYPES = new Set(['node', 'vue', 'packagist'])
|
|
8
|
+
|
|
9
|
+
function hasFlag(args = [], flag) {
|
|
10
|
+
return args.some((arg) => arg === flag || arg.startsWith(`${flag}=`))
|
|
11
|
+
}
|
|
12
|
+
|
|
8
13
|
function normalizeMaintenanceMode(value) {
|
|
9
14
|
if (value == null) {
|
|
10
15
|
return null
|
|
@@ -35,6 +40,8 @@ export function parseCliOptions(args = process.argv.slice(2)) {
|
|
|
35
40
|
.option('--resume-pending', 'Resume a saved pending deployment snapshot without prompting.')
|
|
36
41
|
.option('--discard-pending', 'Discard a saved pending deployment snapshot without prompting.')
|
|
37
42
|
.option('--maintenance <mode>', 'Laravel maintenance mode policy for app deployments (on|off).')
|
|
43
|
+
.option('--auto-commit', 'Automatically commit dirty deploy changes with a Codex-generated message.')
|
|
44
|
+
.option('--skip-versioning', 'Skip updating package/composer version files before continuing.')
|
|
38
45
|
.option('--skip-git-hooks', 'Bypass local git hooks for any commits and pushes Zephyr performs.')
|
|
39
46
|
.option('--skip-checks', 'Skip Zephyr local lint and test execution.')
|
|
40
47
|
.option('--skip-tests', 'Skip Zephyr local test execution in package release and app deployment workflows.')
|
|
@@ -54,6 +61,7 @@ export function parseCliOptions(args = process.argv.slice(2)) {
|
|
|
54
61
|
|
|
55
62
|
const options = program.opts()
|
|
56
63
|
const workflowType = options.type ?? null
|
|
64
|
+
const explicitSkipChecks = hasFlag(args, '--skip-checks')
|
|
57
65
|
|
|
58
66
|
if (workflowType && !WORKFLOW_TYPES.has(workflowType)) {
|
|
59
67
|
throw new InvalidCliOptionsError('Invalid value for --type. Use one of: node, vue, packagist.')
|
|
@@ -68,12 +76,21 @@ export function parseCliOptions(args = process.argv.slice(2)) {
|
|
|
68
76
|
resumePending: Boolean(options.resumePending),
|
|
69
77
|
discardPending: Boolean(options.discardPending),
|
|
70
78
|
maintenanceMode: normalizeMaintenanceMode(options.maintenance),
|
|
79
|
+
autoCommit: Boolean(options.autoCommit),
|
|
80
|
+
skipVersioning: Boolean(options.skipVersioning),
|
|
71
81
|
skipGitHooks: Boolean(options.skipGitHooks),
|
|
72
82
|
skipChecks: Boolean(options.skipChecks),
|
|
73
83
|
skipTests: Boolean(options.skipTests || options.skipChecks),
|
|
74
84
|
skipLint: Boolean(options.skipLint || options.skipChecks),
|
|
75
85
|
skipBuild: Boolean(options.skipBuild),
|
|
76
|
-
skipDeploy: Boolean(options.skipDeploy)
|
|
86
|
+
skipDeploy: Boolean(options.skipDeploy),
|
|
87
|
+
explicitMaintenanceMode: hasFlag(args, '--maintenance'),
|
|
88
|
+
explicitAutoCommit: hasFlag(args, '--auto-commit'),
|
|
89
|
+
explicitSkipVersioning: hasFlag(args, '--skip-versioning'),
|
|
90
|
+
explicitSkipGitHooks: hasFlag(args, '--skip-git-hooks'),
|
|
91
|
+
explicitSkipChecks,
|
|
92
|
+
explicitSkipTests: hasFlag(args, '--skip-tests') || explicitSkipChecks,
|
|
93
|
+
explicitSkipLint: hasFlag(args, '--skip-lint') || explicitSkipChecks
|
|
77
94
|
}
|
|
78
95
|
}
|
|
79
96
|
|
|
@@ -86,6 +103,8 @@ export function validateCliOptions(options = {}) {
|
|
|
86
103
|
resumePending = false,
|
|
87
104
|
discardPending = false,
|
|
88
105
|
maintenanceMode = null,
|
|
106
|
+
autoCommit = false,
|
|
107
|
+
skipVersioning = false,
|
|
89
108
|
skipBuild = false,
|
|
90
109
|
skipDeploy = false
|
|
91
110
|
} = options
|
|
@@ -112,6 +131,10 @@ export function validateCliOptions(options = {}) {
|
|
|
112
131
|
if (maintenanceMode !== null) {
|
|
113
132
|
throw new InvalidCliOptionsError('--maintenance is only valid for app deployments.')
|
|
114
133
|
}
|
|
134
|
+
|
|
135
|
+
if (autoCommit) {
|
|
136
|
+
throw new InvalidCliOptionsError('--auto-commit is only valid for app deployments.')
|
|
137
|
+
}
|
|
115
138
|
} else {
|
|
116
139
|
if (skipBuild || skipDeploy) {
|
|
117
140
|
throw new InvalidCliOptionsError('--skip-build and --skip-deploy are only valid for node/vue release workflows.')
|
|
@@ -121,4 +144,8 @@ export function validateCliOptions(options = {}) {
|
|
|
121
144
|
throw new InvalidCliOptionsError('--non-interactive app deployments require --preset <name>.')
|
|
122
145
|
}
|
|
123
146
|
}
|
|
147
|
+
|
|
148
|
+
if (skipVersioning && options.versionArg) {
|
|
149
|
+
throw new InvalidCliOptionsError('--skip-versioning cannot be used together with an explicit version or bump argument.')
|
|
150
|
+
}
|
|
124
151
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export const DEFAULT_PRESET_OPTIONS = Object.freeze({
|
|
2
|
+
maintenanceMode: null,
|
|
3
|
+
skipGitHooks: false,
|
|
4
|
+
skipTests: false,
|
|
5
|
+
skipLint: false,
|
|
6
|
+
skipVersioning: false,
|
|
7
|
+
autoCommit: false
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
function normalizeBoolean(value, fallback = false) {
|
|
11
|
+
return typeof value === 'boolean' ? value : fallback
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizeMaintenanceMode(value) {
|
|
15
|
+
return typeof value === 'boolean' ? value : null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function normalizePresetOptions(options = {}) {
|
|
19
|
+
return {
|
|
20
|
+
maintenanceMode: normalizeMaintenanceMode(options?.maintenanceMode),
|
|
21
|
+
skipGitHooks: normalizeBoolean(options?.skipGitHooks),
|
|
22
|
+
skipTests: normalizeBoolean(options?.skipTests),
|
|
23
|
+
skipLint: normalizeBoolean(options?.skipLint),
|
|
24
|
+
skipVersioning: normalizeBoolean(options?.skipVersioning),
|
|
25
|
+
autoCommit: normalizeBoolean(options?.autoCommit)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function mergeDeployOptions(executionMode = {}, presetOptions = {}) {
|
|
30
|
+
const normalizedPresetOptions = normalizePresetOptions(presetOptions)
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
maintenanceMode: executionMode.explicitMaintenanceMode === true
|
|
34
|
+
? executionMode.maintenanceMode
|
|
35
|
+
: normalizedPresetOptions.maintenanceMode,
|
|
36
|
+
skipGitHooks: executionMode.explicitSkipGitHooks === true
|
|
37
|
+
? executionMode.skipGitHooks === true
|
|
38
|
+
: normalizedPresetOptions.skipGitHooks,
|
|
39
|
+
skipTests: executionMode.explicitSkipTests === true
|
|
40
|
+
? executionMode.skipTests === true
|
|
41
|
+
: normalizedPresetOptions.skipTests,
|
|
42
|
+
skipLint: executionMode.explicitSkipLint === true
|
|
43
|
+
? executionMode.skipLint === true
|
|
44
|
+
: normalizedPresetOptions.skipLint,
|
|
45
|
+
skipVersioning: executionMode.explicitSkipVersioning === true
|
|
46
|
+
? executionMode.skipVersioning === true
|
|
47
|
+
: normalizedPresetOptions.skipVersioning,
|
|
48
|
+
autoCommit: executionMode.explicitAutoCommit === true
|
|
49
|
+
? executionMode.autoCommit === true
|
|
50
|
+
: normalizedPresetOptions.autoCommit
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function buildPresetOptionsFromExecutionMode(executionMode = {}, existingOptions = {}) {
|
|
55
|
+
const normalizedOptions = normalizePresetOptions(existingOptions)
|
|
56
|
+
|
|
57
|
+
if (executionMode.explicitMaintenanceMode === true) {
|
|
58
|
+
normalizedOptions.maintenanceMode = executionMode.maintenanceMode
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (executionMode.explicitSkipGitHooks === true) {
|
|
62
|
+
normalizedOptions.skipGitHooks = executionMode.skipGitHooks === true
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (executionMode.explicitSkipTests === true) {
|
|
66
|
+
normalizedOptions.skipTests = executionMode.skipTests === true
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (executionMode.explicitSkipLint === true) {
|
|
70
|
+
normalizedOptions.skipLint = executionMode.skipLint === true
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (executionMode.explicitSkipVersioning === true) {
|
|
74
|
+
normalizedOptions.skipVersioning = executionMode.skipVersioning === true
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (executionMode.explicitAutoCommit === true) {
|
|
78
|
+
normalizedOptions.autoCommit = executionMode.autoCommit === true
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return normalizedOptions
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function presetOptionsEqual(left = {}, right = {}) {
|
|
85
|
+
const normalizedLeft = normalizePresetOptions(left)
|
|
86
|
+
const normalizedRight = normalizePresetOptions(right)
|
|
87
|
+
|
|
88
|
+
return JSON.stringify(normalizedLeft) === JSON.stringify(normalizedRight)
|
|
89
|
+
}
|
package/src/config/project.mjs
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs/promises'
|
|
2
2
|
|
|
3
3
|
import {ZephyrError} from '../runtime/errors.mjs'
|
|
4
|
+
import {
|
|
5
|
+
normalizePresetOptions,
|
|
6
|
+
presetOptionsEqual
|
|
7
|
+
} from './preset-options.mjs'
|
|
4
8
|
import { ensureDirectory, getProjectConfigDir, getProjectConfigPath } from '../utils/paths.mjs'
|
|
5
9
|
import { generateId } from '../utils/id.mjs'
|
|
6
10
|
|
|
@@ -44,27 +48,58 @@ export function migratePresets(presets, apps) {
|
|
|
44
48
|
return { presets: [], needsMigration: false }
|
|
45
49
|
}
|
|
46
50
|
|
|
47
|
-
const
|
|
51
|
+
const appLookup = new Map()
|
|
48
52
|
apps.forEach((app) => {
|
|
49
53
|
if (app.id && app.serverName && app.projectPath) {
|
|
50
54
|
const key = `${app.serverName}:${app.projectPath}`
|
|
51
|
-
|
|
55
|
+
appLookup.set(key, app)
|
|
52
56
|
}
|
|
53
57
|
})
|
|
54
58
|
|
|
55
59
|
let needsMigration = false
|
|
56
|
-
const migrated = presets.
|
|
57
|
-
|
|
60
|
+
const migrated = presets.flatMap((preset) => {
|
|
61
|
+
if (!preset || typeof preset !== 'object') {
|
|
62
|
+
needsMigration = true
|
|
63
|
+
return []
|
|
64
|
+
}
|
|
58
65
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
const updated = {
|
|
67
|
+
name: typeof preset.name === 'string' ? preset.name : '',
|
|
68
|
+
appId: typeof preset.appId === 'string' ? preset.appId : null,
|
|
69
|
+
branch: typeof preset.branch === 'string' ? preset.branch : null,
|
|
70
|
+
options: normalizePresetOptions(preset.options)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!presetOptionsEqual(updated.options, preset.options)) {
|
|
74
|
+
needsMigration = true
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (preset.key) {
|
|
78
|
+
needsMigration = true
|
|
79
|
+
const [serverName = null, projectPath = null, legacyBranch = null] = String(preset.key).split(':')
|
|
80
|
+
const app = serverName && projectPath
|
|
81
|
+
? appLookup.get(`${serverName}:${projectPath}`)
|
|
82
|
+
: null
|
|
83
|
+
|
|
84
|
+
if (app?.id) {
|
|
85
|
+
updated.appId = app.id
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!updated.branch && legacyBranch) {
|
|
89
|
+
updated.branch = legacyBranch
|
|
64
90
|
}
|
|
65
91
|
}
|
|
66
92
|
|
|
67
|
-
|
|
93
|
+
if (!updated.name) {
|
|
94
|
+
needsMigration = true
|
|
95
|
+
return []
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!preset.appId || preset.key || preset.branch !== updated.branch) {
|
|
99
|
+
needsMigration = true
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return [updated]
|
|
68
103
|
})
|
|
69
104
|
|
|
70
105
|
return { presets: migrated, needsMigration }
|
|
@@ -177,6 +177,7 @@ async function commitAndPushPendingChanges(targetBranch, rootDir, {
|
|
|
177
177
|
logProcessing,
|
|
178
178
|
logSuccess,
|
|
179
179
|
logWarning,
|
|
180
|
+
autoCommit = false,
|
|
180
181
|
skipGitHooks = false,
|
|
181
182
|
suggestCommitMessage = suggestCommitMessageImpl
|
|
182
183
|
} = {}) {
|
|
@@ -186,7 +187,7 @@ async function commitAndPushPendingChanges(targetBranch, rootDir, {
|
|
|
186
187
|
return
|
|
187
188
|
}
|
|
188
189
|
|
|
189
|
-
if (typeof runPrompt !== 'function') {
|
|
190
|
+
if (!autoCommit && typeof runPrompt !== 'function') {
|
|
190
191
|
throw new Error(DIRTY_DEPLOYMENT_MESSAGE)
|
|
191
192
|
}
|
|
192
193
|
|
|
@@ -213,24 +214,34 @@ async function commitAndPushPendingChanges(targetBranch, rootDir, {
|
|
|
213
214
|
logWarning,
|
|
214
215
|
statusEntries
|
|
215
216
|
})
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
message
|
|
221
|
-
'Pending changes detected before deployment:\n\n' +
|
|
222
|
-
`${formatWorkingTreePreview(statusEntries)}\n\n` +
|
|
223
|
-
'Enter a commit message to stage and commit all current changes before continuing.\n' +
|
|
224
|
-
'Leave blank to cancel.',
|
|
225
|
-
default: suggestedCommitMessage ?? ''
|
|
217
|
+
let message = suggestedCommitMessage?.trim() ?? ''
|
|
218
|
+
|
|
219
|
+
if (autoCommit) {
|
|
220
|
+
if (!message) {
|
|
221
|
+
throw new Error('Deployment auto-commit failed because Codex could not determine a usable commit message.')
|
|
226
222
|
}
|
|
227
|
-
])
|
|
228
223
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
224
|
+
logProcessing?.(`Auto-commit enabled. Using Codex-generated commit message "${message}".`)
|
|
225
|
+
} else {
|
|
226
|
+
const { commitMessage } = await runPrompt([
|
|
227
|
+
{
|
|
228
|
+
type: 'input',
|
|
229
|
+
name: 'commitMessage',
|
|
230
|
+
message:
|
|
231
|
+
'Pending changes detected before deployment:\n\n' +
|
|
232
|
+
`${formatWorkingTreePreview(statusEntries)}\n\n` +
|
|
233
|
+
'Enter a commit message to stage and commit all current changes before continuing.\n' +
|
|
234
|
+
'Leave blank to cancel.',
|
|
235
|
+
default: message
|
|
236
|
+
}
|
|
237
|
+
])
|
|
232
238
|
|
|
233
|
-
|
|
239
|
+
if (!commitMessage || commitMessage.trim().length === 0) {
|
|
240
|
+
throw new Error(DIRTY_DEPLOYMENT_CANCELLED_MESSAGE)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
message = commitMessage.trim()
|
|
244
|
+
}
|
|
234
245
|
|
|
235
246
|
logProcessing?.('Staging all pending changes before deployment...')
|
|
236
247
|
await runCommand('git', ['add', '-A'], { cwd: rootDir })
|
|
@@ -357,6 +368,7 @@ export async function ensureLocalRepositoryState(targetBranch, rootDir = process
|
|
|
357
368
|
logProcessing,
|
|
358
369
|
logSuccess,
|
|
359
370
|
logWarning,
|
|
371
|
+
autoCommit = false,
|
|
360
372
|
skipGitHooks = false,
|
|
361
373
|
suggestCommitMessage: suggestCommitMessageFn = suggestCommitMessageImpl,
|
|
362
374
|
getCurrentBranch: getCurrentBranchFn = getCurrentBranch,
|
|
@@ -436,6 +448,7 @@ export async function ensureLocalRepositoryState(targetBranch, rootDir = process
|
|
|
436
448
|
logProcessing,
|
|
437
449
|
logSuccess,
|
|
438
450
|
logWarning,
|
|
451
|
+
autoCommit,
|
|
439
452
|
skipGitHooks,
|
|
440
453
|
suggestCommitMessage: suggestCommitMessageFn
|
|
441
454
|
})
|
package/src/deploy/preflight.mjs
CHANGED
|
@@ -25,17 +25,30 @@ export async function hasPrePushHook(rootDir) {
|
|
|
25
25
|
return false
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
async function readPackageJson(rootDir) {
|
|
29
|
+
const packageJsonPath = path.join(rootDir, 'package.json')
|
|
30
|
+
const raw = await fs.readFile(packageJsonPath, 'utf8')
|
|
31
|
+
return JSON.parse(raw)
|
|
32
|
+
}
|
|
33
|
+
|
|
28
34
|
export async function hasLintScript(rootDir) {
|
|
29
35
|
try {
|
|
30
|
-
const
|
|
31
|
-
const raw = await fs.readFile(packageJsonPath, 'utf8')
|
|
32
|
-
const packageJson = JSON.parse(raw)
|
|
36
|
+
const packageJson = await readPackageJson(rootDir)
|
|
33
37
|
return packageJson.scripts && typeof packageJson.scripts.lint === 'string'
|
|
34
38
|
} catch {
|
|
35
39
|
return false
|
|
36
40
|
}
|
|
37
41
|
}
|
|
38
42
|
|
|
43
|
+
export async function hasBuildScript(rootDir) {
|
|
44
|
+
try {
|
|
45
|
+
const packageJson = await readPackageJson(rootDir)
|
|
46
|
+
return packageJson.scripts && typeof packageJson.scripts.build === 'string'
|
|
47
|
+
} catch {
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
39
52
|
export async function hasLaravelPint(rootDir) {
|
|
40
53
|
try {
|
|
41
54
|
const pintPath = path.join(rootDir, 'vendor', 'bin', 'pint')
|
|
@@ -91,6 +104,28 @@ export async function resolveSupportedLintCommand(rootDir, {commandExists} = {})
|
|
|
91
104
|
throw error
|
|
92
105
|
}
|
|
93
106
|
|
|
107
|
+
export async function resolveSupportedBuildCommand(rootDir, {commandExists} = {}) {
|
|
108
|
+
const hasNpmBuild = await hasBuildScript(rootDir)
|
|
109
|
+
|
|
110
|
+
if (!hasNpmBuild) {
|
|
111
|
+
return null
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (commandExists && !commandExists('npm')) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
'Release cannot run because `npm run build` is configured but npm is not available in PATH.\n' +
|
|
117
|
+
'Install npm or fix your PATH before deploying.'
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
type: 'npm',
|
|
123
|
+
command: 'npm',
|
|
124
|
+
args: ['run', 'build'],
|
|
125
|
+
label: 'npm build'
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
94
129
|
export async function runLinting(rootDir, {
|
|
95
130
|
runCommand,
|
|
96
131
|
logProcessing,
|
|
@@ -117,6 +152,25 @@ export async function runLinting(rootDir, {
|
|
|
117
152
|
return false
|
|
118
153
|
}
|
|
119
154
|
|
|
155
|
+
export async function runBuild(rootDir, {
|
|
156
|
+
runCommand,
|
|
157
|
+
logProcessing,
|
|
158
|
+
logSuccess,
|
|
159
|
+
commandExists,
|
|
160
|
+
buildCommand = null
|
|
161
|
+
} = {}) {
|
|
162
|
+
const selectedBuildCommand = buildCommand ?? await resolveSupportedBuildCommand(rootDir, {commandExists})
|
|
163
|
+
|
|
164
|
+
if (selectedBuildCommand === null) {
|
|
165
|
+
return false
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
logProcessing?.('Running local frontend build...')
|
|
169
|
+
await runCommand(selectedBuildCommand.command, selectedBuildCommand.args, {cwd: rootDir})
|
|
170
|
+
logSuccess?.('Local frontend build completed.')
|
|
171
|
+
return true
|
|
172
|
+
}
|
|
173
|
+
|
|
120
174
|
export function hasStagedChanges(statusOutput) {
|
|
121
175
|
if (!statusOutput || statusOutput.length === 0) {
|
|
122
176
|
return false
|