@wipcomputer/wip-release 1.9.60 → 1.9.64
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/core.mjs +83 -17
- package/package.json +1 -1
package/core.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { execSync, execFileSync } from 'node:child_process';
|
|
9
9
|
import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, renameSync } from 'node:fs';
|
|
10
|
-
import { join, basename } from 'node:path';
|
|
10
|
+
import { join, basename, dirname } from 'node:path';
|
|
11
11
|
|
|
12
12
|
// ── Version ─────────────────────────────────────────────────────────
|
|
13
13
|
|
|
@@ -149,12 +149,37 @@ function trashReleaseNotes(repoPath) {
|
|
|
149
149
|
|
|
150
150
|
function gitCommitAndTag(repoPath, newVersion, notes) {
|
|
151
151
|
const msg = `v${newVersion}: ${notes || 'Release'}`;
|
|
152
|
-
// Stage
|
|
152
|
+
// Stage ALL files that wip-release modifies:
|
|
153
|
+
// - Root: package.json, CHANGELOG.md, SKILL.md
|
|
154
|
+
// - Sub-tools: tools/*/package.json
|
|
155
|
+
// - Product docs: ai/product/plans-prds/roadmap.md, ai/product/readme-first-product.md
|
|
156
|
+
// - Trashed release notes: _trash/RELEASE-NOTES-*.md
|
|
157
|
+
// Using git add -A on specific paths instead of listing each file (#231)
|
|
153
158
|
for (const f of ['package.json', 'CHANGELOG.md', 'SKILL.md']) {
|
|
154
159
|
if (existsSync(join(repoPath, f))) {
|
|
155
160
|
execFileSync('git', ['add', f], { cwd: repoPath, stdio: 'pipe' });
|
|
156
161
|
}
|
|
157
162
|
}
|
|
163
|
+
// Stage sub-tool package.json files
|
|
164
|
+
const toolsDir = join(repoPath, 'tools');
|
|
165
|
+
if (existsSync(toolsDir)) {
|
|
166
|
+
for (const sub of readdirSync(toolsDir, { withFileTypes: true })) {
|
|
167
|
+
if (!sub.isDirectory()) continue;
|
|
168
|
+
const subPkg = join('tools', sub.name, 'package.json');
|
|
169
|
+
if (existsSync(join(repoPath, subPkg))) {
|
|
170
|
+
execFileSync('git', ['add', subPkg], { cwd: repoPath, stdio: 'pipe' });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Stage product docs and trashed release notes
|
|
175
|
+
const aiProduct = join(repoPath, 'ai', 'product');
|
|
176
|
+
if (existsSync(aiProduct)) {
|
|
177
|
+
execFileSync('git', ['add', 'ai/product/'], { cwd: repoPath, stdio: 'pipe' });
|
|
178
|
+
}
|
|
179
|
+
const trash = join(repoPath, '_trash');
|
|
180
|
+
if (existsSync(trash)) {
|
|
181
|
+
execFileSync('git', ['add', '_trash/'], { cwd: repoPath, stdio: 'pipe' });
|
|
182
|
+
}
|
|
158
183
|
// Use execFileSync to avoid shell injection via notes.
|
|
159
184
|
// --no-verify: wip-release legitimately commits on main (version bump + changelog).
|
|
160
185
|
// The pre-commit hook blocks all commits on main, but wip-release is the one exception.
|
|
@@ -1235,6 +1260,44 @@ export async function release({ repoPath, level, notes, notesSource, dryRun, noP
|
|
|
1235
1260
|
}
|
|
1236
1261
|
}
|
|
1237
1262
|
|
|
1263
|
+
// 0.95. Run test scripts (if any exist)
|
|
1264
|
+
{
|
|
1265
|
+
const toolsDir = join(repoPath, 'tools');
|
|
1266
|
+
const testFiles = [];
|
|
1267
|
+
if (existsSync(toolsDir)) {
|
|
1268
|
+
for (const sub of readdirSync(toolsDir)) {
|
|
1269
|
+
const testPath = join(toolsDir, sub, 'test.sh');
|
|
1270
|
+
if (existsSync(testPath)) testFiles.push({ tool: sub, path: testPath });
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
// Also check repo root test.sh
|
|
1274
|
+
const rootTest = join(repoPath, 'test.sh');
|
|
1275
|
+
if (existsSync(rootTest)) testFiles.push({ tool: '(root)', path: rootTest });
|
|
1276
|
+
|
|
1277
|
+
if (testFiles.length > 0) {
|
|
1278
|
+
let allPassed = true;
|
|
1279
|
+
for (const { tool, path } of testFiles) {
|
|
1280
|
+
try {
|
|
1281
|
+
execFileSync('bash', [path], { cwd: dirname(path), stdio: 'pipe', timeout: 30000 });
|
|
1282
|
+
console.log(` ✓ Tests passed: ${tool}`);
|
|
1283
|
+
} catch (e) {
|
|
1284
|
+
allPassed = false;
|
|
1285
|
+
console.log(` ✗ Tests FAILED: ${tool}`);
|
|
1286
|
+
const output = (e.stdout || '').toString().trim();
|
|
1287
|
+
if (output) {
|
|
1288
|
+
for (const line of output.split('\n').slice(-5)) console.log(` ${line}`);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
if (!allPassed) {
|
|
1293
|
+
console.log('');
|
|
1294
|
+
console.log(' Fix failing tests before releasing.');
|
|
1295
|
+
console.log('');
|
|
1296
|
+
return { currentVersion, newVersion, dryRun: false, failed: true };
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1238
1301
|
if (dryRun) {
|
|
1239
1302
|
// Product docs check (dry-run)
|
|
1240
1303
|
if (!skipProductCheck) {
|
|
@@ -1506,31 +1569,33 @@ export async function release({ repoPath, level, notes, notesSource, dryRun, noP
|
|
|
1506
1569
|
.filter(b => b && b !== 'main' && b !== 'master' && !b.startsWith('*') && !b.includes('--merged-'));
|
|
1507
1570
|
|
|
1508
1571
|
if (merged.length > 0) {
|
|
1572
|
+
const current = execSync('git branch --show-current', { cwd: repoPath, encoding: 'utf8' }).trim();
|
|
1509
1573
|
console.log(` Scanning ${merged.length} merged branch(es) for rename...`);
|
|
1510
1574
|
for (const branch of merged) {
|
|
1511
|
-
const current = execSync('git branch --show-current', { cwd: repoPath, encoding: 'utf8' }).trim();
|
|
1512
1575
|
if (branch === current) continue;
|
|
1576
|
+
// Skip branches with characters that break git commands
|
|
1577
|
+
if (/[+\s~^:?*\[\]]/.test(branch)) continue;
|
|
1513
1578
|
|
|
1514
1579
|
let mergeDate;
|
|
1515
1580
|
try {
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
).trim().split('\n').pop().split(' ')[0];
|
|
1581
|
+
// Use execFileSync (array args) instead of execSync (shell string) to avoid injection
|
|
1582
|
+
const mergeBase = execFileSync('git', ['merge-base', 'main', branch], { cwd: repoPath, encoding: 'utf8' }).trim();
|
|
1583
|
+
const logOutput = execFileSync('git', ['log', 'main', '--format=%ai', '--ancestry-path', `${mergeBase}..main`], { cwd: repoPath, encoding: 'utf8' }).trim();
|
|
1584
|
+
if (logOutput) mergeDate = logOutput.split('\n').pop().split(' ')[0];
|
|
1521
1585
|
} catch {}
|
|
1522
1586
|
if (!mergeDate) {
|
|
1523
1587
|
try {
|
|
1524
|
-
mergeDate =
|
|
1588
|
+
mergeDate = execFileSync('git', ['log', branch, '-1', '--format=%ai'], { cwd: repoPath, encoding: 'utf8' }).trim().split(' ')[0];
|
|
1525
1589
|
} catch {}
|
|
1526
1590
|
}
|
|
1527
1591
|
if (!mergeDate) continue;
|
|
1528
1592
|
|
|
1529
1593
|
const newName = `${branch}--merged-${mergeDate}`;
|
|
1530
1594
|
try {
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1595
|
+
execFileSync('git', ['branch', '-m', branch, newName], { cwd: repoPath, stdio: 'pipe' });
|
|
1596
|
+
execFileSync('git', ['push', 'origin', newName], { cwd: repoPath, stdio: 'pipe' });
|
|
1597
|
+
// Remote branch may already be deleted by GitHub PR merge. That's fine.
|
|
1598
|
+
try { execFileSync('git', ['push', 'origin', '--delete', branch], { cwd: repoPath, stdio: 'pipe' }); } catch {}
|
|
1534
1599
|
console.log(` ✓ Renamed: ${branch} -> ${newName}`);
|
|
1535
1600
|
} catch (e) {
|
|
1536
1601
|
console.log(` ! Could not rename ${branch}: ${e.message}`);
|
|
@@ -1572,8 +1637,8 @@ export async function release({ repoPath, level, notes, notesSource, dryRun, noP
|
|
|
1572
1637
|
|
|
1573
1638
|
for (let i = KEEP_COUNT; i < branches.length; i++) {
|
|
1574
1639
|
try {
|
|
1575
|
-
|
|
1576
|
-
|
|
1640
|
+
execFileSync('git', ['push', 'origin', '--delete', branches[i]], { cwd: repoPath, stdio: 'pipe' });
|
|
1641
|
+
try { execFileSync('git', ['branch', '-d', branches[i]], { cwd: repoPath, stdio: 'pipe' }); } catch {}
|
|
1577
1642
|
pruned++;
|
|
1578
1643
|
} catch {}
|
|
1579
1644
|
}
|
|
@@ -1596,11 +1661,12 @@ export async function release({ repoPath, level, notes, notesSource, dryRun, noP
|
|
|
1596
1661
|
let staleCleaned = 0;
|
|
1597
1662
|
for (const branch of allRemote) {
|
|
1598
1663
|
if (branch === current) continue;
|
|
1664
|
+
if (/[+\s~^:?*\[\]]/.test(branch)) continue;
|
|
1599
1665
|
try {
|
|
1600
|
-
|
|
1666
|
+
execFileSync('git', ['merge-base', '--is-ancestor', `origin/${branch}`, 'origin/main'], { cwd: repoPath, stdio: 'pipe' });
|
|
1601
1667
|
// If we get here, branch is fully merged
|
|
1602
|
-
|
|
1603
|
-
|
|
1668
|
+
try { execFileSync('git', ['push', 'origin', '--delete', branch], { cwd: repoPath, stdio: 'pipe' }); } catch {}
|
|
1669
|
+
try { execFileSync('git', ['branch', '-d', branch], { cwd: repoPath, stdio: 'pipe' }); } catch {}
|
|
1604
1670
|
staleCleaned++;
|
|
1605
1671
|
} catch {}
|
|
1606
1672
|
}
|
package/package.json
CHANGED