@wyxos/zephyr 0.4.9 → 0.5.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/package.json +1 -1
- package/src/application/release/release-node-package.mjs +13 -1
- package/src/application/release/release-packagist-package.mjs +13 -1
- package/src/release/release-type.mjs +293 -0
- package/src/release/shared.mjs +4 -11
- package/src/release-node.mjs +1 -1
- package/src/release-packagist.mjs +1 -1
package/package.json
CHANGED
|
@@ -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}.`)
|
|
@@ -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
|
@@ -16,16 +16,9 @@ import {
|
|
|
16
16
|
parseWorkingTreeStatus,
|
|
17
17
|
suggestReleaseCommitMessage
|
|
18
18
|
} from './commit-message.mjs'
|
|
19
|
+
import {RELEASE_TYPES as SUPPORTED_RELEASE_TYPES} from './release-type.mjs'
|
|
19
20
|
|
|
20
|
-
const RELEASE_TYPES = new Set(
|
|
21
|
-
'major',
|
|
22
|
-
'minor',
|
|
23
|
-
'patch',
|
|
24
|
-
'premajor',
|
|
25
|
-
'preminor',
|
|
26
|
-
'prepatch',
|
|
27
|
-
'prerelease'
|
|
28
|
-
])
|
|
21
|
+
const RELEASE_TYPES = new Set(SUPPORTED_RELEASE_TYPES)
|
|
29
22
|
const DIRTY_WORKING_TREE_MESSAGE = 'Working tree has uncommitted changes. Commit or stash them before releasing.'
|
|
30
23
|
const DIRTY_WORKING_TREE_CANCELLED_MESSAGE = 'Release cancelled: pending changes were not committed.'
|
|
31
24
|
|
|
@@ -58,9 +51,9 @@ export function parseReleaseArgs({
|
|
|
58
51
|
|
|
59
52
|
const positionals = filteredArgs.filter((arg) => !arg.startsWith('--'))
|
|
60
53
|
const presentFlags = new Set(filteredArgs.filter((arg) => arg.startsWith('--')))
|
|
61
|
-
const releaseType = positionals[0] ??
|
|
54
|
+
const releaseType = positionals[0] ?? null
|
|
62
55
|
|
|
63
|
-
if (!RELEASE_TYPES.has(releaseType)) {
|
|
56
|
+
if (releaseType && !RELEASE_TYPES.has(releaseType)) {
|
|
64
57
|
throw new Error(
|
|
65
58
|
`Invalid release type "${releaseType}". Use one of: ${Array.from(RELEASE_TYPES).join(', ')}.`
|
|
66
59
|
)
|
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
|