glab-setup-git-identity 0.6.0

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 (66) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +11 -0
  3. package/.github/workflows/release.yml +372 -0
  4. package/.husky/pre-commit +1 -0
  5. package/.jscpd.json +20 -0
  6. package/.prettierignore +7 -0
  7. package/.prettierrc +10 -0
  8. package/CHANGELOG.md +143 -0
  9. package/LICENSE +24 -0
  10. package/README.md +455 -0
  11. package/bunfig.toml +3 -0
  12. package/deno.json +7 -0
  13. package/docs/case-studies/issue-13/README.md +195 -0
  14. package/docs/case-studies/issue-13/hive-mind-issue-960.json +23 -0
  15. package/docs/case-studies/issue-13/hive-mind-pr-961-diff.txt +773 -0
  16. package/docs/case-studies/issue-13/hive-mind-pr-961.json +126 -0
  17. package/docs/case-studies/issue-21/README.md +384 -0
  18. package/docs/case-studies/issue-21/ci-logs/run-20803315337.txt +1188 -0
  19. package/docs/case-studies/issue-21/ci-logs/run-20885464993.txt +1310 -0
  20. package/docs/case-studies/issue-21/issue-111-data.txt +15 -0
  21. package/docs/case-studies/issue-21/issue-113-data.txt +15 -0
  22. package/docs/case-studies/issue-21/pr-112-data.json +109 -0
  23. package/docs/case-studies/issue-21/pr-112-diff.patch +1336 -0
  24. package/docs/case-studies/issue-21/pr-114-data.json +126 -0
  25. package/docs/case-studies/issue-21/pr-114-diff.patch +879 -0
  26. package/docs/case-studies/issue-3/README.md +338 -0
  27. package/docs/case-studies/issue-3/created-issues.md +32 -0
  28. package/docs/case-studies/issue-3/issue-data.json +29 -0
  29. package/docs/case-studies/issue-3/original-format-release-notes.mjs +212 -0
  30. package/docs/case-studies/issue-3/reference-pr-59-diff.txt +614 -0
  31. package/docs/case-studies/issue-3/reference-pr-59.json +109 -0
  32. package/docs/case-studies/issue-3/release-v0.1.0.json +9 -0
  33. package/docs/case-studies/issue-3/repositories-with-same-script.json +22 -0
  34. package/docs/case-studies/issue-3/research-notes.md +33 -0
  35. package/docs/case-studies/issue-7/BEST-PRACTICES-COMPARISON.md +334 -0
  36. package/docs/case-studies/issue-7/FORMATTER-COMPARISON.md +649 -0
  37. package/docs/case-studies/issue-7/current-repository-analysis.json +70 -0
  38. package/docs/case-studies/issue-7/effect-template-analysis.json +178 -0
  39. package/eslint.config.js +91 -0
  40. package/examples/basic-usage.js +64 -0
  41. package/experiments/test-changeset-scripts.mjs +303 -0
  42. package/experiments/test-failure-detection.mjs +143 -0
  43. package/experiments/test-format-major-changes.mjs +49 -0
  44. package/experiments/test-format-minor-changes.mjs +52 -0
  45. package/experiments/test-format-no-hash.mjs +43 -0
  46. package/experiments/test-format-patch-changes.mjs +46 -0
  47. package/package.json +80 -0
  48. package/scripts/changeset-version.mjs +75 -0
  49. package/scripts/check-changesets.mjs +67 -0
  50. package/scripts/check-version.mjs +129 -0
  51. package/scripts/create-github-release.mjs +93 -0
  52. package/scripts/create-manual-changeset.mjs +89 -0
  53. package/scripts/detect-code-changes.mjs +194 -0
  54. package/scripts/format-github-release.mjs +83 -0
  55. package/scripts/format-release-notes.mjs +219 -0
  56. package/scripts/instant-version-bump.mjs +172 -0
  57. package/scripts/js-paths.mjs +177 -0
  58. package/scripts/merge-changesets.mjs +263 -0
  59. package/scripts/publish-to-npm.mjs +302 -0
  60. package/scripts/setup-npm.mjs +37 -0
  61. package/scripts/validate-changeset.mjs +265 -0
  62. package/scripts/version-and-commit.mjs +284 -0
  63. package/src/cli.js +386 -0
  64. package/src/index.d.ts +255 -0
  65. package/src/index.js +563 -0
  66. package/tests/index.test.js +137 -0
