@wyxos/zephyr 0.5.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 +33 -4
- 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 +38 -4
- package/src/application/deploy/run-deployment.mjs +7 -1
- package/src/application/deploy/run-local-deployment-checks.mjs +70 -11
- 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 +36 -9
- package/src/config/preset-options.mjs +89 -0
- package/src/config/project.mjs +45 -10
- package/src/deploy/local-repo.mjs +28 -27
- package/src/deploy/preflight.mjs +57 -3
- package/src/main.mjs +64 -6
- package/src/release/commit-message.mjs +8 -173
- package/src/release/shared.mjs +9 -21
- package/src/release-node.mjs +8 -1
- package/src/release-packagist.mjs +10 -3
- package/src/runtime/app-context.mjs +13 -1
|
@@ -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
|
|
@@ -178,6 +177,7 @@ async function commitAndPushPendingChanges(targetBranch, rootDir, {
|
|
|
178
177
|
logProcessing,
|
|
179
178
|
logSuccess,
|
|
180
179
|
logWarning,
|
|
180
|
+
autoCommit = false,
|
|
181
181
|
skipGitHooks = false,
|
|
182
182
|
suggestCommitMessage = suggestCommitMessageImpl
|
|
183
183
|
} = {}) {
|
|
@@ -187,7 +187,7 @@ async function commitAndPushPendingChanges(targetBranch, rootDir, {
|
|
|
187
187
|
return
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
if (typeof runPrompt !== 'function') {
|
|
190
|
+
if (!autoCommit && typeof runPrompt !== 'function') {
|
|
191
191
|
throw new Error(DIRTY_DEPLOYMENT_MESSAGE)
|
|
192
192
|
}
|
|
193
193
|
|
|
@@ -213,36 +213,35 @@ async function commitAndPushPendingChanges(targetBranch, rootDir, {
|
|
|
213
213
|
logStep: logProcessing,
|
|
214
214
|
logWarning,
|
|
215
215
|
statusEntries
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
{
|
|
221
|
-
|
|
222
|
-
name: 'shouldCommitPendingChanges',
|
|
223
|
-
message:
|
|
224
|
-
`Pending ${changeLabel} detected before deployment:\n\n` +
|
|
225
|
-
`${formatWorkingTreePreview(statusEntries)}\n\n` +
|
|
226
|
-
'Stage and commit all current changes before continuing?',
|
|
227
|
-
default: true
|
|
216
|
+
})
|
|
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.')
|
|
228
222
|
}
|
|
229
|
-
])
|
|
230
223
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
+
])
|
|
234
238
|
|
|
235
|
-
|
|
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.')
|
|
239
|
+
if (!commitMessage || commitMessage.trim().length === 0) {
|
|
240
|
+
throw new Error(DIRTY_DEPLOYMENT_CANCELLED_MESSAGE)
|
|
242
241
|
}
|
|
243
|
-
])
|
|
244
242
|
|
|
245
|
-
|
|
243
|
+
message = commitMessage.trim()
|
|
244
|
+
}
|
|
246
245
|
|
|
247
246
|
logProcessing?.('Staging all pending changes before deployment...')
|
|
248
247
|
await runCommand('git', ['add', '-A'], { cwd: rootDir })
|
|
@@ -369,6 +368,7 @@ export async function ensureLocalRepositoryState(targetBranch, rootDir = process
|
|
|
369
368
|
logProcessing,
|
|
370
369
|
logSuccess,
|
|
371
370
|
logWarning,
|
|
371
|
+
autoCommit = false,
|
|
372
372
|
skipGitHooks = false,
|
|
373
373
|
suggestCommitMessage: suggestCommitMessageFn = suggestCommitMessageImpl,
|
|
374
374
|
getCurrentBranch: getCurrentBranchFn = getCurrentBranch,
|
|
@@ -448,6 +448,7 @@ export async function ensureLocalRepositoryState(targetBranch, rootDir = process
|
|
|
448
448
|
logProcessing,
|
|
449
449
|
logSuccess,
|
|
450
450
|
logWarning,
|
|
451
|
+
autoCommit,
|
|
451
452
|
skipGitHooks,
|
|
452
453
|
suggestCommitMessage: suggestCommitMessageFn
|
|
453
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
|
package/src/main.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import * as bootstrap from './project/bootstrap.mjs'
|
|
|
11
11
|
import {getErrorCode, ZephyrError} from './runtime/errors.mjs'
|
|
12
12
|
import {PROJECT_CONFIG_DIR} from './utils/paths.mjs'
|
|
13
13
|
import {writeStderrLine} from './utils/output.mjs'
|
|
14
|
+
import {mergeDeployOptions} from './config/preset-options.mjs'
|
|
14
15
|
import {createAppContext} from './runtime/app-context.mjs'
|
|
15
16
|
import {createConfigurationService} from './application/configuration/service.mjs'
|
|
16
17
|
import {selectDeploymentTarget} from './application/configuration/select-deployment-target.mjs'
|
|
@@ -35,11 +36,21 @@ function normalizeMainOptions(firstArg = null, secondArg = null) {
|
|
|
35
36
|
resumePending: firstArg.resumePending === true,
|
|
36
37
|
discardPending: firstArg.discardPending === true,
|
|
37
38
|
maintenanceMode: firstArg.maintenanceMode ?? null,
|
|
39
|
+
autoCommit: firstArg.autoCommit === true,
|
|
40
|
+
skipVersioning: firstArg.skipVersioning === true,
|
|
38
41
|
skipGitHooks: firstArg.skipGitHooks === true,
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
skipChecks: firstArg.skipChecks === true,
|
|
43
|
+
skipTests: firstArg.skipTests === true || firstArg.skipChecks === true,
|
|
44
|
+
skipLint: firstArg.skipLint === true || firstArg.skipChecks === true,
|
|
41
45
|
skipBuild: firstArg.skipBuild === true,
|
|
42
46
|
skipDeploy: firstArg.skipDeploy === true,
|
|
47
|
+
explicitMaintenanceMode: firstArg.explicitMaintenanceMode === true || 'maintenanceMode' in firstArg,
|
|
48
|
+
explicitAutoCommit: firstArg.explicitAutoCommit === true || 'autoCommit' in firstArg,
|
|
49
|
+
explicitSkipVersioning: firstArg.explicitSkipVersioning === true || 'skipVersioning' in firstArg,
|
|
50
|
+
explicitSkipGitHooks: firstArg.explicitSkipGitHooks === true || 'skipGitHooks' in firstArg,
|
|
51
|
+
explicitSkipChecks: firstArg.explicitSkipChecks === true || 'skipChecks' in firstArg,
|
|
52
|
+
explicitSkipTests: firstArg.explicitSkipTests === true || 'skipTests' in firstArg || 'skipChecks' in firstArg,
|
|
53
|
+
explicitSkipLint: firstArg.explicitSkipLint === true || 'skipLint' in firstArg || 'skipChecks' in firstArg,
|
|
43
54
|
context: firstArg.context ?? null
|
|
44
55
|
}
|
|
45
56
|
}
|
|
@@ -53,11 +64,21 @@ function normalizeMainOptions(firstArg = null, secondArg = null) {
|
|
|
53
64
|
resumePending: false,
|
|
54
65
|
discardPending: false,
|
|
55
66
|
maintenanceMode: null,
|
|
67
|
+
autoCommit: false,
|
|
68
|
+
skipVersioning: false,
|
|
56
69
|
skipGitHooks: false,
|
|
70
|
+
skipChecks: false,
|
|
57
71
|
skipTests: false,
|
|
58
72
|
skipLint: false,
|
|
59
73
|
skipBuild: false,
|
|
60
74
|
skipDeploy: false,
|
|
75
|
+
explicitMaintenanceMode: false,
|
|
76
|
+
explicitAutoCommit: false,
|
|
77
|
+
explicitSkipVersioning: false,
|
|
78
|
+
explicitSkipGitHooks: false,
|
|
79
|
+
explicitSkipChecks: false,
|
|
80
|
+
explicitSkipTests: false,
|
|
81
|
+
explicitSkipLint: false,
|
|
61
82
|
context: null
|
|
62
83
|
}
|
|
63
84
|
}
|
|
@@ -108,9 +129,21 @@ async function main(optionsOrWorkflowType = null, versionArg = null) {
|
|
|
108
129
|
workflow: resolveWorkflowName(options.workflowType),
|
|
109
130
|
presetName: options.presetName,
|
|
110
131
|
maintenanceMode: options.maintenanceMode,
|
|
132
|
+
autoCommit: options.autoCommit === true,
|
|
133
|
+
skipVersioning: options.skipVersioning === true,
|
|
111
134
|
skipGitHooks: options.skipGitHooks === true,
|
|
135
|
+
skipChecks: options.skipChecks === true,
|
|
136
|
+
skipTests: options.skipTests === true,
|
|
137
|
+
skipLint: options.skipLint === true,
|
|
112
138
|
resumePending: options.resumePending,
|
|
113
|
-
discardPending: options.discardPending
|
|
139
|
+
discardPending: options.discardPending,
|
|
140
|
+
explicitMaintenanceMode: options.explicitMaintenanceMode === true,
|
|
141
|
+
explicitAutoCommit: options.explicitAutoCommit === true,
|
|
142
|
+
explicitSkipVersioning: options.explicitSkipVersioning === true,
|
|
143
|
+
explicitSkipGitHooks: options.explicitSkipGitHooks === true,
|
|
144
|
+
explicitSkipChecks: options.explicitSkipChecks === true,
|
|
145
|
+
explicitSkipTests: options.explicitSkipTests === true,
|
|
146
|
+
explicitSkipLint: options.explicitSkipLint === true
|
|
114
147
|
}
|
|
115
148
|
const appContext = options.context ?? createAppContext({executionMode})
|
|
116
149
|
const {
|
|
@@ -122,7 +155,11 @@ async function main(optionsOrWorkflowType = null, versionArg = null) {
|
|
|
122
155
|
runCommand,
|
|
123
156
|
emitEvent
|
|
124
157
|
} = appContext
|
|
125
|
-
|
|
158
|
+
let currentExecutionMode = {
|
|
159
|
+
...executionMode,
|
|
160
|
+
...(appContext.executionMode ?? {})
|
|
161
|
+
}
|
|
162
|
+
appContext.executionMode = currentExecutionMode
|
|
126
163
|
const configurationService = createConfigurationService(appContext)
|
|
127
164
|
|
|
128
165
|
try {
|
|
@@ -137,7 +174,12 @@ async function main(optionsOrWorkflowType = null, versionArg = null) {
|
|
|
137
174
|
nonInteractive: currentExecutionMode.interactive === false,
|
|
138
175
|
presetName: currentExecutionMode.presetName,
|
|
139
176
|
maintenanceMode: currentExecutionMode.maintenanceMode,
|
|
177
|
+
autoCommit: currentExecutionMode.autoCommit === true,
|
|
178
|
+
skipVersioning: currentExecutionMode.skipVersioning === true,
|
|
140
179
|
skipGitHooks: currentExecutionMode.skipGitHooks === true,
|
|
180
|
+
skipChecks: currentExecutionMode.skipChecks === true,
|
|
181
|
+
skipTests: currentExecutionMode.skipTests === true,
|
|
182
|
+
skipLint: currentExecutionMode.skipLint === true,
|
|
141
183
|
resumePending: currentExecutionMode.resumePending,
|
|
142
184
|
discardPending: currentExecutionMode.discardPending
|
|
143
185
|
}
|
|
@@ -162,6 +204,7 @@ async function main(optionsOrWorkflowType = null, versionArg = null) {
|
|
|
162
204
|
skipGitHooks: options.skipGitHooks,
|
|
163
205
|
skipTests: options.skipTests,
|
|
164
206
|
skipLint: options.skipLint,
|
|
207
|
+
skipVersioning: options.skipVersioning,
|
|
165
208
|
skipBuild: options.skipBuild,
|
|
166
209
|
skipDeploy: options.skipDeploy,
|
|
167
210
|
context: appContext
|
|
@@ -190,6 +233,7 @@ async function main(optionsOrWorkflowType = null, versionArg = null) {
|
|
|
190
233
|
skipGitHooks: options.skipGitHooks,
|
|
191
234
|
skipTests: options.skipTests,
|
|
192
235
|
skipLint: options.skipLint,
|
|
236
|
+
skipVersioning: options.skipVersioning,
|
|
193
237
|
context: appContext
|
|
194
238
|
})
|
|
195
239
|
emitEvent?.('run_completed', {
|
|
@@ -241,7 +285,7 @@ async function main(optionsOrWorkflowType = null, versionArg = null) {
|
|
|
241
285
|
})
|
|
242
286
|
}
|
|
243
287
|
|
|
244
|
-
const {deploymentConfig} = await selectDeploymentTarget(rootDir, {
|
|
288
|
+
const {deploymentConfig, presetState} = await selectDeploymentTarget(rootDir, {
|
|
245
289
|
configurationService,
|
|
246
290
|
runPrompt,
|
|
247
291
|
logProcessing,
|
|
@@ -251,6 +295,19 @@ async function main(optionsOrWorkflowType = null, versionArg = null) {
|
|
|
251
295
|
executionMode: currentExecutionMode
|
|
252
296
|
})
|
|
253
297
|
|
|
298
|
+
if (presetState) {
|
|
299
|
+
const effectiveDeployOptions = mergeDeployOptions(currentExecutionMode, presetState.options)
|
|
300
|
+
currentExecutionMode = {
|
|
301
|
+
...currentExecutionMode,
|
|
302
|
+
presetName: presetState.name,
|
|
303
|
+
...effectiveDeployOptions,
|
|
304
|
+
skipChecks: currentExecutionMode.skipChecks === true ||
|
|
305
|
+
(effectiveDeployOptions.skipTests === true && effectiveDeployOptions.skipLint === true)
|
|
306
|
+
}
|
|
307
|
+
appContext.executionMode = currentExecutionMode
|
|
308
|
+
await presetState.applyExecutionMode(currentExecutionMode)
|
|
309
|
+
}
|
|
310
|
+
|
|
254
311
|
const snapshotToUse = await resolvePendingSnapshot(rootDir, deploymentConfig, {
|
|
255
312
|
runPrompt,
|
|
256
313
|
logProcessing,
|
|
@@ -262,7 +319,8 @@ async function main(optionsOrWorkflowType = null, versionArg = null) {
|
|
|
262
319
|
rootDir,
|
|
263
320
|
snapshot: snapshotToUse,
|
|
264
321
|
versionArg: options.versionArg,
|
|
265
|
-
context: appContext
|
|
322
|
+
context: appContext,
|
|
323
|
+
presetState
|
|
266
324
|
})
|
|
267
325
|
|
|
268
326
|
emitEvent?.('run_completed', {
|
|
@@ -20,7 +20,8 @@ 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
|
|
26
27
|
const STATUS_LABELS = {
|
|
@@ -32,59 +33,6 @@ const STATUS_LABELS = {
|
|
|
32
33
|
T: 'type-changed',
|
|
33
34
|
U: 'conflicted'
|
|
34
35
|
}
|
|
35
|
-
const TOPIC_STOP_WORDS = new Set([
|
|
36
|
-
'src',
|
|
37
|
-
'test',
|
|
38
|
-
'tests',
|
|
39
|
-
'__tests__',
|
|
40
|
-
'spec',
|
|
41
|
-
'specs',
|
|
42
|
-
'app',
|
|
43
|
-
'lib',
|
|
44
|
-
'dist',
|
|
45
|
-
'packages',
|
|
46
|
-
'package',
|
|
47
|
-
'application',
|
|
48
|
-
'shared',
|
|
49
|
-
'index',
|
|
50
|
-
'main',
|
|
51
|
-
'local',
|
|
52
|
-
'repo',
|
|
53
|
-
'prepare',
|
|
54
|
-
'commit',
|
|
55
|
-
'message',
|
|
56
|
-
'js',
|
|
57
|
-
'jsx',
|
|
58
|
-
'ts',
|
|
59
|
-
'tsx',
|
|
60
|
-
'mjs',
|
|
61
|
-
'cjs',
|
|
62
|
-
'php',
|
|
63
|
-
'json',
|
|
64
|
-
'yaml',
|
|
65
|
-
'yml',
|
|
66
|
-
'md',
|
|
67
|
-
'toml',
|
|
68
|
-
'lock'
|
|
69
|
-
])
|
|
70
|
-
|
|
71
|
-
function buildTargetedFallbackCommitMessage(statusEntries = []) {
|
|
72
|
-
const paths = statusEntries.map((entry) => entry.path.toLowerCase())
|
|
73
|
-
const touchesDeployPrep = paths.some((pathValue) => pathValue.includes('prepare-local-deployment'))
|
|
74
|
-
const touchesLocalRepo = paths.some((pathValue) => pathValue.includes('local-repo'))
|
|
75
|
-
const touchesCommitMessage = paths.some((pathValue) => pathValue.includes('commit-message'))
|
|
76
|
-
const touchesReleaseFlow = paths.some((pathValue) => pathValue.includes('/release/') || pathValue.includes('release-'))
|
|
77
|
-
|
|
78
|
-
if (touchesDeployPrep && touchesLocalRepo) {
|
|
79
|
-
return 'fix: prompt for dirty deploy changes before version bump'
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (touchesCommitMessage && touchesReleaseFlow) {
|
|
83
|
-
return 'fix: tighten release commit suggestions'
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return null
|
|
87
|
-
}
|
|
88
36
|
|
|
89
37
|
function resolveWorkingTreeEntryLabel(entry) {
|
|
90
38
|
if (entry.indexStatus === '?' && entry.worktreeStatus === '?') {
|
|
@@ -105,32 +53,6 @@ function resolveWorkingTreeEntryLabel(entry) {
|
|
|
105
53
|
return 'changed'
|
|
106
54
|
}
|
|
107
55
|
|
|
108
|
-
function tokenizePath(pathValue = '') {
|
|
109
|
-
return pathValue
|
|
110
|
-
.split(/[\\/]/)
|
|
111
|
-
.flatMap((segment) => segment.split(/[^a-zA-Z0-9]+/))
|
|
112
|
-
.map((token) => token.toLowerCase())
|
|
113
|
-
.filter((token) => token.length >= 3 && !TOPIC_STOP_WORDS.has(token))
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function inferCommitTypeFromEntries(statusEntries = []) {
|
|
117
|
-
const paths = statusEntries.map((entry) => entry.path.toLowerCase())
|
|
118
|
-
|
|
119
|
-
if (paths.every((pathValue) => pathValue.endsWith('.md') || pathValue.includes('/docs/') || pathValue.startsWith('docs/'))) {
|
|
120
|
-
return 'docs'
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (paths.every((pathValue) => /\.test\.[^.]+$/.test(pathValue) || pathValue.includes('/tests/'))) {
|
|
124
|
-
return 'test'
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (paths.some((pathValue) => pathValue.includes('.github/workflows/') || pathValue.includes('/ci/'))) {
|
|
128
|
-
return 'ci'
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return 'chore'
|
|
132
|
-
}
|
|
133
|
-
|
|
134
56
|
export function parseWorkingTreeStatus(stdout = '') {
|
|
135
57
|
return stdout
|
|
136
58
|
.split(/\r?\n/)
|
|
@@ -225,92 +147,11 @@ export function sanitizeSuggestedCommitMessage(message) {
|
|
|
225
147
|
return normalized
|
|
226
148
|
}
|
|
227
149
|
|
|
228
|
-
export function buildFallbackCommitMessage(statusEntries = []) {
|
|
229
|
-
const targetedFallback = buildTargetedFallbackCommitMessage(statusEntries)
|
|
230
|
-
if (targetedFallback) {
|
|
231
|
-
return targetedFallback
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const tokenCounts = new Map()
|
|
235
|
-
|
|
236
|
-
for (const entry of statusEntries) {
|
|
237
|
-
for (const token of tokenizePath(entry.path)) {
|
|
238
|
-
tokenCounts.set(token, (tokenCounts.get(token) ?? 0) + 1)
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const orderedTokens = Array.from(tokenCounts.entries())
|
|
243
|
-
.sort((left, right) => {
|
|
244
|
-
if (right[1] !== left[1]) {
|
|
245
|
-
return right[1] - left[1]
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
return left[0].localeCompare(right[0])
|
|
249
|
-
})
|
|
250
|
-
.map(([token]) => token)
|
|
251
|
-
|
|
252
|
-
const primaryTopic = orderedTokens[0] ?? 'release'
|
|
253
|
-
const commitType = inferCommitTypeFromEntries(statusEntries)
|
|
254
|
-
|
|
255
|
-
if (commitType === 'docs') {
|
|
256
|
-
return `docs: update ${primaryTopic} documentation`
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (commitType === 'test') {
|
|
260
|
-
return `test: expand ${primaryTopic} coverage`
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
if (commitType === 'ci') {
|
|
264
|
-
return `ci: update ${primaryTopic} workflow`
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
return `chore: improve ${primaryTopic} workflow`
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
async function collectDiffNumstat(rootDir, {runCommand} = {}) {
|
|
271
|
-
try {
|
|
272
|
-
const {stdout} = await runCommand('git', ['diff', '--numstat', 'HEAD', '--'], {
|
|
273
|
-
capture: true,
|
|
274
|
-
cwd: rootDir
|
|
275
|
-
})
|
|
276
|
-
|
|
277
|
-
return stdout
|
|
278
|
-
.split(/\r?\n/)
|
|
279
|
-
.map((line) => line.trim())
|
|
280
|
-
.filter(Boolean)
|
|
281
|
-
.reduce((map, line) => {
|
|
282
|
-
const [addedRaw, deletedRaw, filePath] = line.split('\t')
|
|
283
|
-
if (!filePath) {
|
|
284
|
-
return map
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const added = Number.parseInt(addedRaw, 10)
|
|
288
|
-
const deleted = Number.parseInt(deletedRaw, 10)
|
|
289
|
-
map.set(filePath, {
|
|
290
|
-
added: Number.isFinite(added) ? added : 0,
|
|
291
|
-
deleted: Number.isFinite(deleted) ? deleted : 0
|
|
292
|
-
})
|
|
293
|
-
return map
|
|
294
|
-
}, new Map())
|
|
295
|
-
} catch {
|
|
296
|
-
return new Map()
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
async function buildCommitMessageContext(rootDir, {
|
|
301
|
-
runCommand,
|
|
302
|
-
statusEntries = []
|
|
303
|
-
} = {}) {
|
|
304
|
-
const changeCountsByPath = await collectDiffNumstat(rootDir, {runCommand})
|
|
305
|
-
return statusEntries.map((entry) => `- ${summarizeWorkingTreeEntry(entry, {changeCountsByPath})}`).join('\n')
|
|
306
|
-
}
|
|
307
|
-
|
|
308
150
|
export async function suggestCommitMessage(rootDir = process.cwd(), {
|
|
309
151
|
runCommand,
|
|
310
152
|
commandExistsImpl = commandExists,
|
|
311
153
|
logStep,
|
|
312
|
-
logWarning
|
|
313
|
-
statusEntries = []
|
|
154
|
+
logWarning
|
|
314
155
|
} = {}) {
|
|
315
156
|
if (!commandExistsImpl('codex')) {
|
|
316
157
|
return null
|
|
@@ -321,10 +162,6 @@ export async function suggestCommitMessage(rootDir = process.cwd(), {
|
|
|
321
162
|
try {
|
|
322
163
|
tempDir = await mkdtemp(path.join(tmpdir(), 'zephyr-release-commit-'))
|
|
323
164
|
const outputPath = path.join(tempDir, 'codex-last-message.txt')
|
|
324
|
-
const commitContext = await buildCommitMessageContext(rootDir, {
|
|
325
|
-
runCommand,
|
|
326
|
-
statusEntries
|
|
327
|
-
})
|
|
328
165
|
|
|
329
166
|
logStep?.('Generating a suggested commit message with Codex...')
|
|
330
167
|
|
|
@@ -339,13 +176,11 @@ export async function suggestCommitMessage(rootDir = process.cwd(), {
|
|
|
339
176
|
'--output-last-message',
|
|
340
177
|
outputPath,
|
|
341
178
|
[
|
|
342
|
-
'
|
|
343
|
-
'Use
|
|
344
|
-
'
|
|
345
|
-
'
|
|
346
|
-
'Do not
|
|
347
|
-
'Pending change summary:',
|
|
348
|
-
commitContext || '- changed files present'
|
|
179
|
+
'Inspect the current repository and decide the best conventional commit message for the pending changes.',
|
|
180
|
+
'Use whatever read-only inspection you need.',
|
|
181
|
+
'Reply with exactly one line in the format "<type>: <subject>".',
|
|
182
|
+
'Do not use scopes like "fix(scope): ...".',
|
|
183
|
+
'Do not include extra text.'
|
|
349
184
|
].join('\n\n')
|
|
350
185
|
], {
|
|
351
186
|
capture: true,
|
package/src/release/shared.mjs
CHANGED
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
getUpstreamRef
|
|
11
11
|
} from '../utils/git.mjs'
|
|
12
12
|
import {
|
|
13
|
-
buildFallbackCommitMessage,
|
|
14
13
|
formatWorkingTreePreview,
|
|
15
14
|
parseWorkingTreeEntries,
|
|
16
15
|
parseWorkingTreeStatus,
|
|
@@ -117,35 +116,24 @@ export async function ensureCleanWorkingTree(rootDir = process.cwd(), {
|
|
|
117
116
|
logStep,
|
|
118
117
|
logWarning,
|
|
119
118
|
statusEntries
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
const changeLabel = statusEntries.length === 1 ? 'change' : 'changes'
|
|
123
|
-
const {shouldCommitPendingChanges} = await runPrompt([
|
|
119
|
+
})
|
|
120
|
+
const {commitMessage} = await runPrompt([
|
|
124
121
|
{
|
|
125
|
-
type: '
|
|
126
|
-
name: '
|
|
122
|
+
type: 'input',
|
|
123
|
+
name: 'commitMessage',
|
|
127
124
|
message:
|
|
128
|
-
|
|
125
|
+
'Pending changes detected before release:\n\n' +
|
|
129
126
|
`${formatWorkingTreePreview(statusEntries)}\n\n` +
|
|
130
|
-
'
|
|
131
|
-
|
|
127
|
+
'Enter a commit message to stage and commit all current changes before continuing.\n' +
|
|
128
|
+
'Leave blank to cancel.',
|
|
129
|
+
default: suggestedCommitMessage ?? ''
|
|
132
130
|
}
|
|
133
131
|
])
|
|
134
132
|
|
|
135
|
-
if (!
|
|
133
|
+
if (!commitMessage || commitMessage.trim().length === 0) {
|
|
136
134
|
throw new Error(DIRTY_WORKING_TREE_CANCELLED_MESSAGE)
|
|
137
135
|
}
|
|
138
136
|
|
|
139
|
-
const {commitMessage} = await runPrompt([
|
|
140
|
-
{
|
|
141
|
-
type: 'input',
|
|
142
|
-
name: 'commitMessage',
|
|
143
|
-
message: 'Commit message for pending release changes',
|
|
144
|
-
default: suggestedCommitMessage,
|
|
145
|
-
validate: (value) => (value && value.trim().length > 0 ? true : 'Commit message cannot be empty.')
|
|
146
|
-
}
|
|
147
|
-
])
|
|
148
|
-
|
|
149
137
|
const message = commitMessage.trim()
|
|
150
138
|
|
|
151
139
|
logStep?.('Staging all pending changes before release...')
|
package/src/release-node.mjs
CHANGED
|
@@ -9,6 +9,7 @@ function hasExplicitReleaseOptions(options = {}) {
|
|
|
9
9
|
'skipGitHooks',
|
|
10
10
|
'skipTests',
|
|
11
11
|
'skipLint',
|
|
12
|
+
'skipVersioning',
|
|
12
13
|
'skipBuild',
|
|
13
14
|
'skipDeploy'
|
|
14
15
|
].some((key) => key in options)
|
|
@@ -21,12 +22,17 @@ export async function releaseNode(options = {}) {
|
|
|
21
22
|
skipGitHooks: options.skipGitHooks === true,
|
|
22
23
|
skipTests: options.skipTests === true,
|
|
23
24
|
skipLint: options.skipLint === true,
|
|
25
|
+
skipVersioning: options.skipVersioning === true,
|
|
24
26
|
skipBuild: options.skipBuild === true,
|
|
25
27
|
skipDeploy: options.skipDeploy === true
|
|
26
28
|
}
|
|
27
29
|
: parseReleaseArgs({
|
|
28
|
-
booleanFlags: ['--skip-git-hooks', '--skip-tests', '--skip-lint', '--skip-build', '--skip-deploy']
|
|
30
|
+
booleanFlags: ['--skip-git-hooks', '--skip-tests', '--skip-lint', '--skip-versioning', '--skip-build', '--skip-deploy']
|
|
29
31
|
})
|
|
32
|
+
|
|
33
|
+
if (parsed.skipVersioning && parsed.releaseType) {
|
|
34
|
+
throw new Error('--skip-versioning cannot be used together with an explicit version or bump argument.')
|
|
35
|
+
}
|
|
30
36
|
const rootDir = options.rootDir ?? process.cwd()
|
|
31
37
|
const context = options.context ?? createAppContext({
|
|
32
38
|
executionMode: {
|
|
@@ -42,6 +48,7 @@ export async function releaseNode(options = {}) {
|
|
|
42
48
|
skipGitHooks: parsed.skipGitHooks === true || executionMode?.skipGitHooks === true,
|
|
43
49
|
skipTests: parsed.skipTests === true,
|
|
44
50
|
skipLint: parsed.skipLint === true,
|
|
51
|
+
skipVersioning: parsed.skipVersioning === true,
|
|
45
52
|
skipBuild: parsed.skipBuild === true,
|
|
46
53
|
skipDeploy: parsed.skipDeploy === true,
|
|
47
54
|
rootDir,
|