@wipcomputer/wip-release 1.9.60 → 1.9.63

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.
Files changed (2) hide show
  1. package/core.mjs +82 -16
  2. package/package.json +1 -1
package/core.mjs CHANGED
@@ -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 known files (ignore missing ones)
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
- const mergeBase = execSync(`git merge-base main ${branch}`, { cwd: repoPath, encoding: 'utf8' }).trim();
1517
- mergeDate = execSync(
1518
- `git log main --format="%ai" --ancestry-path ${mergeBase}..main`,
1519
- { cwd: repoPath, encoding: 'utf8' }
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 = execSync(`git log ${branch} -1 --format="%ai"`, { cwd: repoPath, encoding: 'utf8' }).trim().split(' ')[0];
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
- execSync(`git branch -m "${branch}" "${newName}"`, { cwd: repoPath, stdio: 'pipe' });
1532
- execSync(`git push origin "${newName}"`, { cwd: repoPath, stdio: 'pipe' });
1533
- execSync(`git push origin --delete "${branch}"`, { cwd: repoPath, stdio: 'pipe' });
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
- execSync(`git push origin --delete "${branches[i]}"`, { cwd: repoPath, stdio: 'pipe' });
1576
- execSync(`git branch -d "${branches[i]}" 2>/dev/null || true`, { cwd: repoPath, stdio: 'pipe', shell: true });
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
- execSync(`git merge-base --is-ancestor origin/${branch} origin/main`, { cwd: repoPath, stdio: 'pipe' });
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
- execSync(`git push origin --delete "${branch}"`, { cwd: repoPath, stdio: 'pipe' });
1603
- execSync(`git branch -d "${branch}" 2>/dev/null || true`, { cwd: repoPath, stdio: 'pipe', shell: true });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-release",
3
- "version": "1.9.60",
3
+ "version": "1.9.63",
4
4
  "type": "module",
5
5
  "description": "One-command release pipeline. Bumps version, updates changelog + SKILL.md, publishes to npm + GitHub.",
6
6
  "main": "core.mjs",