@wipcomputer/wip-release 1.9.8 → 1.9.10
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/cli.js +31 -14
- package/core.mjs +31 -1
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -20,18 +20,26 @@ const dryRun = args.includes('--dry-run');
|
|
|
20
20
|
const noPublish = args.includes('--no-publish');
|
|
21
21
|
const skipProductCheck = args.includes('--skip-product-check');
|
|
22
22
|
const skipStaleCheck = args.includes('--skip-stale-check');
|
|
23
|
+
const skipWorktreeCheck = args.includes('--skip-worktree-check');
|
|
23
24
|
const notesFilePath = flag('notes-file');
|
|
24
25
|
let notes = flag('notes');
|
|
25
26
|
let notesSource = notes ? 'flag' : 'none'; // track where notes came from
|
|
26
27
|
|
|
27
|
-
//
|
|
28
|
-
//
|
|
28
|
+
// Release notes priority (highest wins):
|
|
29
|
+
// 1. --notes-file=path Explicit file path (always wins)
|
|
30
|
+
// 2. RELEASE-NOTES-v{ver}.md In repo root (always wins over --notes flag)
|
|
31
|
+
// 3. ai/dev-updates/YYYY-MM-DD* Today's dev update (wins over --notes flag if longer)
|
|
32
|
+
// 4. --notes="text" Flag fallback (only if nothing better exists)
|
|
33
|
+
//
|
|
34
|
+
// Rule: written release notes on disk ALWAYS beat a CLI one-liner.
|
|
35
|
+
// The --notes flag is a fallback, not an override.
|
|
29
36
|
{
|
|
30
37
|
const { readFileSync, existsSync } = await import('node:fs');
|
|
31
38
|
const { resolve, join } = await import('node:path');
|
|
39
|
+
const flagNotes = notes; // save original flag value for fallback
|
|
32
40
|
|
|
33
41
|
if (notesFilePath) {
|
|
34
|
-
// Explicit --notes-file
|
|
42
|
+
// 1. Explicit --notes-file (highest priority)
|
|
35
43
|
const resolved = resolve(notesFilePath);
|
|
36
44
|
if (!existsSync(resolved)) {
|
|
37
45
|
console.error(` ✗ Notes file not found: ${resolved}`);
|
|
@@ -39,8 +47,8 @@ let notesSource = notes ? 'flag' : 'none'; // track where notes came from
|
|
|
39
47
|
}
|
|
40
48
|
notes = readFileSync(resolved, 'utf8').trim();
|
|
41
49
|
notesSource = 'file';
|
|
42
|
-
} else if (
|
|
43
|
-
// Auto-detect
|
|
50
|
+
} else if (level) {
|
|
51
|
+
// 2. Auto-detect RELEASE-NOTES-v{version}.md (ALWAYS checks, even if --notes provided)
|
|
44
52
|
try {
|
|
45
53
|
const { detectCurrentVersion, bumpSemver } = await import('./core.mjs');
|
|
46
54
|
const cwd = process.cwd();
|
|
@@ -49,15 +57,19 @@ let notesSource = notes ? 'flag' : 'none'; // track where notes came from
|
|
|
49
57
|
const dashed = newVersion.replace(/\./g, '-');
|
|
50
58
|
const autoFile = join(cwd, `RELEASE-NOTES-v${dashed}.md`);
|
|
51
59
|
if (existsSync(autoFile)) {
|
|
52
|
-
|
|
60
|
+
const fileContent = readFileSync(autoFile, 'utf8').trim();
|
|
61
|
+
if (flagNotes && flagNotes !== fileContent) {
|
|
62
|
+
console.log(` ! --notes flag ignored: RELEASE-NOTES-v${dashed}.md takes priority`);
|
|
63
|
+
}
|
|
64
|
+
notes = fileContent;
|
|
53
65
|
notesSource = 'file';
|
|
54
66
|
console.log(` ✓ Found RELEASE-NOTES-v${dashed}.md`);
|
|
55
67
|
}
|
|
56
68
|
} catch {}
|
|
57
69
|
}
|
|
58
70
|
|
|
59
|
-
// Auto-detect dev update from ai/dev-updates/
|
|
60
|
-
if (level && (!notes || notes.length <
|
|
71
|
+
// 3. Auto-detect dev update from ai/dev-updates/ (wins over --notes flag if longer)
|
|
72
|
+
if (level && (!notes || (notesSource === 'flag' && notes.length < 200))) {
|
|
61
73
|
try {
|
|
62
74
|
const { readdirSync } = await import('node:fs');
|
|
63
75
|
const devUpdatesDir = join(process.cwd(), 'ai', 'dev-updates');
|
|
@@ -72,6 +84,9 @@ let notesSource = notes ? 'flag' : 'none'; // track where notes came from
|
|
|
72
84
|
const devUpdatePath = join(devUpdatesDir, todayFiles[0]);
|
|
73
85
|
const devUpdateContent = readFileSync(devUpdatePath, 'utf8').trim();
|
|
74
86
|
if (devUpdateContent.length > (notes || '').length) {
|
|
87
|
+
if (flagNotes) {
|
|
88
|
+
console.log(` ! --notes flag ignored: dev update takes priority`);
|
|
89
|
+
}
|
|
75
90
|
notes = devUpdateContent;
|
|
76
91
|
notesSource = 'dev-update';
|
|
77
92
|
console.log(` ✓ Found dev update: ai/dev-updates/${todayFiles[0]}`);
|
|
@@ -101,13 +116,14 @@ Flags:
|
|
|
101
116
|
--no-publish Bump + tag only, skip npm/GitHub
|
|
102
117
|
--skip-product-check Skip product docs check (dev update, roadmap, readme-first)
|
|
103
118
|
--skip-stale-check Skip stale remote branch check
|
|
119
|
+
--skip-worktree-check Skip worktree guard (allow release from worktree)
|
|
104
120
|
|
|
105
|
-
Release notes:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
121
|
+
Release notes (highest priority wins, files ALWAYS beat --notes flag):
|
|
122
|
+
1. --notes-file=path Explicit file path (always wins)
|
|
123
|
+
2. RELEASE-NOTES-v{ver}.md In repo root (wins over --notes)
|
|
124
|
+
3. ai/dev-updates/YYYY-MM-DD* Today's dev update (wins over --notes if longer)
|
|
125
|
+
4. --notes="text" Fallback only (use for repos without release notes files)
|
|
126
|
+
Written notes on disk always take priority over a CLI one-liner.
|
|
111
127
|
|
|
112
128
|
Pipeline:
|
|
113
129
|
1. Bump package.json version
|
|
@@ -130,6 +146,7 @@ release({
|
|
|
130
146
|
noPublish,
|
|
131
147
|
skipProductCheck,
|
|
132
148
|
skipStaleCheck,
|
|
149
|
+
skipWorktreeCheck,
|
|
133
150
|
}).catch(err => {
|
|
134
151
|
console.error(` ✗ ${err.message}`);
|
|
135
152
|
process.exit(1);
|
package/core.mjs
CHANGED
|
@@ -552,7 +552,7 @@ export function checkStaleBranches(repoPath, level) {
|
|
|
552
552
|
/**
|
|
553
553
|
* Run the full release pipeline.
|
|
554
554
|
*/
|
|
555
|
-
export async function release({ repoPath, level, notes, notesSource, dryRun, noPublish, skipProductCheck, skipStaleCheck }) {
|
|
555
|
+
export async function release({ repoPath, level, notes, notesSource, dryRun, noPublish, skipProductCheck, skipStaleCheck, skipWorktreeCheck }) {
|
|
556
556
|
repoPath = repoPath || process.cwd();
|
|
557
557
|
const currentVersion = detectCurrentVersion(repoPath);
|
|
558
558
|
const newVersion = bumpSemver(currentVersion, level);
|
|
@@ -562,6 +562,36 @@ export async function release({ repoPath, level, notes, notesSource, dryRun, noP
|
|
|
562
562
|
console.log(` ${repoName}: ${currentVersion} -> ${newVersion} (${level})`);
|
|
563
563
|
console.log(` ${'─'.repeat(40)}`);
|
|
564
564
|
|
|
565
|
+
// -1. Worktree guard: block releases from linked worktrees
|
|
566
|
+
if (!skipWorktreeCheck) {
|
|
567
|
+
try {
|
|
568
|
+
const gitDir = execFileSync('git', ['rev-parse', '--git-dir'], {
|
|
569
|
+
cwd: repoPath, encoding: 'utf8'
|
|
570
|
+
}).trim();
|
|
571
|
+
|
|
572
|
+
// Linked worktrees have "/worktrees/" in their git-dir path
|
|
573
|
+
if (gitDir.includes('/worktrees/')) {
|
|
574
|
+
// Get the main working tree path from `git worktree list`
|
|
575
|
+
const worktreeList = execFileSync('git', ['worktree', 'list', '--porcelain'], {
|
|
576
|
+
cwd: repoPath, encoding: 'utf8'
|
|
577
|
+
});
|
|
578
|
+
const mainWorktree = worktreeList.split('\n')
|
|
579
|
+
.find(line => line.startsWith('worktree '));
|
|
580
|
+
const mainPath = mainWorktree ? mainWorktree.replace('worktree ', '') : '(unknown)';
|
|
581
|
+
|
|
582
|
+
console.log(` \u2717 wip-release must run from the main working tree, not a worktree.`);
|
|
583
|
+
console.log(` Current: ${repoPath}`);
|
|
584
|
+
console.log(` Main working tree: ${mainPath}`);
|
|
585
|
+
console.log(` Switch to the main working tree and run again.`);
|
|
586
|
+
console.log('');
|
|
587
|
+
return { currentVersion, newVersion, dryRun: false, failed: true };
|
|
588
|
+
}
|
|
589
|
+
console.log(' \u2713 Running from main working tree');
|
|
590
|
+
} catch {
|
|
591
|
+
// Git command failed... skip check gracefully
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
565
595
|
// 0. License compliance gate
|
|
566
596
|
const configPath = join(repoPath, '.license-guard.json');
|
|
567
597
|
if (existsSync(configPath)) {
|
package/package.json
CHANGED