@wipcomputer/wip-release 1.9.27 → 1.9.30

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 (3) hide show
  1. package/cli.js +6 -6
  2. package/core.mjs +96 -15
  3. package/package.json +1 -1
package/cli.js CHANGED
@@ -119,12 +119,12 @@ Flags:
119
119
  --skip-stale-check Skip stale remote branch check
120
120
  --skip-worktree-check Skip worktree guard (allow release from worktree)
121
121
 
122
- Release notes (highest priority wins, files ALWAYS beat --notes flag):
123
- 1. --notes-file=path Explicit file path (always wins)
124
- 2. RELEASE-NOTES-v{ver}.md In repo root (wins over --notes)
125
- 3. ai/dev-updates/YYYY-MM-DD* Today's dev update (wins over --notes if longer)
126
- 4. --notes="text" Fallback only (use for repos without release notes files)
127
- Written notes on disk always take priority over a CLI one-liner.
122
+ Release notes (REQUIRED, must be a file on disk):
123
+ 1. --notes-file=path Explicit file path
124
+ 2. RELEASE-NOTES-v{ver}.md In repo root (auto-detected)
125
+ 3. ai/dev-updates/YYYY-MM-DD* Today's dev update (auto-detected)
126
+ The --notes flag is NOT accepted. Write a file. Commit it on your branch.
127
+ The file shows up in the PR diff so it can be reviewed before merge.
128
128
 
129
129
  Skill publish to website:
130
130
  Add .publish-skill.json to repo root: { "name": "my-tool" }
