@wyxos/zephyr 0.4.9 → 0.6.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 +6 -1
- package/package.json +1 -1
- package/src/application/deploy/prepare-local-deployment.mjs +25 -0
- package/src/application/deploy/run-deployment.mjs +2 -0
- package/src/application/deploy/run-local-deployment-checks.mjs +32 -9
- package/src/application/release/release-node-package.mjs +13 -1
- package/src/application/release/release-packagist-package.mjs +13 -1
- package/src/cli/options.mjs +8 -8
- package/src/deploy/local-repo.mjs +9 -21
- package/src/main.mjs +10 -2
- package/src/release/commit-message.mjs +79 -13
- package/src/release/release-type.mjs +293 -0
- package/src/release/shared.mjs +13 -32
- package/src/release-node.mjs +1 -1
- package/src/release-packagist.mjs +1 -1
- package/src/runtime/app-context.mjs +3 -0
package/README.md
CHANGED
|
@@ -40,9 +40,12 @@ Common workflows:
|
|
|
40
40
|
# Deploy an app using the saved preset or the interactive prompts
|
|
41
41
|
zephyr
|
|
42
42
|
|
|
43
|
-
# Deploy an app
|
|
43
|
+
# Deploy an app with a local npm package version bump
|
|
44
44
|
zephyr minor
|
|
45
45
|
|
|
46
|
+
# Deploy an app while bypassing Zephyr's built-in local checks
|
|
47
|
+
zephyr minor --skip-checks
|
|
48
|
+
|
|
46
49
|
# Deploy a configured app non-interactively
|
|
47
50
|
zephyr --non-interactive --preset wyxos-release --maintenance off
|
|
48
51
|
|
|
@@ -155,6 +158,8 @@ npm run release
|
|
|
155
158
|
- `npm run release` is the recommended app/package entrypoint once the release script has been installed.
|
|
156
159
|
- For `--type node` workflows, Zephyr runs your project's `lint` script when present.
|
|
157
160
|
- For `--type node` workflows, Zephyr runs `test:run` or `test` when present.
|
|
161
|
+
- For fresh Laravel app deploys, Zephyr runs local lint/test checks before creating the version-bump commit so failed checks do not force repeated bump retries.
|
|
162
|
+
- For critical app deploys, `--skip-checks` is shorthand for `--skip-lint --skip-tests`. It skips Zephyr's built-in local checks, but any local `pre-push` hook can still run its own checks during git push unless you also opt into `--skip-git-hooks`.
|
|
158
163
|
- For non-interactive app deploys, use a saved preset name instead of relying on prompt fallback.
|
|
159
164
|
|
|
160
165
|
## Features
|
package/package.json
CHANGED
|
@@ -10,6 +10,8 @@ export async function prepareLocalDeployment(config, {
|
|
|
10
10
|
rootDir = process.cwd(),
|
|
11
11
|
versionArg = null,
|
|
12
12
|
skipGitHooks = false,
|
|
13
|
+
skipTests = false,
|
|
14
|
+
skipLint = false,
|
|
13
15
|
runPrompt,
|
|
14
16
|
runCommand,
|
|
15
17
|
runCommandCapture,
|
|
@@ -31,10 +33,29 @@ export async function prepareLocalDeployment(config, {
|
|
|
31
33
|
const checkSupport = await resolveLocalDeploymentCheckSupport({
|
|
32
34
|
rootDir,
|
|
33
35
|
isLaravel: context.isLaravel,
|
|
36
|
+
skipTests,
|
|
37
|
+
skipLint,
|
|
34
38
|
runCommandCapture
|
|
35
39
|
})
|
|
36
40
|
|
|
37
41
|
if (!snapshot && context.isLaravel) {
|
|
42
|
+
await runLocalDeploymentChecks({
|
|
43
|
+
rootDir,
|
|
44
|
+
isLaravel: context.isLaravel,
|
|
45
|
+
hasHook: context.hasHook,
|
|
46
|
+
skipGitHooks,
|
|
47
|
+
skipTests,
|
|
48
|
+
skipLint,
|
|
49
|
+
forceRunWhenHookPresent: true,
|
|
50
|
+
runCommand,
|
|
51
|
+
runCommandCapture,
|
|
52
|
+
logProcessing,
|
|
53
|
+
logSuccess,
|
|
54
|
+
logWarning,
|
|
55
|
+
lintCommand: checkSupport.lintCommand,
|
|
56
|
+
testCommand: checkSupport.testCommand
|
|
57
|
+
})
|
|
58
|
+
|
|
38
59
|
await bumpLocalPackageVersion(rootDir, {
|
|
39
60
|
versionArg,
|
|
40
61
|
skipGitHooks,
|
|
@@ -52,6 +73,8 @@ export async function prepareLocalDeployment(config, {
|
|
|
52
73
|
logWarning,
|
|
53
74
|
skipGitHooks
|
|
54
75
|
})
|
|
76
|
+
|
|
77
|
+
return context
|
|
55
78
|
}
|
|
56
79
|
|
|
57
80
|
await runLocalDeploymentChecks({
|
|
@@ -59,6 +82,8 @@ export async function prepareLocalDeployment(config, {
|
|
|
59
82
|
isLaravel: context.isLaravel,
|
|
60
83
|
hasHook: context.hasHook,
|
|
61
84
|
skipGitHooks,
|
|
85
|
+
skipTests,
|
|
86
|
+
skipLint,
|
|
62
87
|
runCommand,
|
|
63
88
|
runCommandCapture,
|
|
64
89
|
logProcessing,
|
|
@@ -366,6 +366,8 @@ export async function runDeployment(config, options = {}) {
|
|
|
366
366
|
rootDir,
|
|
367
367
|
versionArg,
|
|
368
368
|
skipGitHooks: executionMode?.skipGitHooks === true,
|
|
369
|
+
skipTests: executionMode?.skipTests === true,
|
|
370
|
+
skipLint: executionMode?.skipLint === true,
|
|
369
371
|
runPrompt,
|
|
370
372
|
runCommand,
|
|
371
373
|
runCommandCapture: context.runCommandCapture,
|
|
@@ -50,19 +50,23 @@ async function resolveSupportedLaravelTestCommand(rootDir, {runCommandCapture} =
|
|
|
50
50
|
export async function resolveLocalDeploymentCheckSupport({
|
|
51
51
|
rootDir,
|
|
52
52
|
isLaravel,
|
|
53
|
+
skipTests = false,
|
|
54
|
+
skipLint = false,
|
|
53
55
|
runCommandCapture
|
|
54
56
|
} = {}) {
|
|
55
57
|
let lintCommand = null
|
|
56
58
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
if (!skipLint) {
|
|
60
|
+
try {
|
|
61
|
+
lintCommand = await preflight.resolveSupportedLintCommand(rootDir, {commandExists})
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (error?.code !== 'ZEPHYR_LINT_COMMAND_NOT_FOUND') {
|
|
64
|
+
throw error
|
|
65
|
+
}
|
|
62
66
|
}
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
const testCommand = isLaravel
|
|
69
|
+
const testCommand = isLaravel && !skipTests
|
|
66
70
|
? await resolveSupportedLaravelTestCommand(rootDir, {runCommandCapture})
|
|
67
71
|
: null
|
|
68
72
|
|
|
@@ -95,6 +99,9 @@ export async function runLocalDeploymentChecks({
|
|
|
95
99
|
isLaravel,
|
|
96
100
|
hasHook,
|
|
97
101
|
skipGitHooks = false,
|
|
102
|
+
skipTests = false,
|
|
103
|
+
skipLint = false,
|
|
104
|
+
forceRunWhenHookPresent = false,
|
|
98
105
|
runCommand,
|
|
99
106
|
runCommandCapture,
|
|
100
107
|
logProcessing,
|
|
@@ -108,6 +115,8 @@ export async function runLocalDeploymentChecks({
|
|
|
108
115
|
: await resolveLocalDeploymentCheckSupport({
|
|
109
116
|
rootDir,
|
|
110
117
|
isLaravel,
|
|
118
|
+
skipTests,
|
|
119
|
+
skipLint,
|
|
111
120
|
runCommandCapture
|
|
112
121
|
})
|
|
113
122
|
|
|
@@ -116,6 +125,10 @@ export async function runLocalDeploymentChecks({
|
|
|
116
125
|
logWarning?.(
|
|
117
126
|
'Pre-push git hook detected. Zephyr will run its built-in release checks manually because --skip-git-hooks is enabled, and the hook will be bypassed during git push.'
|
|
118
127
|
)
|
|
128
|
+
} else if (forceRunWhenHookPresent) {
|
|
129
|
+
logProcessing?.(
|
|
130
|
+
'Pre-push git hook detected. Zephyr will run its built-in release checks now before bumping the deployment version. The hook will still run again during git push.'
|
|
131
|
+
)
|
|
119
132
|
} else {
|
|
120
133
|
logProcessing?.(
|
|
121
134
|
'Pre-push git hook detected. Built-in release checks are supported, but Zephyr will skip executing them here. If Zephyr pushes local commits during this release, the hook will run during git push.'
|
|
@@ -124,11 +137,19 @@ export async function runLocalDeploymentChecks({
|
|
|
124
137
|
}
|
|
125
138
|
}
|
|
126
139
|
|
|
127
|
-
if (
|
|
140
|
+
if (hasHook && !skipGitHooks && (skipLint || skipTests)) {
|
|
141
|
+
logWarning?.(
|
|
142
|
+
'Pre-push git hook detected. --skip-lint/--skip-tests only skip Zephyr\'s built-in checks; your hook may still run its own checks during git push.'
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (skipLint) {
|
|
147
|
+
logWarning?.('Skipping lint because --skip-lint flag was provided.')
|
|
148
|
+
} else if (support.lintCommand === null) {
|
|
128
149
|
logWarning?.('No supported lint command was found. Skipping linting checks.')
|
|
129
150
|
}
|
|
130
151
|
|
|
131
|
-
const lintRan = support.lintCommand === null
|
|
152
|
+
const lintRan = skipLint || support.lintCommand === null
|
|
132
153
|
? false
|
|
133
154
|
: await preflight.runLinting(rootDir, {
|
|
134
155
|
runCommand,
|
|
@@ -152,7 +173,9 @@ export async function runLocalDeploymentChecks({
|
|
|
152
173
|
}
|
|
153
174
|
}
|
|
154
175
|
|
|
155
|
-
if (isLaravel) {
|
|
176
|
+
if (isLaravel && skipTests) {
|
|
177
|
+
logWarning?.('Skipping tests because --skip-tests flag was provided.')
|
|
178
|
+
} else if (isLaravel) {
|
|
156
179
|
await runLocalLaravelTests(rootDir, {
|
|
157
180
|
runCommand,
|
|
158
181
|
logProcessing,
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
runReleaseCommand,
|
|
12
12
|
validateReleaseDependencies
|
|
13
13
|
} from '../../release/shared.mjs'
|
|
14
|
+
import {resolveReleaseType} from '../../release/release-type.mjs'
|
|
14
15
|
import {gitCommitArgs, gitPushArgs, npmVersionArgs} from '../../utils/git-hooks.mjs'
|
|
15
16
|
|
|
16
17
|
async function readPackage(rootDir = process.cwd()) {
|
|
@@ -392,12 +393,23 @@ export async function releaseNodePackage({
|
|
|
392
393
|
skipGitHooks
|
|
393
394
|
})
|
|
394
395
|
await ensureReleaseBranchReady({rootDir, branchMethod: 'show-current', logStep, logWarning})
|
|
396
|
+
const resolvedReleaseType = await resolveReleaseType({
|
|
397
|
+
releaseType,
|
|
398
|
+
currentVersion: pkg.version,
|
|
399
|
+
packageName: pkg.name,
|
|
400
|
+
rootDir,
|
|
401
|
+
interactive,
|
|
402
|
+
runPrompt,
|
|
403
|
+
runCommand,
|
|
404
|
+
logStep,
|
|
405
|
+
logWarning
|
|
406
|
+
})
|
|
395
407
|
|
|
396
408
|
await runLint(skipLint, pkg, rootDir, {logStep, logSuccess, logWarning, runCommand})
|
|
397
409
|
await runTests(skipTests, pkg, rootDir, {logStep, logSuccess, logWarning, runCommand})
|
|
398
410
|
await runLibBuild(skipBuild, pkg, rootDir, {logStep, logSuccess, logWarning, runCommand, skipGitHooks})
|
|
399
411
|
|
|
400
|
-
const updatedPkg = await bumpVersion(
|
|
412
|
+
const updatedPkg = await bumpVersion(resolvedReleaseType, rootDir, {logStep, logSuccess, runCommand, skipGitHooks})
|
|
401
413
|
await runBuild(skipBuild, updatedPkg, rootDir, {logStep, logSuccess, logWarning, runCommand})
|
|
402
414
|
await pushChanges(rootDir, {logStep, logSuccess, runCommand, skipGitHooks})
|
|
403
415
|
await deployGHPages(skipDeploy, updatedPkg, rootDir, {logStep, logSuccess, logWarning, runCommand, skipGitHooks})
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
runReleaseCommand,
|
|
12
12
|
validateReleaseDependencies
|
|
13
13
|
} from '../../release/shared.mjs'
|
|
14
|
+
import {resolveReleaseType} from '../../release/release-type.mjs'
|
|
14
15
|
import {gitCommitArgs, gitPushArgs} from '../../utils/git-hooks.mjs'
|
|
15
16
|
|
|
16
17
|
async function readComposer(rootDir = process.cwd()) {
|
|
@@ -259,11 +260,22 @@ export async function releasePackagistPackage({
|
|
|
259
260
|
skipGitHooks
|
|
260
261
|
})
|
|
261
262
|
await ensureReleaseBranchReady({rootDir, branchMethod: 'show-current', logStep, logWarning})
|
|
263
|
+
const resolvedReleaseType = await resolveReleaseType({
|
|
264
|
+
releaseType,
|
|
265
|
+
currentVersion: composer.version,
|
|
266
|
+
packageName: composer.name,
|
|
267
|
+
rootDir,
|
|
268
|
+
interactive,
|
|
269
|
+
runPrompt,
|
|
270
|
+
runCommand,
|
|
271
|
+
logStep,
|
|
272
|
+
logWarning
|
|
273
|
+
})
|
|
262
274
|
|
|
263
275
|
await runLint(skipLint, rootDir, {logStep, logSuccess, logWarning, runCommand, progressWriter})
|
|
264
276
|
await runTests(skipTests, composer, rootDir, {logStep, logSuccess, logWarning, runCommand, progressWriter})
|
|
265
277
|
|
|
266
|
-
const updatedComposer = await bumpVersion(
|
|
278
|
+
const updatedComposer = await bumpVersion(resolvedReleaseType, rootDir, {logStep, logSuccess, runCommand, skipGitHooks})
|
|
267
279
|
await pushChanges(rootDir, {logStep, logSuccess, runCommand, skipGitHooks})
|
|
268
280
|
|
|
269
281
|
logSuccess?.(`Release workflow completed for ${composer.name}@${updatedComposer.version}.`)
|
package/src/cli/options.mjs
CHANGED
|
@@ -36,8 +36,9 @@ export function parseCliOptions(args = process.argv.slice(2)) {
|
|
|
36
36
|
.option('--discard-pending', 'Discard a saved pending deployment snapshot without prompting.')
|
|
37
37
|
.option('--maintenance <mode>', 'Laravel maintenance mode policy for app deployments (on|off).')
|
|
38
38
|
.option('--skip-git-hooks', 'Bypass local git hooks for any commits and pushes Zephyr performs.')
|
|
39
|
-
.option('--skip-
|
|
40
|
-
.option('--skip-
|
|
39
|
+
.option('--skip-checks', 'Skip Zephyr local lint and test execution.')
|
|
40
|
+
.option('--skip-tests', 'Skip Zephyr local test execution in package release and app deployment workflows.')
|
|
41
|
+
.option('--skip-lint', 'Skip Zephyr local lint execution in package release and app deployment workflows.')
|
|
41
42
|
.option('--skip-build', 'Skip build execution in node/vue release workflows.')
|
|
42
43
|
.option('--skip-deploy', 'Skip GitHub Pages deployment in node/vue release workflows.')
|
|
43
44
|
.argument(
|
|
@@ -68,8 +69,9 @@ export function parseCliOptions(args = process.argv.slice(2)) {
|
|
|
68
69
|
discardPending: Boolean(options.discardPending),
|
|
69
70
|
maintenanceMode: normalizeMaintenanceMode(options.maintenance),
|
|
70
71
|
skipGitHooks: Boolean(options.skipGitHooks),
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
skipChecks: Boolean(options.skipChecks),
|
|
73
|
+
skipTests: Boolean(options.skipTests || options.skipChecks),
|
|
74
|
+
skipLint: Boolean(options.skipLint || options.skipChecks),
|
|
73
75
|
skipBuild: Boolean(options.skipBuild),
|
|
74
76
|
skipDeploy: Boolean(options.skipDeploy)
|
|
75
77
|
}
|
|
@@ -84,8 +86,6 @@ export function validateCliOptions(options = {}) {
|
|
|
84
86
|
resumePending = false,
|
|
85
87
|
discardPending = false,
|
|
86
88
|
maintenanceMode = null,
|
|
87
|
-
skipTests = false,
|
|
88
|
-
skipLint = false,
|
|
89
89
|
skipBuild = false,
|
|
90
90
|
skipDeploy = false
|
|
91
91
|
} = options
|
|
@@ -113,8 +113,8 @@ export function validateCliOptions(options = {}) {
|
|
|
113
113
|
throw new InvalidCliOptionsError('--maintenance is only valid for app deployments.')
|
|
114
114
|
}
|
|
115
115
|
} else {
|
|
116
|
-
if (
|
|
117
|
-
throw new InvalidCliOptionsError('
|
|
116
|
+
if (skipBuild || skipDeploy) {
|
|
117
|
+
throw new InvalidCliOptionsError('--skip-build and --skip-deploy are only valid for node/vue release workflows.')
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
if (nonInteractive && !presetName) {
|
|
@@ -2,7 +2,6 @@ import { getCurrentBranch as getCurrentBranchImpl, getUpstreamRef as getUpstream
|
|
|
2
2
|
import {hasPrePushHook} from './preflight.mjs'
|
|
3
3
|
import {gitCommitArgs, gitPushArgs} from '../utils/git-hooks.mjs'
|
|
4
4
|
import {
|
|
5
|
-
buildFallbackCommitMessage,
|
|
6
5
|
formatWorkingTreePreview,
|
|
7
6
|
parseWorkingTreeEntries,
|
|
8
7
|
suggestCommitMessage as suggestCommitMessageImpl
|
|
@@ -213,35 +212,24 @@ async function commitAndPushPendingChanges(targetBranch, rootDir, {
|
|
|
213
212
|
logStep: logProcessing,
|
|
214
213
|
logWarning,
|
|
215
214
|
statusEntries
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
const changeLabel = statusEntries.length === 1 ? 'change' : 'changes'
|
|
219
|
-
const {shouldCommitPendingChanges} = await runPrompt([
|
|
215
|
+
})
|
|
216
|
+
const { commitMessage } = await runPrompt([
|
|
220
217
|
{
|
|
221
|
-
type: '
|
|
222
|
-
name: '
|
|
218
|
+
type: 'input',
|
|
219
|
+
name: 'commitMessage',
|
|
223
220
|
message:
|
|
224
|
-
|
|
221
|
+
'Pending changes detected before deployment:\n\n' +
|
|
225
222
|
`${formatWorkingTreePreview(statusEntries)}\n\n` +
|
|
226
|
-
'
|
|
227
|
-
|
|
223
|
+
'Enter a commit message to stage and commit all current changes before continuing.\n' +
|
|
224
|
+
'Leave blank to cancel.',
|
|
225
|
+
default: suggestedCommitMessage ?? ''
|
|
228
226
|
}
|
|
229
227
|
])
|
|
230
228
|
|
|
231
|
-
if (!
|
|
229
|
+
if (!commitMessage || commitMessage.trim().length === 0) {
|
|
232
230
|
throw new Error(DIRTY_DEPLOYMENT_CANCELLED_MESSAGE)
|
|
233
231
|
}
|
|
234
232
|
|
|
235
|
-
const { commitMessage } = await runPrompt([
|
|
236
|
-
{
|
|
237
|
-
type: 'input',
|
|
238
|
-
name: 'commitMessage',
|
|
239
|
-
message: 'Commit message for pending deployment changes',
|
|
240
|
-
default: suggestedCommitMessage,
|
|
241
|
-
validate: (value) => (value && value.trim().length > 0 ? true : 'Commit message cannot be empty.')
|
|
242
|
-
}
|
|
243
|
-
])
|
|
244
|
-
|
|
245
233
|
const message = commitMessage.trim()
|
|
246
234
|
|
|
247
235
|
logProcessing?.('Staging all pending changes before deployment...')
|
package/src/main.mjs
CHANGED
|
@@ -36,8 +36,9 @@ function normalizeMainOptions(firstArg = null, secondArg = null) {
|
|
|
36
36
|
discardPending: firstArg.discardPending === true,
|
|
37
37
|
maintenanceMode: firstArg.maintenanceMode ?? null,
|
|
38
38
|
skipGitHooks: firstArg.skipGitHooks === true,
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
skipChecks: firstArg.skipChecks === true,
|
|
40
|
+
skipTests: firstArg.skipTests === true || firstArg.skipChecks === true,
|
|
41
|
+
skipLint: firstArg.skipLint === true || firstArg.skipChecks === true,
|
|
41
42
|
skipBuild: firstArg.skipBuild === true,
|
|
42
43
|
skipDeploy: firstArg.skipDeploy === true,
|
|
43
44
|
context: firstArg.context ?? null
|
|
@@ -54,6 +55,7 @@ function normalizeMainOptions(firstArg = null, secondArg = null) {
|
|
|
54
55
|
discardPending: false,
|
|
55
56
|
maintenanceMode: null,
|
|
56
57
|
skipGitHooks: false,
|
|
58
|
+
skipChecks: false,
|
|
57
59
|
skipTests: false,
|
|
58
60
|
skipLint: false,
|
|
59
61
|
skipBuild: false,
|
|
@@ -109,6 +111,9 @@ async function main(optionsOrWorkflowType = null, versionArg = null) {
|
|
|
109
111
|
presetName: options.presetName,
|
|
110
112
|
maintenanceMode: options.maintenanceMode,
|
|
111
113
|
skipGitHooks: options.skipGitHooks === true,
|
|
114
|
+
skipChecks: options.skipChecks === true,
|
|
115
|
+
skipTests: options.skipTests === true,
|
|
116
|
+
skipLint: options.skipLint === true,
|
|
112
117
|
resumePending: options.resumePending,
|
|
113
118
|
discardPending: options.discardPending
|
|
114
119
|
}
|
|
@@ -138,6 +143,9 @@ async function main(optionsOrWorkflowType = null, versionArg = null) {
|
|
|
138
143
|
presetName: currentExecutionMode.presetName,
|
|
139
144
|
maintenanceMode: currentExecutionMode.maintenanceMode,
|
|
140
145
|
skipGitHooks: currentExecutionMode.skipGitHooks === true,
|
|
146
|
+
skipChecks: currentExecutionMode.skipChecks === true,
|
|
147
|
+
skipTests: currentExecutionMode.skipTests === true,
|
|
148
|
+
skipLint: currentExecutionMode.skipLint === true,
|
|
141
149
|
resumePending: currentExecutionMode.resumePending,
|
|
142
150
|
discardPending: currentExecutionMode.discardPending
|
|
143
151
|
}
|
|
@@ -20,9 +20,12 @@ const GENERIC_SUBJECT_PATTERNS = [
|
|
|
20
20
|
/^update work$/i,
|
|
21
21
|
/^misc(ellaneous)?( updates?)?$/i,
|
|
22
22
|
/^changes$/i,
|
|
23
|
-
/^updates?$/i
|
|
23
|
+
/^updates?$/i,
|
|
24
|
+
/^(improve|update|adjust|refine|align|support|enable)\s+.+\s+(workflow|process|flow)$/i
|
|
24
25
|
]
|
|
25
26
|
const MAX_WORKING_TREE_PREVIEW = 20
|
|
27
|
+
const MAX_DIFF_EXCERPT_LINES = 60
|
|
28
|
+
const MAX_DIFF_EXCERPT_CHARS = 4000
|
|
26
29
|
const STATUS_LABELS = {
|
|
27
30
|
A: 'added',
|
|
28
31
|
C: 'copied',
|
|
@@ -261,10 +264,10 @@ export function buildFallbackCommitMessage(statusEntries = []) {
|
|
|
261
264
|
}
|
|
262
265
|
|
|
263
266
|
if (commitType === 'ci') {
|
|
264
|
-
return `ci: update ${primaryTopic}
|
|
267
|
+
return `ci: update ${primaryTopic} pipeline`
|
|
265
268
|
}
|
|
266
269
|
|
|
267
|
-
return `chore:
|
|
270
|
+
return `chore: update ${primaryTopic} handling`
|
|
268
271
|
}
|
|
269
272
|
|
|
270
273
|
async function collectDiffNumstat(rootDir, {runCommand} = {}) {
|
|
@@ -297,12 +300,68 @@ async function collectDiffNumstat(rootDir, {runCommand} = {}) {
|
|
|
297
300
|
}
|
|
298
301
|
}
|
|
299
302
|
|
|
303
|
+
function shouldIncludeDiffExcerptLine(line = '') {
|
|
304
|
+
if (line.startsWith('diff --git ')) {
|
|
305
|
+
return true
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (line.startsWith('@@')) {
|
|
309
|
+
return true
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return (line.startsWith('+') || line.startsWith('-')) && !line.startsWith('+++') && !line.startsWith('---')
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function trimDiffExcerpt(diffText = '') {
|
|
316
|
+
const selectedLines = []
|
|
317
|
+
let totalChars = 0
|
|
318
|
+
|
|
319
|
+
for (const rawLine of diffText.split(/\r?\n/)) {
|
|
320
|
+
const line = rawLine.trimEnd()
|
|
321
|
+
|
|
322
|
+
if (!shouldIncludeDiffExcerptLine(line)) {
|
|
323
|
+
continue
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const truncatedLine = line.length > 180 ? `${line.slice(0, 177)}...` : line
|
|
327
|
+
const nextTotal = totalChars + truncatedLine.length + 1
|
|
328
|
+
|
|
329
|
+
if (selectedLines.length >= MAX_DIFF_EXCERPT_LINES || nextTotal > MAX_DIFF_EXCERPT_CHARS) {
|
|
330
|
+
break
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
selectedLines.push(truncatedLine)
|
|
334
|
+
totalChars = nextTotal
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return selectedLines.join('\n')
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function collectDiffExcerpt(rootDir, {runCommand} = {}) {
|
|
341
|
+
try {
|
|
342
|
+
const {stdout} = await runCommand('git', ['diff', '--unified=0', '--no-color', 'HEAD', '--'], {
|
|
343
|
+
capture: true,
|
|
344
|
+
cwd: rootDir
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
return trimDiffExcerpt(stdout)
|
|
348
|
+
} catch {
|
|
349
|
+
return ''
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
300
353
|
async function buildCommitMessageContext(rootDir, {
|
|
301
354
|
runCommand,
|
|
302
355
|
statusEntries = []
|
|
303
356
|
} = {}) {
|
|
304
357
|
const changeCountsByPath = await collectDiffNumstat(rootDir, {runCommand})
|
|
305
|
-
|
|
358
|
+
const summary = statusEntries.map((entry) => `- ${summarizeWorkingTreeEntry(entry, {changeCountsByPath})}`).join('\n')
|
|
359
|
+
const diffExcerpt = await collectDiffExcerpt(rootDir, {runCommand})
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
summary,
|
|
363
|
+
diffExcerpt
|
|
364
|
+
}
|
|
306
365
|
}
|
|
307
366
|
|
|
308
367
|
export async function suggestCommitMessage(rootDir = process.cwd(), {
|
|
@@ -328,6 +387,21 @@ export async function suggestCommitMessage(rootDir = process.cwd(), {
|
|
|
328
387
|
|
|
329
388
|
logStep?.('Generating a suggested commit message with Codex...')
|
|
330
389
|
|
|
390
|
+
const promptSections = [
|
|
391
|
+
'Write exactly one short conventional commit message for these pending changes.',
|
|
392
|
+
'Use the exact format "<type>: <subject>" with no scope, no exclamation mark, and no extra text.',
|
|
393
|
+
'Choose the most appropriate type from: fix, feat, chore, docs, refactor, test, style, perf, build, ci, revert.',
|
|
394
|
+
'Base the subject on the actual behavior, safeguard, bug fix, feature, or API change shown below.',
|
|
395
|
+
'Avoid generic nouns like "workflow", "process", "flow", "changes", or "updates" unless the diff truly changes CI or docs.',
|
|
396
|
+
'Do not describe the commit itself, staging, or "pending changes"; describe the underlying code or product change.',
|
|
397
|
+
'Pending change summary:',
|
|
398
|
+
commitContext.summary || '- changed files present'
|
|
399
|
+
]
|
|
400
|
+
|
|
401
|
+
if (commitContext.diffExcerpt) {
|
|
402
|
+
promptSections.push('Diff excerpt:', commitContext.diffExcerpt)
|
|
403
|
+
}
|
|
404
|
+
|
|
331
405
|
await runCommand('codex', [
|
|
332
406
|
'exec',
|
|
333
407
|
'--ephemeral',
|
|
@@ -338,15 +412,7 @@ export async function suggestCommitMessage(rootDir = process.cwd(), {
|
|
|
338
412
|
'--skip-git-repo-check',
|
|
339
413
|
'--output-last-message',
|
|
340
414
|
outputPath,
|
|
341
|
-
|
|
342
|
-
'Write exactly one short conventional commit message for these pending changes.',
|
|
343
|
-
'Use the exact format "<type>: <subject>" with no scope, no exclamation mark, and no extra text.',
|
|
344
|
-
'Choose the most appropriate type from: fix, feat, chore, docs, refactor, test, style, perf, build, ci, revert.',
|
|
345
|
-
'Make the subject specific enough to describe the actual behavior or workflow change, not just that files changed.',
|
|
346
|
-
'Do not describe the commit itself, staging, or "pending changes"; describe the underlying behavior or workflow fix.',
|
|
347
|
-
'Pending change summary:',
|
|
348
|
-
commitContext || '- changed files present'
|
|
349
|
-
].join('\n\n')
|
|
415
|
+
promptSections.join('\n\n')
|
|
350
416
|
], {
|
|
351
417
|
capture: true,
|
|
352
418
|
cwd: rootDir
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import {mkdtemp, readFile, rm} from 'node:fs/promises'
|
|
2
|
+
import {tmpdir} from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import process from 'node:process'
|
|
5
|
+
import semver from 'semver'
|
|
6
|
+
|
|
7
|
+
import {commandExists} from '../utils/command.mjs'
|
|
8
|
+
|
|
9
|
+
export const RELEASE_TYPES = [
|
|
10
|
+
'major',
|
|
11
|
+
'minor',
|
|
12
|
+
'patch',
|
|
13
|
+
'premajor',
|
|
14
|
+
'preminor',
|
|
15
|
+
'prepatch',
|
|
16
|
+
'prerelease'
|
|
17
|
+
]
|
|
18
|
+
const STABLE_RELEASE_TYPES = ['major', 'minor', 'patch']
|
|
19
|
+
|
|
20
|
+
function sanitizeSuggestedReleaseType(message, allowedTypes = RELEASE_TYPES) {
|
|
21
|
+
if (typeof message !== 'string') {
|
|
22
|
+
return null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const normalized = message
|
|
26
|
+
.trim()
|
|
27
|
+
.toLowerCase()
|
|
28
|
+
.replace(/^release type:\s*/i, '')
|
|
29
|
+
.split(/\s+/)[0]
|
|
30
|
+
?.replace(/[^a-z]/g, '')
|
|
31
|
+
|
|
32
|
+
if (!normalized || !allowedTypes.includes(normalized)) {
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return normalized
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function inferReleaseTypeHeuristically({
|
|
40
|
+
currentVersion = '0.0.0',
|
|
41
|
+
commitLog = '',
|
|
42
|
+
diffStat = ''
|
|
43
|
+
} = {}) {
|
|
44
|
+
const combined = `${commitLog}\n${diffStat}`
|
|
45
|
+
const hasBreakingChange = /breaking change|breaking changes|^[a-z]+(?:\(.+\))?!:/im.test(combined)
|
|
46
|
+
const hasFeatureChange = /\bfeat(?:\(.+\))?:/im.test(commitLog) || /\bfeature\b/i.test(combined)
|
|
47
|
+
const hasPrereleaseVersion = Array.isArray(semver.parse(currentVersion)?.prerelease)
|
|
48
|
+
&& semver.parse(currentVersion)?.prerelease?.length > 0
|
|
49
|
+
|
|
50
|
+
if (hasPrereleaseVersion) {
|
|
51
|
+
if (hasBreakingChange) {
|
|
52
|
+
return 'premajor'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (hasFeatureChange) {
|
|
56
|
+
return 'preminor'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return 'prerelease'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (hasBreakingChange) {
|
|
63
|
+
return 'major'
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (hasFeatureChange) {
|
|
67
|
+
return 'minor'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return 'patch'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function resolveSuggestedReleaseTypeOptions(currentVersion = '0.0.0') {
|
|
74
|
+
const parsedVersion = semver.parse(currentVersion)
|
|
75
|
+
|
|
76
|
+
if (Array.isArray(parsedVersion?.prerelease) && parsedVersion.prerelease.length > 0) {
|
|
77
|
+
return RELEASE_TYPES
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return STABLE_RELEASE_TYPES
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function buildChoiceOrder(suggestedReleaseType) {
|
|
84
|
+
return [
|
|
85
|
+
suggestedReleaseType,
|
|
86
|
+
...RELEASE_TYPES.filter((type) => type !== suggestedReleaseType)
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function readLatestReleaseTag(rootDir, {runCommand} = {}) {
|
|
91
|
+
try {
|
|
92
|
+
const {stdout} = await runCommand('git', ['describe', '--tags', '--abbrev=0'], {
|
|
93
|
+
capture: true,
|
|
94
|
+
cwd: rootDir
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
return stdout.trim() || null
|
|
98
|
+
} catch {
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function readCommitLog(rootDir, {runCommand, latestTag} = {}) {
|
|
104
|
+
const args = latestTag
|
|
105
|
+
? ['log', '--format=%h %s', `${latestTag}..HEAD`]
|
|
106
|
+
: ['log', '--format=%h %s', '-20']
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const {stdout} = await runCommand('git', args, {
|
|
110
|
+
capture: true,
|
|
111
|
+
cwd: rootDir
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
return stdout.trim()
|
|
115
|
+
} catch {
|
|
116
|
+
return ''
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function readDiffStat(rootDir, {runCommand, latestTag} = {}) {
|
|
121
|
+
const args = latestTag
|
|
122
|
+
? ['diff', '--stat', `${latestTag}..HEAD`, '--']
|
|
123
|
+
: ['diff', '--stat', 'HEAD~20..HEAD', '--']
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const {stdout} = await runCommand('git', args, {
|
|
127
|
+
capture: true,
|
|
128
|
+
cwd: rootDir
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
return stdout.trim()
|
|
132
|
+
} catch {
|
|
133
|
+
return ''
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function buildReleaseSuggestionContext(rootDir, {
|
|
138
|
+
runCommand,
|
|
139
|
+
currentVersion,
|
|
140
|
+
packageName
|
|
141
|
+
} = {}) {
|
|
142
|
+
const latestTag = await readLatestReleaseTag(rootDir, {runCommand})
|
|
143
|
+
const commitLog = await readCommitLog(rootDir, {runCommand, latestTag})
|
|
144
|
+
const diffStat = await readDiffStat(rootDir, {runCommand, latestTag})
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
currentVersion,
|
|
148
|
+
packageName,
|
|
149
|
+
latestTag,
|
|
150
|
+
commitLog,
|
|
151
|
+
diffStat
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function suggestReleaseType(rootDir = process.cwd(), {
|
|
156
|
+
runCommand,
|
|
157
|
+
currentVersion,
|
|
158
|
+
packageName,
|
|
159
|
+
commandExistsImpl = commandExists,
|
|
160
|
+
logStep,
|
|
161
|
+
logWarning
|
|
162
|
+
} = {}) {
|
|
163
|
+
const context = await buildReleaseSuggestionContext(rootDir, {
|
|
164
|
+
runCommand,
|
|
165
|
+
currentVersion,
|
|
166
|
+
packageName
|
|
167
|
+
})
|
|
168
|
+
const allowedSuggestedReleaseTypes = resolveSuggestedReleaseTypeOptions(currentVersion)
|
|
169
|
+
const heuristicReleaseType = inferReleaseTypeHeuristically(context)
|
|
170
|
+
|
|
171
|
+
if (!commandExistsImpl('codex')) {
|
|
172
|
+
return {
|
|
173
|
+
...context,
|
|
174
|
+
releaseType: heuristicReleaseType,
|
|
175
|
+
source: 'heuristic'
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let tempDir = null
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
tempDir = await mkdtemp(path.join(tmpdir(), 'zephyr-release-type-'))
|
|
183
|
+
const outputPath = path.join(tempDir, 'codex-last-message.txt')
|
|
184
|
+
|
|
185
|
+
logStep?.('Evaluating the recommended version bump with Codex...')
|
|
186
|
+
|
|
187
|
+
await runCommand('codex', [
|
|
188
|
+
'exec',
|
|
189
|
+
'--ephemeral',
|
|
190
|
+
'--model',
|
|
191
|
+
'gpt-5.4-mini',
|
|
192
|
+
'--sandbox',
|
|
193
|
+
'read-only',
|
|
194
|
+
'--skip-git-repo-check',
|
|
195
|
+
'--output-last-message',
|
|
196
|
+
outputPath,
|
|
197
|
+
[
|
|
198
|
+
'Choose exactly one semver release type for the next release.',
|
|
199
|
+
`Reply with exactly one of: ${allowedSuggestedReleaseTypes.join(', ')}.`,
|
|
200
|
+
'Base the choice on the actual code and workflow changes since the last release tag.',
|
|
201
|
+
'Prefer stable release types unless the current version already has a prerelease identifier.',
|
|
202
|
+
`Package: ${packageName || 'unknown package'}`,
|
|
203
|
+
`Current version: ${currentVersion || 'unknown'}`,
|
|
204
|
+
`Latest release tag: ${context.latestTag || 'none found'}`,
|
|
205
|
+
'Commits since the last release:',
|
|
206
|
+
context.commitLog || '- no commits found',
|
|
207
|
+
'Diff summary since the last release:',
|
|
208
|
+
context.diffStat || '- no diff summary available'
|
|
209
|
+
].join('\n\n')
|
|
210
|
+
], {
|
|
211
|
+
capture: true,
|
|
212
|
+
cwd: rootDir
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
const rawSuggestion = await readFile(outputPath, 'utf8')
|
|
216
|
+
const releaseType = sanitizeSuggestedReleaseType(rawSuggestion, allowedSuggestedReleaseTypes)
|
|
217
|
+
|
|
218
|
+
if (!releaseType) {
|
|
219
|
+
return {
|
|
220
|
+
...context,
|
|
221
|
+
releaseType: heuristicReleaseType,
|
|
222
|
+
source: 'heuristic'
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
...context,
|
|
228
|
+
releaseType,
|
|
229
|
+
source: 'codex'
|
|
230
|
+
}
|
|
231
|
+
} catch (error) {
|
|
232
|
+
logWarning?.(`Codex could not suggest a release type: ${error.message}`)
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
...context,
|
|
236
|
+
releaseType: heuristicReleaseType,
|
|
237
|
+
source: 'heuristic'
|
|
238
|
+
}
|
|
239
|
+
} finally {
|
|
240
|
+
if (tempDir) {
|
|
241
|
+
await rm(tempDir, {recursive: true, force: true}).catch(() => {})
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export async function resolveReleaseType({
|
|
247
|
+
releaseType = null,
|
|
248
|
+
currentVersion = '0.0.0',
|
|
249
|
+
packageName = '',
|
|
250
|
+
rootDir = process.cwd(),
|
|
251
|
+
interactive = true,
|
|
252
|
+
runPrompt,
|
|
253
|
+
runCommand,
|
|
254
|
+
logStep,
|
|
255
|
+
logWarning
|
|
256
|
+
} = {}) {
|
|
257
|
+
if (releaseType) {
|
|
258
|
+
return releaseType
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const suggested = await suggestReleaseType(rootDir, {
|
|
262
|
+
runCommand,
|
|
263
|
+
currentVersion,
|
|
264
|
+
packageName,
|
|
265
|
+
logStep,
|
|
266
|
+
logWarning
|
|
267
|
+
})
|
|
268
|
+
const rangeLabel = suggested.latestTag
|
|
269
|
+
? `based on changes since ${suggested.latestTag}`
|
|
270
|
+
: 'based on recent changes'
|
|
271
|
+
|
|
272
|
+
if (!interactive || typeof runPrompt !== 'function') {
|
|
273
|
+
logStep?.(`No release type specified. Using suggested ${suggested.releaseType} bump ${rangeLabel}.`)
|
|
274
|
+
return suggested.releaseType
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const {selectedReleaseType} = await runPrompt([
|
|
278
|
+
{
|
|
279
|
+
type: 'list',
|
|
280
|
+
name: 'selectedReleaseType',
|
|
281
|
+
message:
|
|
282
|
+
`Recommended release bump for ${packageName || 'this package'}@${currentVersion} ` +
|
|
283
|
+
`${rangeLabel}. Choose the version bump to use:`,
|
|
284
|
+
choices: buildChoiceOrder(suggested.releaseType).map((type) => ({
|
|
285
|
+
name: type === suggested.releaseType ? `${type} (recommended)` : type,
|
|
286
|
+
value: type
|
|
287
|
+
})),
|
|
288
|
+
default: suggested.releaseType
|
|
289
|
+
}
|
|
290
|
+
])
|
|
291
|
+
|
|
292
|
+
return selectedReleaseType
|
|
293
|
+
}
|
package/src/release/shared.mjs
CHANGED
|
@@ -10,22 +10,14 @@ import {
|
|
|
10
10
|
getUpstreamRef
|
|
11
11
|
} from '../utils/git.mjs'
|
|
12
12
|
import {
|
|
13
|
-
buildFallbackCommitMessage,
|
|
14
13
|
formatWorkingTreePreview,
|
|
15
14
|
parseWorkingTreeEntries,
|
|
16
15
|
parseWorkingTreeStatus,
|
|
17
16
|
suggestReleaseCommitMessage
|
|
18
17
|
} from './commit-message.mjs'
|
|
18
|
+
import {RELEASE_TYPES as SUPPORTED_RELEASE_TYPES} from './release-type.mjs'
|
|
19
19
|
|
|
20
|
-
const RELEASE_TYPES = new Set(
|
|
21
|
-
'major',
|
|
22
|
-
'minor',
|
|
23
|
-
'patch',
|
|
24
|
-
'premajor',
|
|
25
|
-
'preminor',
|
|
26
|
-
'prepatch',
|
|
27
|
-
'prerelease'
|
|
28
|
-
])
|
|
20
|
+
const RELEASE_TYPES = new Set(SUPPORTED_RELEASE_TYPES)
|
|
29
21
|
const DIRTY_WORKING_TREE_MESSAGE = 'Working tree has uncommitted changes. Commit or stash them before releasing.'
|
|
30
22
|
const DIRTY_WORKING_TREE_CANCELLED_MESSAGE = 'Release cancelled: pending changes were not committed.'
|
|
31
23
|
|
|
@@ -58,9 +50,9 @@ export function parseReleaseArgs({
|
|
|
58
50
|
|
|
59
51
|
const positionals = filteredArgs.filter((arg) => !arg.startsWith('--'))
|
|
60
52
|
const presentFlags = new Set(filteredArgs.filter((arg) => arg.startsWith('--')))
|
|
61
|
-
const releaseType = positionals[0] ??
|
|
53
|
+
const releaseType = positionals[0] ?? null
|
|
62
54
|
|
|
63
|
-
if (!RELEASE_TYPES.has(releaseType)) {
|
|
55
|
+
if (releaseType && !RELEASE_TYPES.has(releaseType)) {
|
|
64
56
|
throw new Error(
|
|
65
57
|
`Invalid release type "${releaseType}". Use one of: ${Array.from(RELEASE_TYPES).join(', ')}.`
|
|
66
58
|
)
|
|
@@ -124,35 +116,24 @@ export async function ensureCleanWorkingTree(rootDir = process.cwd(), {
|
|
|
124
116
|
logStep,
|
|
125
117
|
logWarning,
|
|
126
118
|
statusEntries
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
const changeLabel = statusEntries.length === 1 ? 'change' : 'changes'
|
|
130
|
-
const {shouldCommitPendingChanges} = await runPrompt([
|
|
119
|
+
})
|
|
120
|
+
const {commitMessage} = await runPrompt([
|
|
131
121
|
{
|
|
132
|
-
type: '
|
|
133
|
-
name: '
|
|
122
|
+
type: 'input',
|
|
123
|
+
name: 'commitMessage',
|
|
134
124
|
message:
|
|
135
|
-
|
|
125
|
+
'Pending changes detected before release:\n\n' +
|
|
136
126
|
`${formatWorkingTreePreview(statusEntries)}\n\n` +
|
|
137
|
-
'
|
|
138
|
-
|
|
127
|
+
'Enter a commit message to stage and commit all current changes before continuing.\n' +
|
|
128
|
+
'Leave blank to cancel.',
|
|
129
|
+
default: suggestedCommitMessage ?? ''
|
|
139
130
|
}
|
|
140
131
|
])
|
|
141
132
|
|
|
142
|
-
if (!
|
|
133
|
+
if (!commitMessage || commitMessage.trim().length === 0) {
|
|
143
134
|
throw new Error(DIRTY_WORKING_TREE_CANCELLED_MESSAGE)
|
|
144
135
|
}
|
|
145
136
|
|
|
146
|
-
const {commitMessage} = await runPrompt([
|
|
147
|
-
{
|
|
148
|
-
type: 'input',
|
|
149
|
-
name: 'commitMessage',
|
|
150
|
-
message: 'Commit message for pending release changes',
|
|
151
|
-
default: suggestedCommitMessage,
|
|
152
|
-
validate: (value) => (value && value.trim().length > 0 ? true : 'Commit message cannot be empty.')
|
|
153
|
-
}
|
|
154
|
-
])
|
|
155
|
-
|
|
156
137
|
const message = commitMessage.trim()
|
|
157
138
|
|
|
158
139
|
logStep?.('Staging all pending changes before release...')
|
package/src/release-node.mjs
CHANGED
|
@@ -17,7 +17,7 @@ function hasExplicitReleaseOptions(options = {}) {
|
|
|
17
17
|
export async function releaseNode(options = {}) {
|
|
18
18
|
const parsed = hasExplicitReleaseOptions(options)
|
|
19
19
|
? {
|
|
20
|
-
releaseType: options.releaseType ??
|
|
20
|
+
releaseType: 'releaseType' in options ? (options.releaseType ?? null) : null,
|
|
21
21
|
skipGitHooks: options.skipGitHooks === true,
|
|
22
22
|
skipTests: options.skipTests === true,
|
|
23
23
|
skipLint: options.skipLint === true,
|
|
@@ -15,7 +15,7 @@ function hasExplicitReleaseOptions(options = {}) {
|
|
|
15
15
|
export async function releasePackagist(options = {}) {
|
|
16
16
|
const parsed = hasExplicitReleaseOptions(options)
|
|
17
17
|
? {
|
|
18
|
-
releaseType: options.releaseType ??
|
|
18
|
+
releaseType: 'releaseType' in options ? (options.releaseType ?? null) : null,
|
|
19
19
|
skipGitHooks: options.skipGitHooks === true,
|
|
20
20
|
skipTests: options.skipTests === true,
|
|
21
21
|
skipLint: options.skipLint === true
|
|
@@ -25,6 +25,9 @@ export function createAppContext({
|
|
|
25
25
|
presetName: executionMode.presetName ?? null,
|
|
26
26
|
maintenanceMode: executionMode.maintenanceMode ?? null,
|
|
27
27
|
skipGitHooks: executionMode.skipGitHooks === true,
|
|
28
|
+
skipChecks: executionMode.skipChecks === true,
|
|
29
|
+
skipTests: executionMode.skipTests === true || executionMode.skipChecks === true,
|
|
30
|
+
skipLint: executionMode.skipLint === true || executionMode.skipChecks === true,
|
|
28
31
|
resumePending: executionMode.resumePending === true,
|
|
29
32
|
discardPending: executionMode.discardPending === true
|
|
30
33
|
}
|