@@ -0,0 +1,773 @@
1
+ diff --git a/.changeset/robust-changeset-cicd.md b/.changeset/robust-changeset-cicd.md
2
+ new file mode 100644
3
+ index 00000000..aa632642
4
+ --- /dev/null
5
+ +++ b/.changeset/robust-changeset-cicd.md
6
+ @@ -0,0 +1,11 @@
7
+ +---
8
+ +'@link-assistant/hive-mind': minor
9
+ +---
10
+ +
11
+ +Improve changeset CI/CD robustness for multiple concurrent PRs
12
+ +
13
+ +- Update validate-changeset.mjs to only check changesets ADDED by the current PR (not pre-existing ones)
14
+ +- Add merge-changesets.mjs script to combine multiple pending changesets during release
15
+ +- Merged changesets use highest version bump type (major > minor > patch) and combine descriptions chronologically
16
+ +- Update release workflow to merge multiple changesets before version bump
17
+ +- This prevents PR failures when multiple PRs merge before a release cycle completes
18
+ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
19
+ index 6a810320..148c34bc 100644
20
+ --- a/.github/workflows/release.yml
21
+ +++ b/.github/workflows/release.yml
22
+ @@ -129,6 +129,11 @@ jobs:
23
+ run: npm install
24
+
25
+ - name: Check for changesets
26
+ + env:
27
+ + # Pass PR context to the validation script
28
+ + GITHUB_BASE_REF: ${{ github.base_ref }}
29
+ + GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }}
30
+ + GITHUB_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
31
+ run: |
32
+ # Skip changeset check for automated version PRs
33
+ if [[ "${{ github.head_ref }}" == "changeset-release/"* ]]; then
34
+ @@ -137,6 +142,8 @@ jobs:
35
+ fi
36
+
37
+ # Run changeset validation script
38
+ + # This validates that exactly ONE changeset was ADDED by this PR
39
+ + # Pre-existing changesets from other merged PRs are ignored
40
+ node scripts/validate-changeset.mjs
41
+
42
+ # === FAST CHECKS (FAIL FAST) ===
43
+ @@ -1521,6 +1528,13 @@ jobs:
44
+ CHANGESET_COUNT=$(find .changeset -name "*.md" ! -name "README.md" | wc -l)
45
+ echo "Found $CHANGESET_COUNT changeset file(s)"
46
+ echo "has_changesets=$([[ $CHANGESET_COUNT -gt 0 ]] && echo 'true' || echo 'false')" >> $GITHUB_OUTPUT
47
+ + echo "changeset_count=$CHANGESET_COUNT" >> $GITHUB_OUTPUT
48
+ +
49
+ + - name: Merge multiple changesets
50
+ + if: steps.check_changesets.outputs.has_changesets == 'true' && steps.check_changesets.outputs.changeset_count > 1
51
+ + run: |
52
+ + echo "Multiple changesets detected, merging..."
53
+ + node scripts/merge-changesets.mjs
54
+
55
+ - name: Version packages and commit to main
56
+ if: steps.check_changesets.outputs.has_changesets == 'true'
57
+ diff --git a/experiments/test-changeset-scripts.mjs b/experiments/test-changeset-scripts.mjs
58
+ new file mode 100644
59
+ index 00000000..b8ea38b1
60
+ --- /dev/null
61
+ +++ b/experiments/test-changeset-scripts.mjs
62
+ @@ -0,0 +1,257 @@
63
+ +#!/usr/bin/env node
64
+ +
65
+ +/**
66
+ + * Test suite for changeset-related scripts
67
+ + * Tests validate-changeset.mjs and merge-changesets.mjs functionality
68
+ + */
69
+ +
70
+ +import { execSync } from 'child_process';
71
+ +import { fileURLToPath } from 'url';
72
+ +import { dirname, join } from 'path';
73
+ +import { mkdirSync, writeFileSync, rmSync, existsSync, readdirSync } from 'fs';
74
+ +
75
+ +const __filename = fileURLToPath(import.meta.url);
76
+ +const __dirname = dirname(__filename);
77
+ +const projectRoot = join(__dirname, '..');
78
+ +const validateChangesetPath = join(projectRoot, 'scripts', 'validate-changeset.mjs');
79
+ +const mergeChangesetsPath = join(projectRoot, 'scripts', 'merge-changesets.mjs');
80
+ +
81
+ +let testsPassed = 0;
82
+ +let testsFailed = 0;
83
+ +
84
+ +function runTest(name, testFn) {
85
+ + process.stdout.write(`Testing ${name}... `);
86
+ + try {
87
+ + testFn();
88
+ + console.log('✅ PASSED');
89
+ + testsPassed++;
90
+ + } catch (error) {
91
+ + console.log(`❌ FAILED: ${error.message}`);
92
+ + testsFailed++;
93
+ + }
94
+ +}
95
+ +
96
+ +function execCommand(command, options = {}) {
97
+ + try {
98
+ + return {
99
+ + output: execSync(command, { encoding: 'utf8', stdio: 'pipe', cwd: projectRoot, ...options }),
100
+ + exitCode: 0,
101
+ + };
102
+ + } catch (error) {
103
+ + return {
104
+ + output: (error.stdout || '') + (error.stderr || ''),
105
+ + exitCode: error.status || 1,
106
+ + };
107
+ + }
108
+ +}
109
+ +
110
+ +// ==========================================
111
+ +// Tests for validate-changeset.mjs
112
+ +// ==========================================
113
+ +
114
+ +// Test 1: Script exists and is executable
115
+ +runTest('validate-changeset.mjs exists', () => {
116
+ + if (!existsSync(validateChangesetPath)) {
117
+ + throw new Error('validate-changeset.mjs not found');
118
+ + }
119
+ +});
120
+ +
121
+ +// Test 2: Syntax check
122
+ +runTest('validate-changeset.mjs syntax check', () => {
123
+ + const { output, exitCode } = execCommand(`node --check ${validateChangesetPath}`);
124
+ + if (exitCode !== 0) {
125
+ + throw new Error(`Syntax error: ${output}`);
126
+ + }
127
+ +});
128
+ +
129
+ +// Test 3: Script runs without crashing (fallback mode)
130
+ +runTest('validate-changeset.mjs runs in fallback mode', () => {
131
+ + // Without git diff context, it falls back to checking all changesets
132
+ + const { output, exitCode } = execCommand(`node ${validateChangesetPath}`);
133
+ + // Should either pass (if there's exactly one changeset) or fail (if not)
134
+ + // But should not crash with an exception
135
+ + if (output.includes('Error during changeset validation') && output.includes('Cannot read')) {
136
+ + throw new Error('Script crashed unexpectedly');
137
+ + }
138
+ +});
139
+ +
140
+ +// ==========================================
141
+ +// Tests for merge-changesets.mjs
142
+ +// ==========================================
143
+ +
144
+ +// Test 4: Script exists
145
+ +runTest('merge-changesets.mjs exists', () => {
146
+ + if (!existsSync(mergeChangesetsPath)) {
147
+ + throw new Error('merge-changesets.mjs not found');
148
+ + }
149
+ +});
150
+ +
151
+ +// Test 5: Syntax check
152
+ +runTest('merge-changesets.mjs syntax check', () => {
153
+ + const { output, exitCode } = execCommand(`node --check ${mergeChangesetsPath}`);
154
+ + if (exitCode !== 0) {
155
+ + throw new Error(`Syntax error: ${output}`);
156
+ + }
157
+ +});
158
+ +
159
+ +// ==========================================
160
+ +// Unit tests using mock changeset directories
161
+ +// ==========================================
162
+ +
163
+ +const testDir = join(projectRoot, 'experiments', 'test-changesets-temp');
164
+ +const testChangesetDir = join(testDir, '.changeset');
165
+ +
166
+ +function setupTestEnvironment() {
167
+ + // Clean up if exists
168
+ + if (existsSync(testDir)) {
169
+ + rmSync(testDir, { recursive: true });
170
+ + }
171
+ + mkdirSync(testChangesetDir, { recursive: true });
172
+ +
173
+ + // Create README.md (should be ignored)
174
+ + writeFileSync(join(testChangesetDir, 'README.md'), '# Changesets\n');
175
+ +
176
+ + // Create config.json (should be ignored)
177
+ + writeFileSync(join(testChangesetDir, 'config.json'), '{}');
178
+ +}
179
+ +
180
+ +function cleanupTestEnvironment() {
181
+ + if (existsSync(testDir)) {
182
+ + rmSync(testDir, { recursive: true });
183
+ + }
184
+ +}
185
+ +
186
+ +function createChangeset(filename, type, description) {
187
+ + const content = `---
188
+ +'@link-assistant/hive-mind': ${type}
189
+ +---
190
+ +
191
+ +${description}
192
+ +`;
193
+ + writeFileSync(join(testChangesetDir, filename), content);
194
+ +}
195
+ +
196
+ +// Test 6: Merge changesets correctly combines multiple changesets
197
+ +runTest('merge-changesets.mjs combines multiple changesets', () => {
198
+ + setupTestEnvironment();
199
+ + try {
200
+ + // Create two changesets
201
+ + createChangeset('first-change.md', 'patch', 'First change description');
202
+ +
203
+ + // Wait a bit to ensure different mtime
204
+ + execSync('sleep 0.1');
205
+ +
206
+ + createChangeset('second-change.md', 'minor', 'Second change description');
207
+ +
208
+ + // Run merge script in test directory
209
+ + const { output, exitCode } = execCommand(`node ${mergeChangesetsPath}`, { cwd: testDir });
210
+ +
211
+ + if (exitCode !== 0) {
212
+ + throw new Error(`Merge failed: ${output}`);
213
+ + }
214
+ +
215
+ + // Check that merged changeset was created
216
+ + const files = readdirSync(testChangesetDir).filter(f => f.endsWith('.md') && f !== 'README.md');
217
+ + if (files.length !== 1) {
218
+ + throw new Error(`Expected 1 merged changeset, found ${files.length}`);
219
+ + }
220
+ +
221
+ + // Check that it uses the higher bump type (minor)
222
+ + if (!output.includes('Using highest: minor')) {
223
+ + throw new Error('Expected merged changeset to use minor bump type');
224
+ + }
225
+ +
226
+ + // Check that both descriptions are included
227
+ + if (!output.includes('First change description') || !output.includes('Second change description')) {
228
+ + throw new Error('Merged changeset should contain both descriptions');
229
+ + }
230
+ + } finally {
231
+ + cleanupTestEnvironment();
232
+ + }
233
+ +});
234
+ +
235
+ +// Test 7: Merge changesets uses major if any is major
236
+ +runTest('merge-changesets.mjs uses highest bump type (major)', () => {
237
+ + setupTestEnvironment();
238
+ + try {
239
+ + createChangeset('patch-change.md', 'patch', 'Patch change');
240
+ + createChangeset('major-change.md', 'major', 'Major change');
241
+ + createChangeset('minor-change.md', 'minor', 'Minor change');
242
+ +
243
+ + const { output, exitCode } = execCommand(`node ${mergeChangesetsPath}`, { cwd: testDir });
244
+ +
245
+ + if (exitCode !== 0) {
246
+ + throw new Error(`Merge failed: ${output}`);
247
+ + }
248
+ +
249
+ + if (!output.includes('Using highest: major')) {
250
+ + throw new Error('Expected merged changeset to use major bump type');
251
+ + }
252
+ + } finally {
253
+ + cleanupTestEnvironment();
254
+ + }
255
+ +});
256
+ +
257
+ +// Test 8: Merge does nothing with single changeset
258
+ +runTest('merge-changesets.mjs skips with single changeset', () => {
259
+ + setupTestEnvironment();
260
+ + try {
261
+ + createChangeset('only-change.md', 'patch', 'Only change');
262
+ +
263
+ + const { output, exitCode } = execCommand(`node ${mergeChangesetsPath}`, { cwd: testDir });
264
+ +
265
+ + if (exitCode !== 0) {
266
+ + throw new Error(`Script failed: ${output}`);
267
+ + }
268
+ +
269
+ + if (!output.includes('No merging needed')) {
270
+ + throw new Error('Expected script to skip merging with single changeset');
271
+ + }
272
+ +
273
+ + // Verify original changeset still exists
274
+ + const files = readdirSync(testChangesetDir).filter(f => f.endsWith('.md') && f !== 'README.md');
275
+ + if (files.length !== 1 || files[0] !== 'only-change.md') {
276
+ + throw new Error('Original changeset should not be modified');
277
+ + }
278
+ + } finally {
279
+ + cleanupTestEnvironment();
280
+ + }
281
+ +});
282
+ +
283
+ +// Test 9: Merge does nothing with no changesets
284
+ +runTest('merge-changesets.mjs skips with no changesets', () => {
285
+ + setupTestEnvironment();
286
+ + try {
287
+ + const { output, exitCode } = execCommand(`node ${mergeChangesetsPath}`, { cwd: testDir });
288
+ +
289
+ + if (exitCode !== 0) {
290
+ + throw new Error(`Script failed: ${output}`);
291
+ + }
292
+ +
293
+ + if (!output.includes('No merging needed')) {
294
+ + throw new Error('Expected script to skip merging with no changesets');
295
+ + }
296
+ + } finally {
297
+ + cleanupTestEnvironment();
298
+ + }
299
+ +});
300
+ +
301
+ +// Test 10: Validate changeset format checking
302
+ +runTest('validate-changeset.mjs format validation', () => {
303
+ + // This test verifies the script can be imported and has expected functions
304
+ + // Actual validation is tested through integration
305
+ + const { output, exitCode } = execCommand(`node --check ${validateChangesetPath}`);
306
+ + if (exitCode !== 0) {
307
+ + throw new Error('Script has syntax errors');
308
+ + }
309
+ +});
310
+ +
311
+ +// Summary
312
+ +console.log('\n' + '='.repeat(50));
313
+ +console.log(`Test Results for changeset scripts:`);
314
+ +console.log(` ✅ Passed: ${testsPassed}`);
315
+ +console.log(` ❌ Failed: ${testsFailed}`);
316
+ +console.log('='.repeat(50));
317
+ +
318
+ +// Exit with appropriate code
319
+ +process.exit(testsFailed > 0 ? 1 : 0);
320
+ diff --git a/scripts/merge-changesets.mjs b/scripts/merge-changesets.mjs
321
+ new file mode 100644
322
+ index 00000000..a71bd5b2
323
+ --- /dev/null
324
+ +++ b/scripts/merge-changesets.mjs
325
+ @@ -0,0 +1,184 @@
326
+ +#!/usr/bin/env node
327
+ +
328
+ +/**
329
+ + * Merge multiple changeset files into a single changeset
330
+ + *
331
+ + * Key behavior:
332
+ + * - Combines all pending changesets into a single changeset file
333
+ + * - Uses the highest version bump type (major > minor > patch)
334
+ + * - Preserves all descriptions in chronological order (by file modification time)
335
+ + * - Removes the individual changeset files after merging
336
+ + * - Does nothing if there's only one or no changesets
337
+ + *
338
+ + * This script is run before `changeset version` to ensure a clean release
339
+ + * even when multiple PRs have merged before a release cycle.
340
+ + */
341
+ +
342
+ +import { readdirSync, readFileSync, writeFileSync, unlinkSync, statSync } from 'fs';
343
+ +import { join } from 'path';
344
+ +
345
+ +const PACKAGE_NAME = '@link-assistant/hive-mind';
346
+ +const CHANGESET_DIR = '.changeset';
347
+ +
348
+ +// Version bump type priority (higher number = higher priority)
349
+ +const BUMP_PRIORITY = {
350
+ + patch: 1,
351
+ + minor: 2,
352
+ + major: 3,
353
+ +};
354
+ +
355
+ +/**
356
+ + * Generate a random changeset file name (similar to what @changesets/cli does)
357
+ + * @returns {string}
358
+ + */
359
+ +function generateChangesetName() {
360
+ + const adjectives = ['bright', 'calm', 'cool', 'cyan', 'dark', 'fast', 'gold', 'good', 'green', 'happy', 'kind', 'loud', 'neat', 'nice', 'pink', 'proud', 'quick', 'red', 'rich', 'safe', 'shy', 'soft', 'sweet', 'tall', 'warm', 'wise', 'young'];
361
+ + const nouns = ['apple', 'bird', 'book', 'car', 'cat', 'cloud', 'desk', 'dog', 'door', 'fish', 'flower', 'frog', 'grass', 'house', 'key', 'lake', 'leaf', 'moon', 'mouse', 'owl', 'park', 'rain', 'river', 'rock', 'sea', 'star', 'sun', 'tree', 'wave', 'wind'];
362
+ +
363
+ + const randomAdjective = adjectives[Math.floor(Math.random() * adjectives.length)];
364
+ + const randomNoun = nouns[Math.floor(Math.random() * nouns.length)];
365
+ +
366
+ + return `${randomAdjective}-${randomNoun}`;
367
+ +}
368
+ +
369
+ +/**
370
+ + * Parse a changeset file and extract its metadata
371
+ + * @param {string} filePath
372
+ + * @returns {{type: string, description: string, mtime: Date} | null}
373
+ + */
374
+ +function parseChangeset(filePath) {
375
+ + try {
376
+ + const content = readFileSync(filePath, 'utf-8');
377
+ + const stats = statSync(filePath);
378
+ +
379
+ + // Extract version type
380
+ + const versionTypeRegex = new RegExp(`^['"]${PACKAGE_NAME.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}['"]:\\s+(major|minor|patch)`, 'm');
381
+ + const versionTypeMatch = content.match(versionTypeRegex);
382
+ +
383
+ + if (!versionTypeMatch) {
384
+ + console.warn(`Warning: Could not parse version type from ${filePath}, skipping`);
385
+ + return null;
386
+ + }
387
+ +
388
+ + // Extract description
389
+ + const parts = content.split('---');
390
+ + const description = parts.length >= 3 ? parts.slice(2).join('---').trim() : '';
391
+ +
392
+ + return {
393
+ + type: versionTypeMatch[1],
394
+ + description,
395
+ + mtime: stats.mtime,
396
+ + };
397
+ + } catch (error) {
398
+ + console.warn(`Warning: Failed to parse ${filePath}: ${error.message}`);
399
+ + return null;
400
+ + }
401
+ +}
402
+ +
403
+ +/**
404
+ + * Get the highest priority bump type
405
+ + * @param {string[]} types
406
+ + * @returns {string}
407
+ + */
408
+ +function getHighestBumpType(types) {
409
+ + let highest = 'patch';
410
+ + for (const type of types) {
411
+ + if (BUMP_PRIORITY[type] > BUMP_PRIORITY[highest]) {
412
+ + highest = type;
413
+ + }
414
+ + }
415
+ + return highest;
416
+ +}
417
+ +
418
+ +/**
419
+ + * Create a merged changeset file
420
+ + * @param {string} type
421
+ + * @param {string[]} descriptions
422
+ + * @returns {string}
423
+ + */
424
+ +function createMergedChangeset(type, descriptions) {
425
+ + const combinedDescription = descriptions.join('\n\n');
426
+ +
427
+ + return `---
428
+ +'${PACKAGE_NAME}': ${type}
429
+ +---
430
+ +
431
+ +${combinedDescription}
432
+ +`;
433
+ +}
434
+ +
435
+ +function main() {
436
+ + console.log('Checking for multiple changesets to merge...');
437
+ +
438
+ + // Get all changeset files
439
+ + const changesetFiles = readdirSync(CHANGESET_DIR).filter(file => file.endsWith('.md') && file !== 'README.md');
440
+ +
441
+ + console.log(`Found ${changesetFiles.length} changeset file(s)`);
442
+ +
443
+ + // If 0 or 1 changesets, nothing to merge
444
+ + if (changesetFiles.length <= 1) {
445
+ + console.log('No merging needed (0 or 1 changeset found)');
446
+ + return;
447
+ + }
448
+ +
449
+ + console.log('Multiple changesets found, merging...');
450
+ + changesetFiles.forEach(file => console.log(` - ${file}`));
451
+ +
452
+ + // Parse all changesets
453
+ + const parsedChangesets = [];
454
+ + for (const file of changesetFiles) {
455
+ + const filePath = join(CHANGESET_DIR, file);
456
+ + const parsed = parseChangeset(filePath);
457
+ + if (parsed) {
458
+ + parsedChangesets.push({
459
+ + file,
460
+ + filePath,
461
+ + ...parsed,
462
+ + });
463
+ + }
464
+ + }
465
+ +
466
+ + if (parsedChangesets.length === 0) {
467
+ + console.error('Error: No valid changesets could be parsed');
468
+ + process.exit(1);
469
+ + }
470
+ +
471
+ + // Sort by modification time (oldest first) to preserve chronological order
472
+ + parsedChangesets.sort((a, b) => a.mtime.getTime() - b.mtime.getTime());
473
+ +
474
+ + // Determine the highest bump type
475
+ + const bumpTypes = parsedChangesets.map(c => c.type);
476
+ + const highestBumpType = getHighestBumpType(bumpTypes);
477
+ +
478
+ + console.log(`\nMerge summary:`);
479
+ + console.log(` Bump types found: ${[...new Set(bumpTypes)].join(', ')}`);
480
+ + console.log(` Using highest: ${highestBumpType}`);
481
+ +
482
+ + // Collect descriptions in chronological order
483
+ + const descriptions = parsedChangesets.filter(c => c.description).map(c => c.description);
484
+ +
485
+ + console.log(` Descriptions to merge: ${descriptions.length}`);
486
+ +
487
+ + // Create merged changeset content
488
+ + const mergedContent = createMergedChangeset(highestBumpType, descriptions);
489
+ +
490
+ + // Generate a unique name for the merged changeset
491
+ + const mergedFileName = `merged-${generateChangesetName()}.md`;
492
+ + const mergedFilePath = join(CHANGESET_DIR, mergedFileName);
493
+ +
494
+ + // Write the merged changeset
495
+ + writeFileSync(mergedFilePath, mergedContent);
496
+ + console.log(`\nCreated merged changeset: ${mergedFileName}`);
497
+ +
498
+ + // Remove the original changeset files
499
+ + console.log('\nRemoving original changeset files:');
500
+ + for (const changeset of parsedChangesets) {
501
+ + unlinkSync(changeset.filePath);
502
+ + console.log(` Removed: ${changeset.file}`);
503
+ + }
504
+ +
505
+ + console.log('\nChangeset merge completed successfully');
506
+ + console.log(`\nMerged changeset content:\n${mergedContent}`);
507
+ +}
508
+ +
509
+ +main();
510
+ diff --git a/scripts/validate-changeset.mjs b/scripts/validate-changeset.mjs
511
+ index bb5e8d59..0240a3e3 100644
512
+ --- a/scripts/validate-changeset.mjs
513
+ +++ b/scripts/validate-changeset.mjs
514
+ @@ -1,81 +1,206 @@
515
+ #!/usr/bin/env node
516
+
517
+ /**
518
+ - * Validate changeset for CI - ensures exactly one valid changeset exists
519
+ + * Validate changeset for CI - ensures exactly one valid changeset is added by the PR
520
+ + *
521
+ + * Key behavior:
522
+ + * - Only checks changeset files ADDED by the current PR (not pre-existing ones)
523
+ + * - Uses git diff to compare PR head against base branch
524
+ + * - Validates that the PR adds exactly one changeset with proper format
525
+ */
526
+
527
+ -import { readdirSync, readFileSync } from 'fs';
528
+ +import { execSync } from 'child_process';
529
+ +import { readFileSync, readdirSync, existsSync } from 'fs';
530
+ import { join } from 'path';
531
+
532
+ const PACKAGE_NAME = '@link-assistant/hive-mind';
533
+
534
+ -try {
535
+ - // Count changeset files (excluding README.md and config.json)
536
+ +/**
537
+ + * Get changeset files added in the current PR using git diff
538
+ + * @returns {string[]} Array of added changeset file names
539
+ + */
540
+ +function getAddedChangesetFiles() {
541
+ const changesetDir = '.changeset';
542
+ - const changesetFiles = readdirSync(changesetDir).filter(file => file.endsWith('.md') && file !== 'README.md');
543
+
544
+ - const changesetCount = changesetFiles.length;
545
+ - console.log(`Found ${changesetCount} changeset file(s)`);
546
+ + // Try to get PR context from environment variables (GitHub Actions)
547
+ + const baseSha = process.env.GITHUB_BASE_SHA || process.env.BASE_SHA;
548
+ + const headSha = process.env.GITHUB_HEAD_SHA || process.env.HEAD_SHA;
549
+ +
550
+ + // If we have explicit SHAs, use them
551
+ + if (baseSha && headSha) {
552
+ + console.log(`Comparing ${baseSha}...${headSha}`);
553
+ + try {
554
+ + // Ensure we have the base commit
555
+ + try {
556
+ + execSync(`git cat-file -e ${baseSha}`, { stdio: 'ignore' });
557
+ + } catch {
558
+ + // Try to fetch the base commit if not available locally
559
+ + console.log('Base commit not available locally, attempting fetch...');
560
+ + try {
561
+ + execSync(`git fetch origin ${baseSha}`, { stdio: 'inherit' });
562
+ + } catch {
563
+ + // If that fails, try fetching all
564
+ + execSync(`git fetch origin`, { stdio: 'inherit' });
565
+ + }
566
+ + }
567
+ +
568
+ + const diffOutput = execSync(`git diff --name-status ${baseSha} ${headSha}`, {
569
+ + encoding: 'utf-8',
570
+ + });
571
+ +
572
+ + const addedChangesets = [];
573
+ + for (const line of diffOutput.trim().split('\n')) {
574
+ + if (!line) continue;
575
+ + const [status, filePath] = line.split('\t');
576
+ + // Only count files that were Added (A) in the changeset directory
577
+ + if (status === 'A' && filePath.startsWith(`${changesetDir}/`) && filePath.endsWith('.md') && !filePath.endsWith('README.md')) {
578
+ + addedChangesets.push(filePath.replace(`${changesetDir}/`, ''));
579
+ + }
580
+ + }
581
+ + return addedChangesets;
582
+ + } catch (error) {
583
+ + console.log(`Git diff with explicit SHAs failed: ${error.message}`);
584
+ + // Fall through to alternative detection
585
+ + }
586
+ + }
587
+ +
588
+ + // Try GitHub PR context (available in pull_request events)
589
+ + const prBase = process.env.GITHUB_BASE_REF;
590
+ + if (prBase) {
591
+ + console.log(`Comparing against base branch: ${prBase}`);
592
+ + try {
593
+ + // Fetch the base branch to ensure we have it
594
+ + try {
595
+ + execSync(`git fetch origin ${prBase}`, { stdio: 'inherit' });
596
+ + } catch {
597
+ + // Ignore fetch errors, we might already have it
598
+ + }
599
+ +
600
+ + const diffOutput = execSync(`git diff --name-status origin/${prBase}...HEAD`, {
601
+ + encoding: 'utf-8',
602
+ + });
603
+ +
604
+ + const addedChangesets = [];
605
+ + for (const line of diffOutput.trim().split('\n')) {
606
+ + if (!line) continue;
607
+ + const [status, filePath] = line.split('\t');
608
+ + if (status === 'A' && filePath.startsWith(`${changesetDir}/`) && filePath.endsWith('.md') && !filePath.endsWith('README.md')) {
609
+ + addedChangesets.push(filePath.replace(`${changesetDir}/`, ''));
610
+ + }
611
+ + }
612
+ + return addedChangesets;
613
+ + } catch (error) {
614
+ + console.log(`Git diff with base ref failed: ${error.message}`);
615
+ + }
616
+ + }
617
+ +
618
+ + // Fallback: If we can't determine the diff, check all changesets in directory
619
+ + // This maintains backward compatibility for local development
620
+ + console.log('Warning: Could not determine PR diff, checking all changesets in directory');
621
+ + if (!existsSync(changesetDir)) {
622
+ + return [];
623
+ + }
624
+ +
625
+ + const allChangesets = readdirSync(changesetDir).filter(file => file.endsWith('.md') && file !== 'README.md');
626
+ +
627
+ + return allChangesets;
628
+ +}
629
+ +
630
+ +/**
631
+ + * Validate a single changeset file
632
+ + * @param {string} filePath Full path to the changeset file
633
+ + * @returns {{valid: boolean, type?: string, description?: string, error?: string}}
634
+ + */
635
+ +function validateChangesetFile(filePath) {
636
+ + try {
637
+ + const content = readFileSync(filePath, 'utf-8');
638
+
639
+ - // Ensure exactly one changeset file exists
640
+ + // Check if changeset has a valid type (major, minor, or patch)
641
+ + const versionTypeRegex = new RegExp(`^['"]${PACKAGE_NAME.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}['"]:\\s+(major|minor|patch)`, 'm');
642
+ + const versionTypeMatch = content.match(versionTypeRegex);
643
+ +
644
+ + if (!versionTypeMatch) {
645
+ + return {
646
+ + valid: false,
647
+ + error: `Changeset must specify a version type: major, minor, or patch\nExpected format:\n---\n'${PACKAGE_NAME}': patch\n---\n\nYour description here`,
648
+ + };
649
+ + }
650
+ +
651
+ + // Extract description (everything after the closing ---) and check it's not empty
652
+ + const parts = content.split('---');
653
+ + if (parts.length < 3) {
654
+ + return {
655
+ + valid: false,
656
+ + error: "Changeset must include a description of the changes (after the closing '---')",
657
+ + };
658
+ + }
659
+ +
660
+ + const description = parts.slice(2).join('---').trim();
661
+ + if (!description) {
662
+ + return {
663
+ + valid: false,
664
+ + error: 'Changeset must include a non-empty description of the changes',
665
+ + };
666
+ + }
667
+ +
668
+ + return {
669
+ + valid: true,
670
+ + type: versionTypeMatch[1],
671
+ + description,
672
+ + };
673
+ + } catch (error) {
674
+ + return {
675
+ + valid: false,
676
+ + error: `Failed to read changeset file: ${error.message}`,
677
+ + };
678
+ + }
679
+ +}
680
+ +
681
+ +try {
682
+ + console.log('Validating changesets added by this PR...');
683
+ +
684
+ + // Get changeset files added in this PR
685
+ + const addedChangesetFiles = getAddedChangesetFiles();
686
+ + const changesetCount = addedChangesetFiles.length;
687
+ +
688
+ + console.log(`Found ${changesetCount} changeset file(s) added by this PR`);
689
+ + if (changesetCount > 0) {
690
+ + console.log('Added changesets:');
691
+ + addedChangesetFiles.forEach(file => console.log(` - ${file}`));
692
+ + }
693
+ +
694
+ + // Ensure exactly one changeset file was added
695
+ if (changesetCount === 0) {
696
+ - console.error("::error::No changeset found. Please add a changeset by running 'npm run changeset' and commit the result.");
697
+ + console.error("::error::No changeset found in this PR. Please add a changeset by running 'npm run changeset' and commit the result.");
698
+ process.exit(1);
699
+ } else if (changesetCount > 1) {
700
+ - console.error(`::error::Multiple changesets found (${changesetCount}). Each PR should have exactly ONE changeset.`);
701
+ - console.error('::error::Found changeset files:');
702
+ - changesetFiles.forEach(file => console.error(` ${file}`));
703
+ + console.error(`::error::Multiple changesets found in this PR (${changesetCount}). Each PR should add exactly ONE changeset.`);
704
+ + console.error('::error::Found changeset files added by this PR:');
705
+ + addedChangesetFiles.forEach(file => console.error(` ${file}`));
706
+ + console.error('\n::error::Please combine these into a single changeset or remove the extras.');
707
+ process.exit(1);
708
+ }
709
+
710
+ - // Get the changeset file
711
+ - const changesetFile = join(changesetDir, changesetFiles[0]);
712
+ + // Validate the single changeset file
713
+ + const changesetFile = join('.changeset', addedChangesetFiles[0]);
714
+ console.log(`Validating changeset: ${changesetFile}`);
715
+
716
+ - // Read the changeset file
717
+ - const content = readFileSync(changesetFile, 'utf-8');
718
+ -
719
+ - // Check if changeset has a valid type (major, minor, or patch)
720
+ - const versionTypeRegex = new RegExp(`^['"]${PACKAGE_NAME.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}['"]:\\s+(major|minor|patch)`, 'm');
721
+ - if (!versionTypeRegex.test(content)) {
722
+ - console.error('::error::Changeset must specify a version type: major, minor, or patch');
723
+ - console.error(`::error::Expected format in ${changesetFile}:`);
724
+ - console.error('::error::---');
725
+ - console.error(`::error::'${PACKAGE_NAME}': patch`);
726
+ - console.error('::error::---');
727
+ - console.error('::error::');
728
+ - console.error('::error::Your description here');
729
+ - console.error('\nFile content:');
730
+ - console.error(content);
731
+ - process.exit(1);
732
+ - }
733
+ + const validation = validateChangesetFile(changesetFile);
734
+
735
+ - // Extract description (everything after the closing ---) and check it's not empty
736
+ - const parts = content.split('---');
737
+ - if (parts.length < 3) {
738
+ - console.error('::error::Changeset must include a description of the changes');
739
+ - console.error("::error::The description should appear after the closing '---' in the changeset file");
740
+ - console.error(`::error::Current content of ${changesetFile}:`);
741
+ - console.error(content);
742
+ + if (!validation.valid) {
743
+ + console.error(`::error::${validation.error}`);
744
+ + console.error(`\nFile content of ${changesetFile}:`);
745
+ + try {
746
+ + console.error(readFileSync(changesetFile, 'utf-8'));
747
+ + } catch {
748
+ + console.error('(could not read file)');
749
+ + }
750
+ process.exit(1);
751
+ }
752
+
753
+ - const description = parts.slice(2).join('---').trim();
754
+ - if (!description) {
755
+ - console.error('::error::Changeset must include a description of the changes');
756
+ - console.error("::error::The description should appear after the closing '---' in the changeset file");
757
+ - console.error(`::error::Current content of ${changesetFile}:`);
758
+ - console.error(content);
759
+ - process.exit(1);
760
+ - }
761
+ -
762
+ - // Extract version type
763
+ - const versionTypeMatch = content.match(versionTypeRegex);
764
+ - const versionType = versionTypeMatch ? versionTypeMatch[1] : 'unknown';
765
+ -
766
+ console.log('Changeset validation passed');
767
+ - console.log(` Type: ${versionType}`);
768
+ - console.log(` Description: ${description}`);
769
+ + console.log(` Type: ${validation.type}`);
770
+ + console.log(` Description: ${validation.description}`);
771
+ } catch (error) {
772
+ console.error('Error during changeset validation:', error.message);
773
+ if (process.env.DEBUG) {