package/core.mjs CHANGED
@@ -227,31 +227,96 @@ function checkReleaseNotes(notes, notesSource, level) {
227
227
  const issues = [];
228
228
 
229
229
  if (!notes) {
230
- issues.push('No release notes provided. Write a RELEASE-NOTES-v{version}.md or ai/dev-updates/ file.');
230
+ issues.push('No release notes found. A file is REQUIRED.');
231
+ issues.push('Write RELEASE-NOTES-v{version}.md or ai/dev-updates/YYYY-MM-DD--description.md');
232
+ issues.push('Commit it on your branch so it is reviewable in the PR.');
231
233
  return { ok: false, issues, block: true };
232
234
  }
233
235
 
234
- // Notes too short. All levels blocked.
235
- if (notes.length < 50) {
236
- issues.push('Release notes are too short (under 50 chars). Explain what changed and why.');
237
- issues.push('Write a RELEASE-NOTES-v{version}.md or ai/dev-updates/ file.');
236
+ // HARD RULE: release notes must come from a file on disk.
237
+ // --notes flag is NOT accepted. Write a file. Commit it. Review it.
238
+ if (notesSource === 'flag') {
239
+ issues.push('Release notes must come from a file, not the --notes flag.');
240
+ issues.push('Write RELEASE-NOTES-v{version}.md or ai/dev-updates/YYYY-MM-DD--description.md');
241
+ issues.push('Commit it on your branch so it is reviewable in the PR before merge.');
242
+ return { ok: false, issues, block: true };
238
243
  }
239
244
 
240
- // Bare --notes flag for minor/major is never acceptable.
241
- if (notesSource === 'flag' && (level === 'minor' || level === 'major')) {
242
- issues.push('Minor/major releases require a file, not --notes flag.');
243
- issues.push('Write RELEASE-NOTES-v{version}.md (dashes not dots) and commit it.');
245
+ // Notes too short.
246
+ if (notes.length < 50) {
247
+ issues.push('Release notes are too short (under 50 chars). Explain what changed and why.');
244
248
  }
245
249
 
246
- // Check for changelog-style one-liners regardless of source
250
+ // Check for changelog-style one-liners
247
251
  const looksLikeChangelog = /^(fix|add|update|remove|bump|chore|refactor|docs?)[\s:]/i.test(notes);
248
252
  if (looksLikeChangelog && notes.length < 100) {
249
253
  issues.push('Notes look like a changelog entry, not a narrative. Explain the impact.');
250
254
  }
251
255
 
256
+ // Release notes should reference at least one issue
257
+ const hasIssueRef = /#\d+/.test(notes);
258
+ if (!hasIssueRef) {
259
+ issues.push('No issue reference found (#XX). Every release should close or reference an issue.');
260
+ }
261
+
252
262
  return { ok: issues.length === 0, issues, block: issues.length > 0 };
253
263
  }
254
264
 
265
+ /**
266
+ * Scaffold a RELEASE-NOTES-v{version}.md template if one doesn't exist.
267
+ * Called when the release notes gate blocks. Gives the agent a file to fill in.
268
+ */
269
+ export function scaffoldReleaseNotes(repoPath, version) {
270
+ const dashed = version.replace(/\./g, '-');
271
+ const notesPath = join(repoPath, `RELEASE-NOTES-v${dashed}.md`);
272
+ if (existsSync(notesPath)) return notesPath;
273
+
274
+ const pkg = JSON.parse(readFileSync(join(repoPath, 'package.json'), 'utf8'));
275
+ const name = pkg.name?.replace(/^@[^/]+\//, '') || basename(repoPath);
276
+
277
+ // Auto-detect issue references from commits since last tag
278
+ let issueRefs = '';
279
+ try {
280
+ const lastTag = execFileSync('git', ['describe', '--tags', '--abbrev=0'],
281
+ { cwd: repoPath, encoding: 'utf8' }).trim();
282
+ const log = execFileSync('git', ['log', `${lastTag}..HEAD`, '--oneline'],
283
+ { cwd: repoPath, encoding: 'utf8' });
284
+ const issues = [...new Set(log.match(/#\d+/g) || [])];
285
+ if (issues.length > 0) {
286
+ issueRefs = issues.map(i => `- ${i}`).join('\n');
287
+ }
288
+ } catch {}
289
+
290
+ const template = `# Release Notes: ${name} v${version}
291
+
292
+ **One-line summary of what this release does**
293
+
294
+ ## What changed
295
+
296
+ Describe the changes. Not a commit list. Explain:
297
+ - What was built or fixed
298
+ - Why it matters
299
+ - What the user should know
300
+
301
+ ## Why
302
+
303
+ What problem does this solve? What was broken or missing?
304
+
305
+ ## Issues closed
306
+
307
+ ${issueRefs || '- #XX (replace with actual issue numbers)'}
308
+
309
+ ## How to verify
310
+
311
+ \`\`\`bash
312
+ # Commands to test the changes
313
+ \`\`\`
314
+ `;
315
+
316
+ writeFileSync(notesPath, template);
317
+ return notesPath;
318
+ }
319
+
255
320
  /**
256
321
  * Check if a file was modified in commits since the last git tag.
257
322
  */
@@ -457,6 +522,23 @@ export function createGitHubRelease(repoPath, newVersion, notes, currentVersion)
457
522
  console.warn(` ! GitHub release body is only ${bodyLen} chars. Notes may be truncated.`);
458
523
  }
459
524
  } catch {}
525
+
526
+ // Auto-close referenced issues
527
+ const issueNums = [...new Set((body.match(/#(\d+)/g) || []).map(m => m.slice(1)))];
528
+ for (const num of issueNums) {
529
+ try {
530
+ // Only close if issue exists and is open on the public repo
531
+ const publicSlug = repoSlug.replace(/-private$/, '');
532
+ execFileSync('gh', [
533
+ 'issue', 'close', num,
534
+ '--repo', publicSlug,
535
+ '--comment', `Closed by v${newVersion}. See release notes.`
536
+ ], { cwd: repoPath, stdio: 'pipe' });
537
+ console.log(` ✓ Closed #${num} on ${publicSlug}`);
538
+ } catch {
539
+ // Issue doesn't exist on public repo or already closed. Fine.
540
+ }
541
+ }
460
542
  } finally {
461
543
  try { execFileSync('rm', ['-f', tmpFile]); } catch {}
462
544
  }
@@ -783,11 +865,10 @@ export async function release({ repoPath, level, notes, notesSource, dryRun, noP
783
865
  console.log(` ✗ Release notes blocked:`);
784
866
  for (const issue of notesCheck.issues) console.log(` - ${issue}`);
785
867
  console.log('');
786
- console.log(' Release notes must explain what changed and why.');
787
- console.log(' Options:');
788
- console.log(' 1. Write RELEASE-NOTES-v{version}.md (dashes not dots) and commit it');
789
- console.log(' 2. Write ai/dev-updates/YYYY-MM-DD--description.md and commit it');
790
- console.log(' 3. Use --notes="at least 50 chars explaining the change and its impact"');
868
+ // Scaffold a template so the agent has something to fill in
869
+ const templatePath = scaffoldReleaseNotes(repoPath, newVersion);
870
+ console.log(` Scaffolded template: ${basename(templatePath)}`);
871
+ console.log(' Fill it in, commit, then run wip-release again.');
791
872
  console.log('');
792
873
  return { currentVersion, newVersion, dryRun: false, failed: true };
793
874
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-release",
3
- "version": "1.9.27",
3
+ "version": "1.9.30",
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",