@wyxos/zephyr 0.2.2 → 0.2.3
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 +49 -48
- package/src/index.mjs +18 -1
- package/src/release-node.mjs +462 -462
- package/src/release-packagist.mjs +332 -0
package/src/release-node.mjs
CHANGED
|
@@ -1,462 +1,462 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process'
|
|
2
|
-
import { fileURLToPath } from 'node:url'
|
|
3
|
-
import { dirname, join } from 'node:path'
|
|
4
|
-
import { readFile } from 'node:fs/promises'
|
|
5
|
-
import fs from 'node:fs'
|
|
6
|
-
import path from 'node:path'
|
|
7
|
-
import process from 'node:process'
|
|
8
|
-
|
|
9
|
-
const STEP_PREFIX = '→'
|
|
10
|
-
const OK_PREFIX = '✔'
|
|
11
|
-
const WARN_PREFIX = '⚠'
|
|
12
|
-
|
|
13
|
-
const IS_WINDOWS = process.platform === 'win32'
|
|
14
|
-
|
|
15
|
-
function logStep(message) {
|
|
16
|
-
console.log(`${STEP_PREFIX} ${message}`)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function logSuccess(message) {
|
|
20
|
-
console.log(`${OK_PREFIX} ${message}`)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function logWarning(message) {
|
|
24
|
-
console.warn(`${WARN_PREFIX} ${message}`)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function runCommand(command, args, { cwd = process.cwd(), capture = false, useShell = false } = {}) {
|
|
28
|
-
return new Promise((resolve, reject) => {
|
|
29
|
-
// On Windows, npm-related commands need shell: true to resolve npx.cmd
|
|
30
|
-
// Git commands work fine without shell, so we only use it when explicitly requested
|
|
31
|
-
const spawnOptions = {
|
|
32
|
-
cwd,
|
|
33
|
-
stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit'
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (useShell || (IS_WINDOWS && (command === 'npm' || command === 'npx'))) {
|
|
37
|
-
spawnOptions.shell = true
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const child = spawn(command, args, spawnOptions)
|
|
41
|
-
let stdout = ''
|
|
42
|
-
let stderr = ''
|
|
43
|
-
|
|
44
|
-
if (capture) {
|
|
45
|
-
child.stdout.on('data', (chunk) => {
|
|
46
|
-
stdout += chunk
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
child.stderr.on('data', (chunk) => {
|
|
50
|
-
stderr += chunk
|
|
51
|
-
})
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
child.on('error', reject)
|
|
55
|
-
child.on('close', (code) => {
|
|
56
|
-
if (code === 0) {
|
|
57
|
-
resolve(capture ? { stdout: stdout.trim(), stderr: stderr.trim() } : undefined)
|
|
58
|
-
} else {
|
|
59
|
-
const error = new Error(`Command failed (${code}): ${command} ${args.join(' ')}`)
|
|
60
|
-
if (capture) {
|
|
61
|
-
error.stdout = stdout
|
|
62
|
-
error.stderr = stderr
|
|
63
|
-
}
|
|
64
|
-
error.exitCode = code
|
|
65
|
-
reject(error)
|
|
66
|
-
}
|
|
67
|
-
})
|
|
68
|
-
})
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async function readPackage(rootDir = process.cwd()) {
|
|
72
|
-
const packagePath = join(rootDir, 'package.json')
|
|
73
|
-
const raw = await readFile(packagePath, 'utf8')
|
|
74
|
-
return JSON.parse(raw)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function hasScript(pkg, scriptName) {
|
|
78
|
-
return pkg?.scripts?.[scriptName] !== undefined
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async function ensureCleanWorkingTree(rootDir = process.cwd()) {
|
|
82
|
-
const { stdout } = await runCommand('git', ['status', '--porcelain'], { capture: true, cwd: rootDir })
|
|
83
|
-
|
|
84
|
-
if (stdout.length > 0) {
|
|
85
|
-
throw new Error('Working tree has uncommitted changes. Commit or stash them before releasing.')
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async function getCurrentBranch(rootDir = process.cwd()) {
|
|
90
|
-
const { stdout } = await runCommand('git', ['branch', '--show-current'], { capture: true, cwd: rootDir })
|
|
91
|
-
return stdout || null
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async function getUpstreamRef(rootDir = process.cwd()) {
|
|
95
|
-
try {
|
|
96
|
-
const { stdout } = await runCommand('git', ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'], {
|
|
97
|
-
capture: true,
|
|
98
|
-
cwd: rootDir
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
return stdout || null
|
|
102
|
-
} catch {
|
|
103
|
-
return null
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
async function ensureUpToDateWithUpstream(branch, upstreamRef, rootDir = process.cwd()) {
|
|
108
|
-
if (!upstreamRef) {
|
|
109
|
-
logWarning(`Branch ${branch} has no upstream configured; skipping ahead/behind checks.`)
|
|
110
|
-
return
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const [remoteName, ...branchParts] = upstreamRef.split('/')
|
|
114
|
-
const remoteBranch = branchParts.join('/')
|
|
115
|
-
|
|
116
|
-
if (remoteName && remoteBranch) {
|
|
117
|
-
logStep(`Fetching latest updates from ${remoteName}/${remoteBranch}...`)
|
|
118
|
-
try {
|
|
119
|
-
await runCommand('git', ['fetch', remoteName, remoteBranch], { cwd: rootDir })
|
|
120
|
-
} catch (error) {
|
|
121
|
-
throw new Error(`Failed to fetch ${upstreamRef}: ${error.message}`)
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const aheadResult = await runCommand('git', ['rev-list', '--count', `${upstreamRef}..HEAD`], {
|
|
126
|
-
capture: true,
|
|
127
|
-
cwd: rootDir
|
|
128
|
-
})
|
|
129
|
-
const behindResult = await runCommand('git', ['rev-list', '--count', `HEAD..${upstreamRef}`], {
|
|
130
|
-
capture: true,
|
|
131
|
-
cwd: rootDir
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
const ahead = Number.parseInt(aheadResult.stdout || '0', 10)
|
|
135
|
-
const behind = Number.parseInt(behindResult.stdout || '0', 10)
|
|
136
|
-
|
|
137
|
-
if (Number.isFinite(behind) && behind > 0) {
|
|
138
|
-
if (remoteName && remoteBranch) {
|
|
139
|
-
logStep(`Fast-forwarding ${branch} with ${upstreamRef}...`)
|
|
140
|
-
|
|
141
|
-
try {
|
|
142
|
-
await runCommand('git', ['pull', '--ff-only', remoteName, remoteBranch], { cwd: rootDir })
|
|
143
|
-
} catch (error) {
|
|
144
|
-
throw new Error(
|
|
145
|
-
`Unable to fast-forward ${branch} with ${upstreamRef}. Resolve conflicts manually, then rerun the release.\n${error.message}`
|
|
146
|
-
)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return ensureUpToDateWithUpstream(branch, upstreamRef, rootDir)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
throw new Error(
|
|
153
|
-
`Branch ${branch} is behind ${upstreamRef} by ${behind} commit${behind === 1 ? '' : 's'}. Pull or rebase first.`
|
|
154
|
-
)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (Number.isFinite(ahead) && ahead > 0) {
|
|
158
|
-
logWarning(`Branch ${branch} is ahead of ${upstreamRef} by ${ahead} commit${ahead === 1 ? '' : 's'}.`)
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function parseArgs() {
|
|
163
|
-
const args = process.argv.slice(2)
|
|
164
|
-
// Filter out --type flag as it's handled by zephyr CLI
|
|
165
|
-
const filteredArgs = args.filter((arg) => !arg.startsWith('--type='))
|
|
166
|
-
const positionals = filteredArgs.filter((arg) => !arg.startsWith('--'))
|
|
167
|
-
const flags = new Set(filteredArgs.filter((arg) => arg.startsWith('--')))
|
|
168
|
-
|
|
169
|
-
const releaseType = positionals[0] ?? 'patch'
|
|
170
|
-
const skipTests = flags.has('--skip-tests')
|
|
171
|
-
const skipLint = flags.has('--skip-lint')
|
|
172
|
-
const skipBuild = flags.has('--skip-build')
|
|
173
|
-
const skipDeploy = flags.has('--skip-deploy')
|
|
174
|
-
|
|
175
|
-
const allowedTypes = new Set([
|
|
176
|
-
'major',
|
|
177
|
-
'minor',
|
|
178
|
-
'patch',
|
|
179
|
-
'premajor',
|
|
180
|
-
'preminor',
|
|
181
|
-
'prepatch',
|
|
182
|
-
'prerelease'
|
|
183
|
-
])
|
|
184
|
-
|
|
185
|
-
if (!allowedTypes.has(releaseType)) {
|
|
186
|
-
throw new Error(
|
|
187
|
-
`Invalid release type "${releaseType}". Use one of: ${Array.from(allowedTypes).join(', ')}.`
|
|
188
|
-
)
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return { releaseType, skipTests, skipLint, skipBuild, skipDeploy }
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
async function runLint(skipLint, pkg, rootDir = process.cwd()) {
|
|
195
|
-
if (skipLint) {
|
|
196
|
-
logWarning('Skipping lint because --skip-lint flag was provided.')
|
|
197
|
-
return
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
if (!hasScript(pkg, 'lint')) {
|
|
201
|
-
logStep('Skipping lint (no lint script found in package.json).')
|
|
202
|
-
return
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
logStep('Running lint...')
|
|
206
|
-
await runCommand('npm', ['run', 'lint'], { cwd: rootDir })
|
|
207
|
-
logSuccess('Lint passed.')
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
async function runTests(skipTests, pkg, rootDir = process.cwd()) {
|
|
211
|
-
if (skipTests) {
|
|
212
|
-
logWarning('Skipping tests because --skip-tests flag was provided.')
|
|
213
|
-
return
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Check for test:run or test script
|
|
217
|
-
if (!hasScript(pkg, 'test:run') && !hasScript(pkg, 'test')) {
|
|
218
|
-
logStep('Skipping tests (no test or test:run script found in package.json).')
|
|
219
|
-
return
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
logStep('Running test suite...')
|
|
223
|
-
|
|
224
|
-
// Prefer test:run if available, otherwise use test with --run flag
|
|
225
|
-
if (hasScript(pkg, 'test:run')) {
|
|
226
|
-
await runCommand('npm', ['run', 'test:run'], { cwd: rootDir })
|
|
227
|
-
} else {
|
|
228
|
-
// For test script, try to pass --run flag (works with vitest)
|
|
229
|
-
await runCommand('npm', ['test', '--', '--run'], { cwd: rootDir })
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
logSuccess('Tests passed.')
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
async function runBuild(skipBuild, pkg, rootDir = process.cwd()) {
|
|
236
|
-
if (skipBuild) {
|
|
237
|
-
logWarning('Skipping build because --skip-build flag was provided.')
|
|
238
|
-
return
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (!hasScript(pkg, 'build')) {
|
|
242
|
-
logStep('Skipping build (no build script found in package.json).')
|
|
243
|
-
return
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
logStep('Building project...')
|
|
247
|
-
await runCommand('npm', ['run', 'build'], { cwd: rootDir })
|
|
248
|
-
logSuccess('Build completed.')
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
async function runLibBuild(skipBuild, pkg, rootDir = process.cwd()) {
|
|
252
|
-
if (skipBuild) {
|
|
253
|
-
logWarning('Skipping library build because --skip-build flag was provided.')
|
|
254
|
-
return
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
if (!hasScript(pkg, 'build:lib')) {
|
|
258
|
-
logStep('Skipping library build (no build:lib script found in package.json).')
|
|
259
|
-
return false
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
logStep('Building library...')
|
|
263
|
-
await runCommand('npm', ['run', 'build:lib'], { cwd: rootDir })
|
|
264
|
-
logSuccess('Library built.')
|
|
265
|
-
|
|
266
|
-
// Check for lib changes and commit them if any
|
|
267
|
-
const { stdout: statusAfterBuild } = await runCommand('git', ['status', '--porcelain'], { capture: true, cwd: rootDir })
|
|
268
|
-
const hasLibChanges = statusAfterBuild.split('\n').some(line => {
|
|
269
|
-
const trimmed = line.trim()
|
|
270
|
-
return trimmed.includes('lib/') && (trimmed.startsWith('M') || trimmed.startsWith('??') || trimmed.startsWith('A') || trimmed.startsWith('D'))
|
|
271
|
-
})
|
|
272
|
-
|
|
273
|
-
if (hasLibChanges) {
|
|
274
|
-
logStep('Committing lib build artifacts...')
|
|
275
|
-
await runCommand('git', ['add', 'lib/'], { cwd: rootDir })
|
|
276
|
-
await runCommand('git', ['commit', '-m', 'chore: build lib artifacts'], { cwd: rootDir })
|
|
277
|
-
logSuccess('Lib build artifacts committed.')
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
return hasLibChanges
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
async function ensureNpmAuth(rootDir = process.cwd()) {
|
|
284
|
-
logStep('Confirming npm authentication...')
|
|
285
|
-
await runCommand('npm', ['whoami'], { cwd: rootDir })
|
|
286
|
-
logSuccess('npm authenticated.')
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
async function bumpVersion(releaseType, rootDir = process.cwd()) {
|
|
290
|
-
logStep(`Bumping package version...`)
|
|
291
|
-
|
|
292
|
-
// Lib changes should already be committed by runLibBuild, but check anyway
|
|
293
|
-
const { stdout: statusBefore } = await runCommand('git', ['status', '--porcelain'], { capture: true, cwd: rootDir })
|
|
294
|
-
const hasLibChanges = statusBefore.split('\n').some(line => {
|
|
295
|
-
const trimmed = line.trim()
|
|
296
|
-
return trimmed.includes('lib/') && (trimmed.startsWith('M') || trimmed.startsWith('??') || trimmed.startsWith('A') || trimmed.startsWith('D'))
|
|
297
|
-
})
|
|
298
|
-
|
|
299
|
-
if (hasLibChanges) {
|
|
300
|
-
logStep('Stashing lib build artifacts...')
|
|
301
|
-
await runCommand('git', ['stash', 'push', '-u', '-m', 'temp: lib build artifacts', 'lib/'], { cwd: rootDir })
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
try {
|
|
305
|
-
// npm version will update package.json and create a commit with default message
|
|
306
|
-
await runCommand('npm', ['version', releaseType], { cwd: rootDir })
|
|
307
|
-
} finally {
|
|
308
|
-
// Restore lib changes and ensure they're in the commit
|
|
309
|
-
if (hasLibChanges) {
|
|
310
|
-
logStep('Restoring lib build artifacts...')
|
|
311
|
-
await runCommand('git', ['stash', 'pop'], { cwd: rootDir })
|
|
312
|
-
await runCommand('git', ['add', 'lib/'], { cwd: rootDir })
|
|
313
|
-
const { stdout: statusAfter } = await runCommand('git', ['status', '--porcelain'], { capture: true, cwd: rootDir })
|
|
314
|
-
if (statusAfter.includes('lib/')) {
|
|
315
|
-
await runCommand('git', ['commit', '--amend', '--no-edit'], { cwd: rootDir })
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const pkg = await readPackage(rootDir)
|
|
321
|
-
const commitMessage = `chore: release ${pkg.version}`
|
|
322
|
-
|
|
323
|
-
// Amend the commit message to use our custom format
|
|
324
|
-
await runCommand('git', ['commit', '--amend', '-m', commitMessage], { cwd: rootDir })
|
|
325
|
-
|
|
326
|
-
logSuccess(`Version updated to ${pkg.version}.`)
|
|
327
|
-
return pkg
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
async function pushChanges(rootDir = process.cwd()) {
|
|
331
|
-
logStep('Pushing commits and tags to origin...')
|
|
332
|
-
await runCommand('git', ['push', '--follow-tags'], { cwd: rootDir })
|
|
333
|
-
logSuccess('Git push completed.')
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
async function publishPackage(pkg, rootDir = process.cwd()) {
|
|
337
|
-
const publishArgs = ['publish', '--ignore-scripts'] // Skip prepublishOnly since we already built lib
|
|
338
|
-
|
|
339
|
-
if (pkg.name.startsWith('@')) {
|
|
340
|
-
publishArgs.push('--access', 'public')
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
logStep(`Publishing ${pkg.name}@${pkg.version} to npm...`)
|
|
344
|
-
await runCommand('npm', publishArgs, { cwd: rootDir })
|
|
345
|
-
logSuccess('npm publish completed.')
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function extractDomainFromHomepage(homepage) {
|
|
349
|
-
if (!homepage) return null
|
|
350
|
-
try {
|
|
351
|
-
const url = new URL(homepage)
|
|
352
|
-
return url.hostname
|
|
353
|
-
} catch {
|
|
354
|
-
// If it's not a valid URL, try to extract domain from string
|
|
355
|
-
const match = homepage.match(/(?:https?:\/\/)?([^\/]+)/)
|
|
356
|
-
return match ? match[1] : null
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
async function deployGHPages(skipDeploy, pkg, rootDir = process.cwd()) {
|
|
361
|
-
if (skipDeploy) {
|
|
362
|
-
logWarning('Skipping GitHub Pages deployment because --skip-deploy flag was provided.')
|
|
363
|
-
return
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// Check if dist directory exists (indicates build output for deployment)
|
|
367
|
-
const distPath = path.join(rootDir, 'dist')
|
|
368
|
-
let distExists = false
|
|
369
|
-
try {
|
|
370
|
-
const stats = await fs.promises.stat(distPath)
|
|
371
|
-
distExists = stats.isDirectory()
|
|
372
|
-
} catch {
|
|
373
|
-
distExists = false
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
if (!distExists) {
|
|
377
|
-
logStep('Skipping GitHub Pages deployment (no dist directory found).')
|
|
378
|
-
return
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
logStep('Deploying to GitHub Pages...')
|
|
382
|
-
|
|
383
|
-
// Write CNAME file to dist if homepage is set
|
|
384
|
-
const cnamePath = path.join(distPath, 'CNAME')
|
|
385
|
-
|
|
386
|
-
if (pkg.homepage) {
|
|
387
|
-
const domain = extractDomainFromHomepage(pkg.homepage)
|
|
388
|
-
if (domain) {
|
|
389
|
-
try {
|
|
390
|
-
await fs.promises.mkdir(distPath, { recursive: true })
|
|
391
|
-
await fs.promises.writeFile(cnamePath, domain)
|
|
392
|
-
} catch (error) {
|
|
393
|
-
logWarning(`Could not write CNAME file: ${error.message}`)
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
const worktreeDir = path.resolve(rootDir, '.gh-pages')
|
|
399
|
-
|
|
400
|
-
try {
|
|
401
|
-
await runCommand('git', ['worktree', 'remove', worktreeDir, '-f'], { cwd: rootDir })
|
|
402
|
-
} catch { }
|
|
403
|
-
|
|
404
|
-
try {
|
|
405
|
-
await runCommand('git', ['worktree', 'add', worktreeDir, 'gh-pages'], { cwd: rootDir })
|
|
406
|
-
} catch {
|
|
407
|
-
await runCommand('git', ['worktree', 'add', worktreeDir, '-b', 'gh-pages'], { cwd: rootDir })
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
await runCommand('git', ['-C', worktreeDir, 'config', 'user.name', 'wyxos'])
|
|
411
|
-
await runCommand('git', ['-C', worktreeDir, 'config', 'user.email', 'github@wyxos.com'])
|
|
412
|
-
|
|
413
|
-
// Clear worktree directory
|
|
414
|
-
for (const entry of fs.readdirSync(worktreeDir)) {
|
|
415
|
-
if (entry === '.git') continue
|
|
416
|
-
const target = path.join(worktreeDir, entry)
|
|
417
|
-
fs.rmSync(target, { recursive: true, force: true })
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// Copy dist to worktree
|
|
421
|
-
fs.cpSync(distPath, worktreeDir, { recursive: true })
|
|
422
|
-
|
|
423
|
-
await runCommand('git', ['-C', worktreeDir, 'add', '-A'])
|
|
424
|
-
await runCommand('git', ['-C', worktreeDir, 'commit', '-m', `deploy: demo ${new Date().toISOString()}`, '--allow-empty'])
|
|
425
|
-
await runCommand('git', ['-C', worktreeDir, 'push', '-f', 'origin', 'gh-pages'])
|
|
426
|
-
|
|
427
|
-
logSuccess('GitHub Pages deployment completed.')
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
export async function releaseNode() {
|
|
431
|
-
const { releaseType, skipTests, skipLint, skipBuild, skipDeploy } = parseArgs()
|
|
432
|
-
const rootDir = process.cwd()
|
|
433
|
-
|
|
434
|
-
logStep('Reading package metadata...')
|
|
435
|
-
const pkg = await readPackage(rootDir)
|
|
436
|
-
|
|
437
|
-
logStep('Checking working tree status...')
|
|
438
|
-
await ensureCleanWorkingTree(rootDir)
|
|
439
|
-
|
|
440
|
-
const branch = await getCurrentBranch(rootDir)
|
|
441
|
-
if (!branch) {
|
|
442
|
-
throw new Error('Unable to determine current branch.')
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
logStep(`Current branch: ${branch}`)
|
|
446
|
-
const upstreamRef = await getUpstreamRef(rootDir)
|
|
447
|
-
await ensureUpToDateWithUpstream(branch, upstreamRef, rootDir)
|
|
448
|
-
|
|
449
|
-
await runLint(skipLint, pkg, rootDir)
|
|
450
|
-
await runTests(skipTests, pkg, rootDir)
|
|
451
|
-
await runBuild(skipBuild, pkg, rootDir)
|
|
452
|
-
await runLibBuild(skipBuild, pkg, rootDir)
|
|
453
|
-
await ensureNpmAuth(rootDir)
|
|
454
|
-
|
|
455
|
-
const updatedPkg = await bumpVersion(releaseType, rootDir)
|
|
456
|
-
await pushChanges(rootDir)
|
|
457
|
-
await publishPackage(updatedPkg, rootDir)
|
|
458
|
-
await deployGHPages(skipDeploy, updatedPkg, rootDir)
|
|
459
|
-
|
|
460
|
-
logSuccess(`Release workflow completed for ${updatedPkg.name}@${updatedPkg.version}.`)
|
|
461
|
-
}
|
|
462
|
-
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { dirname, join } from 'node:path'
|
|
4
|
+
import { readFile } from 'node:fs/promises'
|
|
5
|
+
import fs from 'node:fs'
|
|
6
|
+
import path from 'node:path'
|
|
7
|
+
import process from 'node:process'
|
|
8
|
+
|
|
9
|
+
const STEP_PREFIX = '→'
|
|
10
|
+
const OK_PREFIX = '✔'
|
|
11
|
+
const WARN_PREFIX = '⚠'
|
|
12
|
+
|
|
13
|
+
const IS_WINDOWS = process.platform === 'win32'
|
|
14
|
+
|
|
15
|
+
function logStep(message) {
|
|
16
|
+
console.log(`${STEP_PREFIX} ${message}`)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function logSuccess(message) {
|
|
20
|
+
console.log(`${OK_PREFIX} ${message}`)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function logWarning(message) {
|
|
24
|
+
console.warn(`${WARN_PREFIX} ${message}`)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function runCommand(command, args, { cwd = process.cwd(), capture = false, useShell = false } = {}) {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
// On Windows, npm-related commands need shell: true to resolve npx.cmd
|
|
30
|
+
// Git commands work fine without shell, so we only use it when explicitly requested
|
|
31
|
+
const spawnOptions = {
|
|
32
|
+
cwd,
|
|
33
|
+
stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (useShell || (IS_WINDOWS && (command === 'npm' || command === 'npx'))) {
|
|
37
|
+
spawnOptions.shell = true
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const child = spawn(command, args, spawnOptions)
|
|
41
|
+
let stdout = ''
|
|
42
|
+
let stderr = ''
|
|
43
|
+
|
|
44
|
+
if (capture) {
|
|
45
|
+
child.stdout.on('data', (chunk) => {
|
|
46
|
+
stdout += chunk
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
child.stderr.on('data', (chunk) => {
|
|
50
|
+
stderr += chunk
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
child.on('error', reject)
|
|
55
|
+
child.on('close', (code) => {
|
|
56
|
+
if (code === 0) {
|
|
57
|
+
resolve(capture ? { stdout: stdout.trim(), stderr: stderr.trim() } : undefined)
|
|
58
|
+
} else {
|
|
59
|
+
const error = new Error(`Command failed (${code}): ${command} ${args.join(' ')}`)
|
|
60
|
+
if (capture) {
|
|
61
|
+
error.stdout = stdout
|
|
62
|
+
error.stderr = stderr
|
|
63
|
+
}
|
|
64
|
+
error.exitCode = code
|
|
65
|
+
reject(error)
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function readPackage(rootDir = process.cwd()) {
|
|
72
|
+
const packagePath = join(rootDir, 'package.json')
|
|
73
|
+
const raw = await readFile(packagePath, 'utf8')
|
|
74
|
+
return JSON.parse(raw)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function hasScript(pkg, scriptName) {
|
|
78
|
+
return pkg?.scripts?.[scriptName] !== undefined
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function ensureCleanWorkingTree(rootDir = process.cwd()) {
|
|
82
|
+
const { stdout } = await runCommand('git', ['status', '--porcelain'], { capture: true, cwd: rootDir })
|
|
83
|
+
|
|
84
|
+
if (stdout.length > 0) {
|
|
85
|
+
throw new Error('Working tree has uncommitted changes. Commit or stash them before releasing.')
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function getCurrentBranch(rootDir = process.cwd()) {
|
|
90
|
+
const { stdout } = await runCommand('git', ['branch', '--show-current'], { capture: true, cwd: rootDir })
|
|
91
|
+
return stdout || null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function getUpstreamRef(rootDir = process.cwd()) {
|
|
95
|
+
try {
|
|
96
|
+
const { stdout } = await runCommand('git', ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'], {
|
|
97
|
+
capture: true,
|
|
98
|
+
cwd: rootDir
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
return stdout || null
|
|
102
|
+
} catch {
|
|
103
|
+
return null
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function ensureUpToDateWithUpstream(branch, upstreamRef, rootDir = process.cwd()) {
|
|
108
|
+
if (!upstreamRef) {
|
|
109
|
+
logWarning(`Branch ${branch} has no upstream configured; skipping ahead/behind checks.`)
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const [remoteName, ...branchParts] = upstreamRef.split('/')
|
|
114
|
+
const remoteBranch = branchParts.join('/')
|
|
115
|
+
|
|
116
|
+
if (remoteName && remoteBranch) {
|
|
117
|
+
logStep(`Fetching latest updates from ${remoteName}/${remoteBranch}...`)
|
|
118
|
+
try {
|
|
119
|
+
await runCommand('git', ['fetch', remoteName, remoteBranch], { cwd: rootDir })
|
|
120
|
+
} catch (error) {
|
|
121
|
+
throw new Error(`Failed to fetch ${upstreamRef}: ${error.message}`)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const aheadResult = await runCommand('git', ['rev-list', '--count', `${upstreamRef}..HEAD`], {
|
|
126
|
+
capture: true,
|
|
127
|
+
cwd: rootDir
|
|
128
|
+
})
|
|
129
|
+
const behindResult = await runCommand('git', ['rev-list', '--count', `HEAD..${upstreamRef}`], {
|
|
130
|
+
capture: true,
|
|
131
|
+
cwd: rootDir
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
const ahead = Number.parseInt(aheadResult.stdout || '0', 10)
|
|
135
|
+
const behind = Number.parseInt(behindResult.stdout || '0', 10)
|
|
136
|
+
|
|
137
|
+
if (Number.isFinite(behind) && behind > 0) {
|
|
138
|
+
if (remoteName && remoteBranch) {
|
|
139
|
+
logStep(`Fast-forwarding ${branch} with ${upstreamRef}...`)
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
await runCommand('git', ['pull', '--ff-only', remoteName, remoteBranch], { cwd: rootDir })
|
|
143
|
+
} catch (error) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`Unable to fast-forward ${branch} with ${upstreamRef}. Resolve conflicts manually, then rerun the release.\n${error.message}`
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return ensureUpToDateWithUpstream(branch, upstreamRef, rootDir)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
throw new Error(
|
|
153
|
+
`Branch ${branch} is behind ${upstreamRef} by ${behind} commit${behind === 1 ? '' : 's'}. Pull or rebase first.`
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (Number.isFinite(ahead) && ahead > 0) {
|
|
158
|
+
logWarning(`Branch ${branch} is ahead of ${upstreamRef} by ${ahead} commit${ahead === 1 ? '' : 's'}.`)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function parseArgs() {
|
|
163
|
+
const args = process.argv.slice(2)
|
|
164
|
+
// Filter out --type flag as it's handled by zephyr CLI
|
|
165
|
+
const filteredArgs = args.filter((arg) => !arg.startsWith('--type='))
|
|
166
|
+
const positionals = filteredArgs.filter((arg) => !arg.startsWith('--'))
|
|
167
|
+
const flags = new Set(filteredArgs.filter((arg) => arg.startsWith('--')))
|
|
168
|
+
|
|
169
|
+
const releaseType = positionals[0] ?? 'patch'
|
|
170
|
+
const skipTests = flags.has('--skip-tests')
|
|
171
|
+
const skipLint = flags.has('--skip-lint')
|
|
172
|
+
const skipBuild = flags.has('--skip-build')
|
|
173
|
+
const skipDeploy = flags.has('--skip-deploy')
|
|
174
|
+
|
|
175
|
+
const allowedTypes = new Set([
|
|
176
|
+
'major',
|
|
177
|
+
'minor',
|
|
178
|
+
'patch',
|
|
179
|
+
'premajor',
|
|
180
|
+
'preminor',
|
|
181
|
+
'prepatch',
|
|
182
|
+
'prerelease'
|
|
183
|
+
])
|
|
184
|
+
|
|
185
|
+
if (!allowedTypes.has(releaseType)) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
`Invalid release type "${releaseType}". Use one of: ${Array.from(allowedTypes).join(', ')}.`
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return { releaseType, skipTests, skipLint, skipBuild, skipDeploy }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function runLint(skipLint, pkg, rootDir = process.cwd()) {
|
|
195
|
+
if (skipLint) {
|
|
196
|
+
logWarning('Skipping lint because --skip-lint flag was provided.')
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!hasScript(pkg, 'lint')) {
|
|
201
|
+
logStep('Skipping lint (no lint script found in package.json).')
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
logStep('Running lint...')
|
|
206
|
+
await runCommand('npm', ['run', 'lint'], { cwd: rootDir })
|
|
207
|
+
logSuccess('Lint passed.')
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function runTests(skipTests, pkg, rootDir = process.cwd()) {
|
|
211
|
+
if (skipTests) {
|
|
212
|
+
logWarning('Skipping tests because --skip-tests flag was provided.')
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Check for test:run or test script
|
|
217
|
+
if (!hasScript(pkg, 'test:run') && !hasScript(pkg, 'test')) {
|
|
218
|
+
logStep('Skipping tests (no test or test:run script found in package.json).')
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
logStep('Running test suite...')
|
|
223
|
+
|
|
224
|
+
// Prefer test:run if available, otherwise use test with --run flag
|
|
225
|
+
if (hasScript(pkg, 'test:run')) {
|
|
226
|
+
await runCommand('npm', ['run', 'test:run'], { cwd: rootDir })
|
|
227
|
+
} else {
|
|
228
|
+
// For test script, try to pass --run flag (works with vitest)
|
|
229
|
+
await runCommand('npm', ['test', '--', '--run'], { cwd: rootDir })
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
logSuccess('Tests passed.')
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function runBuild(skipBuild, pkg, rootDir = process.cwd()) {
|
|
236
|
+
if (skipBuild) {
|
|
237
|
+
logWarning('Skipping build because --skip-build flag was provided.')
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (!hasScript(pkg, 'build')) {
|
|
242
|
+
logStep('Skipping build (no build script found in package.json).')
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
logStep('Building project...')
|
|
247
|
+
await runCommand('npm', ['run', 'build'], { cwd: rootDir })
|
|
248
|
+
logSuccess('Build completed.')
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function runLibBuild(skipBuild, pkg, rootDir = process.cwd()) {
|
|
252
|
+
if (skipBuild) {
|
|
253
|
+
logWarning('Skipping library build because --skip-build flag was provided.')
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (!hasScript(pkg, 'build:lib')) {
|
|
258
|
+
logStep('Skipping library build (no build:lib script found in package.json).')
|
|
259
|
+
return false
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
logStep('Building library...')
|
|
263
|
+
await runCommand('npm', ['run', 'build:lib'], { cwd: rootDir })
|
|
264
|
+
logSuccess('Library built.')
|
|
265
|
+
|
|
266
|
+
// Check for lib changes and commit them if any
|
|
267
|
+
const { stdout: statusAfterBuild } = await runCommand('git', ['status', '--porcelain'], { capture: true, cwd: rootDir })
|
|
268
|
+
const hasLibChanges = statusAfterBuild.split('\n').some(line => {
|
|
269
|
+
const trimmed = line.trim()
|
|
270
|
+
return trimmed.includes('lib/') && (trimmed.startsWith('M') || trimmed.startsWith('??') || trimmed.startsWith('A') || trimmed.startsWith('D'))
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
if (hasLibChanges) {
|
|
274
|
+
logStep('Committing lib build artifacts...')
|
|
275
|
+
await runCommand('git', ['add', 'lib/'], { cwd: rootDir })
|
|
276
|
+
await runCommand('git', ['commit', '-m', 'chore: build lib artifacts'], { cwd: rootDir })
|
|
277
|
+
logSuccess('Lib build artifacts committed.')
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return hasLibChanges
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function ensureNpmAuth(rootDir = process.cwd()) {
|
|
284
|
+
logStep('Confirming npm authentication...')
|
|
285
|
+
await runCommand('npm', ['whoami'], { cwd: rootDir })
|
|
286
|
+
logSuccess('npm authenticated.')
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function bumpVersion(releaseType, rootDir = process.cwd()) {
|
|
290
|
+
logStep(`Bumping package version...`)
|
|
291
|
+
|
|
292
|
+
// Lib changes should already be committed by runLibBuild, but check anyway
|
|
293
|
+
const { stdout: statusBefore } = await runCommand('git', ['status', '--porcelain'], { capture: true, cwd: rootDir })
|
|
294
|
+
const hasLibChanges = statusBefore.split('\n').some(line => {
|
|
295
|
+
const trimmed = line.trim()
|
|
296
|
+
return trimmed.includes('lib/') && (trimmed.startsWith('M') || trimmed.startsWith('??') || trimmed.startsWith('A') || trimmed.startsWith('D'))
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
if (hasLibChanges) {
|
|
300
|
+
logStep('Stashing lib build artifacts...')
|
|
301
|
+
await runCommand('git', ['stash', 'push', '-u', '-m', 'temp: lib build artifacts', 'lib/'], { cwd: rootDir })
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
// npm version will update package.json and create a commit with default message
|
|
306
|
+
await runCommand('npm', ['version', releaseType], { cwd: rootDir })
|
|
307
|
+
} finally {
|
|
308
|
+
// Restore lib changes and ensure they're in the commit
|
|
309
|
+
if (hasLibChanges) {
|
|
310
|
+
logStep('Restoring lib build artifacts...')
|
|
311
|
+
await runCommand('git', ['stash', 'pop'], { cwd: rootDir })
|
|
312
|
+
await runCommand('git', ['add', 'lib/'], { cwd: rootDir })
|
|
313
|
+
const { stdout: statusAfter } = await runCommand('git', ['status', '--porcelain'], { capture: true, cwd: rootDir })
|
|
314
|
+
if (statusAfter.includes('lib/')) {
|
|
315
|
+
await runCommand('git', ['commit', '--amend', '--no-edit'], { cwd: rootDir })
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const pkg = await readPackage(rootDir)
|
|
321
|
+
const commitMessage = `chore: release ${pkg.version}`
|
|
322
|
+
|
|
323
|
+
// Amend the commit message to use our custom format
|
|
324
|
+
await runCommand('git', ['commit', '--amend', '-m', commitMessage], { cwd: rootDir })
|
|
325
|
+
|
|
326
|
+
logSuccess(`Version updated to ${pkg.version}.`)
|
|
327
|
+
return pkg
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function pushChanges(rootDir = process.cwd()) {
|
|
331
|
+
logStep('Pushing commits and tags to origin...')
|
|
332
|
+
await runCommand('git', ['push', '--follow-tags'], { cwd: rootDir })
|
|
333
|
+
logSuccess('Git push completed.')
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function publishPackage(pkg, rootDir = process.cwd()) {
|
|
337
|
+
const publishArgs = ['publish', '--ignore-scripts'] // Skip prepublishOnly since we already built lib
|
|
338
|
+
|
|
339
|
+
if (pkg.name.startsWith('@')) {
|
|
340
|
+
publishArgs.push('--access', 'public')
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
logStep(`Publishing ${pkg.name}@${pkg.version} to npm...`)
|
|
344
|
+
await runCommand('npm', publishArgs, { cwd: rootDir })
|
|
345
|
+
logSuccess('npm publish completed.')
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function extractDomainFromHomepage(homepage) {
|
|
349
|
+
if (!homepage) return null
|
|
350
|
+
try {
|
|
351
|
+
const url = new URL(homepage)
|
|
352
|
+
return url.hostname
|
|
353
|
+
} catch {
|
|
354
|
+
// If it's not a valid URL, try to extract domain from string
|
|
355
|
+
const match = homepage.match(/(?:https?:\/\/)?([^\/]+)/)
|
|
356
|
+
return match ? match[1] : null
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function deployGHPages(skipDeploy, pkg, rootDir = process.cwd()) {
|
|
361
|
+
if (skipDeploy) {
|
|
362
|
+
logWarning('Skipping GitHub Pages deployment because --skip-deploy flag was provided.')
|
|
363
|
+
return
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Check if dist directory exists (indicates build output for deployment)
|
|
367
|
+
const distPath = path.join(rootDir, 'dist')
|
|
368
|
+
let distExists = false
|
|
369
|
+
try {
|
|
370
|
+
const stats = await fs.promises.stat(distPath)
|
|
371
|
+
distExists = stats.isDirectory()
|
|
372
|
+
} catch {
|
|
373
|
+
distExists = false
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (!distExists) {
|
|
377
|
+
logStep('Skipping GitHub Pages deployment (no dist directory found).')
|
|
378
|
+
return
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
logStep('Deploying to GitHub Pages...')
|
|
382
|
+
|
|
383
|
+
// Write CNAME file to dist if homepage is set
|
|
384
|
+
const cnamePath = path.join(distPath, 'CNAME')
|
|
385
|
+
|
|
386
|
+
if (pkg.homepage) {
|
|
387
|
+
const domain = extractDomainFromHomepage(pkg.homepage)
|
|
388
|
+
if (domain) {
|
|
389
|
+
try {
|
|
390
|
+
await fs.promises.mkdir(distPath, { recursive: true })
|
|
391
|
+
await fs.promises.writeFile(cnamePath, domain)
|
|
392
|
+
} catch (error) {
|
|
393
|
+
logWarning(`Could not write CNAME file: ${error.message}`)
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const worktreeDir = path.resolve(rootDir, '.gh-pages')
|
|
399
|
+
|
|
400
|
+
try {
|
|
401
|
+
await runCommand('git', ['worktree', 'remove', worktreeDir, '-f'], { cwd: rootDir })
|
|
402
|
+
} catch { }
|
|
403
|
+
|
|
404
|
+
try {
|
|
405
|
+
await runCommand('git', ['worktree', 'add', worktreeDir, 'gh-pages'], { cwd: rootDir })
|
|
406
|
+
} catch {
|
|
407
|
+
await runCommand('git', ['worktree', 'add', worktreeDir, '-b', 'gh-pages'], { cwd: rootDir })
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
await runCommand('git', ['-C', worktreeDir, 'config', 'user.name', 'wyxos'])
|
|
411
|
+
await runCommand('git', ['-C', worktreeDir, 'config', 'user.email', 'github@wyxos.com'])
|
|
412
|
+
|
|
413
|
+
// Clear worktree directory
|
|
414
|
+
for (const entry of fs.readdirSync(worktreeDir)) {
|
|
415
|
+
if (entry === '.git') continue
|
|
416
|
+
const target = path.join(worktreeDir, entry)
|
|
417
|
+
fs.rmSync(target, { recursive: true, force: true })
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Copy dist to worktree
|
|
421
|
+
fs.cpSync(distPath, worktreeDir, { recursive: true })
|
|
422
|
+
|
|
423
|
+
await runCommand('git', ['-C', worktreeDir, 'add', '-A'])
|
|
424
|
+
await runCommand('git', ['-C', worktreeDir, 'commit', '-m', `deploy: demo ${new Date().toISOString()}`, '--allow-empty'])
|
|
425
|
+
await runCommand('git', ['-C', worktreeDir, 'push', '-f', 'origin', 'gh-pages'])
|
|
426
|
+
|
|
427
|
+
logSuccess('GitHub Pages deployment completed.')
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export async function releaseNode() {
|
|
431
|
+
const { releaseType, skipTests, skipLint, skipBuild, skipDeploy } = parseArgs()
|
|
432
|
+
const rootDir = process.cwd()
|
|
433
|
+
|
|
434
|
+
logStep('Reading package metadata...')
|
|
435
|
+
const pkg = await readPackage(rootDir)
|
|
436
|
+
|
|
437
|
+
logStep('Checking working tree status...')
|
|
438
|
+
await ensureCleanWorkingTree(rootDir)
|
|
439
|
+
|
|
440
|
+
const branch = await getCurrentBranch(rootDir)
|
|
441
|
+
if (!branch) {
|
|
442
|
+
throw new Error('Unable to determine current branch.')
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
logStep(`Current branch: ${branch}`)
|
|
446
|
+
const upstreamRef = await getUpstreamRef(rootDir)
|
|
447
|
+
await ensureUpToDateWithUpstream(branch, upstreamRef, rootDir)
|
|
448
|
+
|
|
449
|
+
await runLint(skipLint, pkg, rootDir)
|
|
450
|
+
await runTests(skipTests, pkg, rootDir)
|
|
451
|
+
await runBuild(skipBuild, pkg, rootDir)
|
|
452
|
+
await runLibBuild(skipBuild, pkg, rootDir)
|
|
453
|
+
await ensureNpmAuth(rootDir)
|
|
454
|
+
|
|
455
|
+
const updatedPkg = await bumpVersion(releaseType, rootDir)
|
|
456
|
+
await pushChanges(rootDir)
|
|
457
|
+
await publishPackage(updatedPkg, rootDir)
|
|
458
|
+
await deployGHPages(skipDeploy, updatedPkg, rootDir)
|
|
459
|
+
|
|
460
|
+
logSuccess(`Release workflow completed for ${updatedPkg.name}@${updatedPkg.version}.`)
|
|
461
|
+
}
|
|
462
|
+
|