claude-git-hooks 2.51.2 → 2.66.1
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/CHANGELOG.md +513 -30
- package/lib/commands/back-merge.js +91 -11
- package/lib/commands/bump-version.js +72 -76
- package/lib/commands/create-pr.js +124 -44
- package/lib/commands/create-release.js +198 -52
- package/lib/commands/help.js +13 -3
- package/lib/messages/library-warnings.js +147 -0
- package/lib/utils/claude-client.js +6 -1
- package/lib/utils/git-tag-manager.js +104 -0
- package/lib/utils/github-api.js +30 -0
- package/lib/utils/judge.js +2 -1
- package/lib/utils/linter-runner.js +6 -0
- package/lib/utils/version-manager.js +30 -17
- package/package.json +85 -83
|
@@ -39,7 +39,8 @@ import {
|
|
|
39
39
|
checkoutBranch,
|
|
40
40
|
pushBranch,
|
|
41
41
|
deleteRemoteBranch,
|
|
42
|
-
createCommit
|
|
42
|
+
createCommit,
|
|
43
|
+
stageFiles
|
|
43
44
|
} from '../utils/git-operations.js';
|
|
44
45
|
import {
|
|
45
46
|
getLatestLocalTag,
|
|
@@ -650,6 +651,53 @@ export async function runBackMerge(args) {
|
|
|
650
651
|
showSuccess(`✓ Merge committed: ${commitMsg}`);
|
|
651
652
|
console.log('');
|
|
652
653
|
|
|
654
|
+
// 14b. Co-change correlation pipeline (AUT-3777)
|
|
655
|
+
let coChangeReport = null;
|
|
656
|
+
try {
|
|
657
|
+
const { coChangePipeline } = await import('../../.library/librarian/index.js');
|
|
658
|
+
|
|
659
|
+
showInfo('Running co-change correlation pipeline...');
|
|
660
|
+
const summary = coChangePipeline({ repoCtx: {} });
|
|
661
|
+
const { modifiedFiles, perStep, report, warnings, mode } = summary;
|
|
662
|
+
|
|
663
|
+
// Surface summary to stdout
|
|
664
|
+
showInfo(
|
|
665
|
+
`Co-change: mode=${mode}, detected=${perStep.detection.modifications}, ` +
|
|
666
|
+
`injected=${perStep.injection.modifications}, warnings=${warnings.length}`
|
|
667
|
+
);
|
|
668
|
+
for (const w of warnings) {
|
|
669
|
+
showWarning(w);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Save report for PR body attachment after push
|
|
673
|
+
coChangeReport = report;
|
|
674
|
+
|
|
675
|
+
if (modifiedFiles.length === 0) {
|
|
676
|
+
showInfo('Co-change pipeline produced no Library changes');
|
|
677
|
+
} else {
|
|
678
|
+
const root = getRepoRoot();
|
|
679
|
+
const absFiles = modifiedFiles.map(f => path.isAbsolute(f) ? f : path.join(root, f));
|
|
680
|
+
const stageResult = stageFiles(absFiles);
|
|
681
|
+
|
|
682
|
+
if (!stageResult.success) {
|
|
683
|
+
showWarning(`Failed to stage co-change files: ${stageResult.error}`);
|
|
684
|
+
} else {
|
|
685
|
+
const windowDesc = tagName ? `${tagName}..HEAD` : 'full history';
|
|
686
|
+
const libCommitMsg = `chore(library): co-change correlations for ${windowDesc}`;
|
|
687
|
+
const libCommit = createCommit(libCommitMsg);
|
|
688
|
+
|
|
689
|
+
if (!libCommit.success) {
|
|
690
|
+
showWarning(`Failed to commit co-change files: ${libCommit.error}`);
|
|
691
|
+
} else {
|
|
692
|
+
showSuccess(`Co-change committed: ${libCommitMsg} (${modifiedFiles.length} file(s))`);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
} catch (err) {
|
|
697
|
+
logger.warning(`co-change pipeline skipped: ${err.message}`);
|
|
698
|
+
logger.debug('back-merge', 'Co-change pipeline skipped', { error: err.message });
|
|
699
|
+
}
|
|
700
|
+
|
|
653
701
|
// 15. Verify sync
|
|
654
702
|
try {
|
|
655
703
|
const postMergeDivergence = getDivergence(opts.into, `origin/${opts.from}`);
|
|
@@ -719,18 +767,50 @@ export async function runBackMerge(args) {
|
|
|
719
767
|
await _revertFollowup(rcBranchForLog, repoRoot);
|
|
720
768
|
}
|
|
721
769
|
|
|
722
|
-
// 18b.
|
|
723
|
-
if (pushStatus === 'pushed') {
|
|
770
|
+
// 18b. Attach co-change report to PR body (AUT-3777)
|
|
771
|
+
if (coChangeReport && pushStatus === 'pushed') {
|
|
724
772
|
try {
|
|
725
|
-
const {
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
773
|
+
const { parseGitHubRepo } = await import('../utils/github-client.js');
|
|
774
|
+
const { findExistingPR, updatePullRequestBody } = await import('../utils/github-api.js');
|
|
775
|
+
const repoInfo = parseGitHubRepo();
|
|
776
|
+
|
|
777
|
+
const existingPR = await findExistingPR({
|
|
778
|
+
owner: repoInfo.owner,
|
|
779
|
+
repo: repoInfo.repo,
|
|
780
|
+
head: opts.from,
|
|
781
|
+
base: opts.into,
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
if (existingPR) {
|
|
785
|
+
const OPEN_MARKER = '<!-- LIBRARY_COCHANGE_REPORT -->';
|
|
786
|
+
const CLOSE_MARKER = '<!-- /LIBRARY_COCHANGE_REPORT -->';
|
|
787
|
+
const section = `${OPEN_MARKER}\n${coChangeReport}\n${CLOSE_MARKER}`;
|
|
788
|
+
const markerRegex = new RegExp(
|
|
789
|
+
`${OPEN_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${CLOSE_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`
|
|
790
|
+
);
|
|
791
|
+
|
|
792
|
+
let newBody;
|
|
793
|
+
if (markerRegex.test(existingPR.body || '')) {
|
|
794
|
+
// Re-run: replace existing section
|
|
795
|
+
newBody = (existingPR.body).replace(markerRegex, section);
|
|
796
|
+
} else {
|
|
797
|
+
// First run: append section
|
|
798
|
+
newBody = `${existingPR.body || ''}\n\n${section}`;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
await updatePullRequestBody(repoInfo.owner, repoInfo.repo, existingPR.number, newBody);
|
|
802
|
+
showSuccess('Co-change report attached to PR body');
|
|
803
|
+
} else {
|
|
804
|
+
logger.debug('back-merge', 'No existing PR found for report attachment', {
|
|
805
|
+
head: opts.from,
|
|
806
|
+
base: opts.into,
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
} catch (prErr) {
|
|
810
|
+
logger.debug('back-merge', 'Could not attach co-change report to PR', {
|
|
811
|
+
error: prErr.message,
|
|
812
|
+
});
|
|
732
813
|
}
|
|
733
|
-
console.log('');
|
|
734
814
|
}
|
|
735
815
|
|
|
736
816
|
// 19. Summary
|
|
@@ -48,12 +48,14 @@ import {
|
|
|
48
48
|
showError,
|
|
49
49
|
showWarning,
|
|
50
50
|
promptConfirmation,
|
|
51
|
-
|
|
52
|
-
promptToggleList,
|
|
53
|
-
promptEditField
|
|
51
|
+
promptToggleList
|
|
54
52
|
} from '../utils/interactive-ui.js';
|
|
55
53
|
import logger from '../utils/logger.js';
|
|
56
54
|
import { colors, error, checkGitRepo } from './helpers.js';
|
|
55
|
+
import {
|
|
56
|
+
CONSOLE_WARNING_TEMPLATE,
|
|
57
|
+
LIBRARY_VERIFY_SKIPPED_WARNING
|
|
58
|
+
} from '../messages/library-warnings.js';
|
|
57
59
|
|
|
58
60
|
/**
|
|
59
61
|
* Validates prerequisites before version bump
|
|
@@ -205,7 +207,9 @@ function displayDiscoveryTable(discovery) {
|
|
|
205
207
|
);
|
|
206
208
|
console.log('');
|
|
207
209
|
|
|
208
|
-
|
|
210
|
+
const visibleFiles = discovery.files.filter((f) => f.selected);
|
|
211
|
+
|
|
212
|
+
if (visibleFiles.length === 0) {
|
|
209
213
|
console.log(' No version files found.');
|
|
210
214
|
console.log('');
|
|
211
215
|
return;
|
|
@@ -217,8 +221,8 @@ function displayDiscoveryTable(discovery) {
|
|
|
217
221
|
);
|
|
218
222
|
console.log(` ${'-'.repeat(3)} ${'-'.repeat(35)} ${'-'.repeat(15)} ${'-'.repeat(15)}`);
|
|
219
223
|
|
|
220
|
-
// Table rows
|
|
221
|
-
|
|
224
|
+
// Table rows (only semver files)
|
|
225
|
+
visibleFiles.forEach((file, index) => {
|
|
222
226
|
const num = `${index + 1}`.padEnd(3);
|
|
223
227
|
const filePath = file.relativePath.padEnd(35);
|
|
224
228
|
const fileType = file.projectLabel.padEnd(15);
|
|
@@ -289,70 +293,20 @@ function showDryRunPreview(info) {
|
|
|
289
293
|
*/
|
|
290
294
|
async function promptFileSelection(discovery) {
|
|
291
295
|
console.log('');
|
|
292
|
-
showWarning('Version mismatch detected or interactive mode enabled');
|
|
293
|
-
console.log('');
|
|
294
296
|
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const choice = await promptMenu('Choose action:', options, 'a');
|
|
303
|
-
|
|
304
|
-
if (choice === 'c') {
|
|
305
|
-
showInfo('Version bump cancelled');
|
|
306
|
-
process.exit(0);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (choice === 'a') {
|
|
310
|
-
// All files selected
|
|
311
|
-
return discovery.files;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
if (choice === 's') {
|
|
315
|
-
// Toggle list selection
|
|
316
|
-
const items = discovery.files.map((file, index) => ({
|
|
317
|
-
key: `${index}`,
|
|
318
|
-
label: `${file.relativePath} (${file.projectLabel}) - ${file.version || 'N/A'}`,
|
|
319
|
-
selected: file.selected
|
|
320
|
-
}));
|
|
321
|
-
|
|
322
|
-
const selectedKeys = await promptToggleList(
|
|
323
|
-
'Select files to update (type number to toggle, Enter to confirm):',
|
|
324
|
-
items
|
|
325
|
-
);
|
|
297
|
+
const semverFiles = discovery.files.filter((f) => f.selected);
|
|
298
|
+
const items = semverFiles.map((file, index) => ({
|
|
299
|
+
key: `${index}`,
|
|
300
|
+
label: `${file.relativePath} (${file.projectLabel}) - ${file.version || 'N/A'}`,
|
|
301
|
+
selected: true
|
|
302
|
+
}));
|
|
326
303
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
if (choice === 'e') {
|
|
332
|
-
// Per-file version editing
|
|
333
|
-
showInfo('Enter target version for each file (press Enter to keep calculated version)');
|
|
334
|
-
console.log('');
|
|
335
|
-
|
|
336
|
-
for (const file of discovery.files) {
|
|
337
|
-
const input = await promptEditField(
|
|
338
|
-
`${file.relativePath} (${file.projectLabel})`,
|
|
339
|
-
file.version || 'N/A'
|
|
340
|
-
);
|
|
341
|
-
|
|
342
|
-
// If user entered something different from current version, store it
|
|
343
|
-
if (input !== file.version && input !== 'N/A') {
|
|
344
|
-
if (!validateVersionFormat(input)) {
|
|
345
|
-
showWarning(`Invalid version format: ${input} — skipping ${file.relativePath}`);
|
|
346
|
-
continue;
|
|
347
|
-
}
|
|
348
|
-
file.targetVersion = input;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
return discovery.files;
|
|
353
|
-
}
|
|
304
|
+
const selectedKeys = await promptToggleList(
|
|
305
|
+
'Select files to update (type number to toggle, Enter to confirm):',
|
|
306
|
+
items
|
|
307
|
+
);
|
|
354
308
|
|
|
355
|
-
return
|
|
309
|
+
return semverFiles.filter((_, index) => selectedKeys.includes(`${index}`));
|
|
356
310
|
}
|
|
357
311
|
|
|
358
312
|
/**
|
|
@@ -374,6 +328,25 @@ function restoreSnapshot(snapshot) {
|
|
|
374
328
|
});
|
|
375
329
|
}
|
|
376
330
|
|
|
331
|
+
/**
|
|
332
|
+
* Emit a loud Library staleness warning to stderr.
|
|
333
|
+
* Content comes from the wording constant + verify result data.
|
|
334
|
+
* Formatting uses ANSI codes for visual emphasis on TTY stderr.
|
|
335
|
+
*
|
|
336
|
+
* @param {import('../../.library/librarian/index.js').VerifyResult} result
|
|
337
|
+
* @private
|
|
338
|
+
*/
|
|
339
|
+
function _emitLibraryWarning(result) {
|
|
340
|
+
const y = colors.yellow;
|
|
341
|
+
const r = colors.reset;
|
|
342
|
+
const bar = `${y}${'='.repeat(63)}${r}`;
|
|
343
|
+
const content = CONSOLE_WARNING_TEMPLATE(result, { autoRegen: 'deferred' });
|
|
344
|
+
|
|
345
|
+
const lines = ['', bar, '', `${y}${content}${r}`, '', bar, ''];
|
|
346
|
+
|
|
347
|
+
process.stderr.write(lines.join('\n'));
|
|
348
|
+
}
|
|
349
|
+
|
|
377
350
|
/**
|
|
378
351
|
* Bump version command
|
|
379
352
|
* @param {Array<string>} args - Command arguments
|
|
@@ -441,6 +414,22 @@ export async function runBumpVersion(args) {
|
|
|
441
414
|
|
|
442
415
|
showSuccess('Prerequisites validated');
|
|
443
416
|
|
|
417
|
+
// Library verification gate — silent on clean, loud-warn on stale, never-hard-fail
|
|
418
|
+
logger.debug('bump-version', 'Running Library verification gate');
|
|
419
|
+
try {
|
|
420
|
+
const { verify } = await import('../../.library/librarian/index.js');
|
|
421
|
+
const verifyResult = await verify();
|
|
422
|
+
|
|
423
|
+
if (verifyResult.clean) {
|
|
424
|
+
logger.debug('bump-version', 'Library is clean — no warning needed');
|
|
425
|
+
} else {
|
|
426
|
+
_emitLibraryWarning(verifyResult);
|
|
427
|
+
}
|
|
428
|
+
} catch (verifyErr) {
|
|
429
|
+
const msg = `\n${colors.yellow} ${LIBRARY_VERIFY_SKIPPED_WARNING} ${verifyErr.message}${colors.reset}\n\n`;
|
|
430
|
+
process.stderr.write(msg);
|
|
431
|
+
}
|
|
432
|
+
|
|
444
433
|
// Step 2: Discover all version files
|
|
445
434
|
logger.debug('bump-version', 'Step 2: Discovering version files');
|
|
446
435
|
const discovery = discoverVersionFiles();
|
|
@@ -465,15 +454,6 @@ export async function runBumpVersion(args) {
|
|
|
465
454
|
// Display discovery table
|
|
466
455
|
displayDiscoveryTable(discovery);
|
|
467
456
|
|
|
468
|
-
const currentVersion = discovery.resolvedVersion;
|
|
469
|
-
|
|
470
|
-
if (!currentVersion) {
|
|
471
|
-
showError('Could not determine current version from discovered files');
|
|
472
|
-
process.exit(1);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
showInfo(`Current version: ${currentVersion}`);
|
|
476
|
-
|
|
477
457
|
// Step 3: File selection (if mismatch or interactive mode)
|
|
478
458
|
let selectedFiles = discovery.files.filter((f) => f.selected);
|
|
479
459
|
|
|
@@ -489,6 +469,22 @@ export async function runBumpVersion(args) {
|
|
|
489
469
|
console.log('');
|
|
490
470
|
}
|
|
491
471
|
|
|
472
|
+
// Derive current version from selected files (not from discovery.resolvedVersion)
|
|
473
|
+
const selectedVersions = selectedFiles
|
|
474
|
+
.filter((f) => f.version !== null)
|
|
475
|
+
.map((f) => f.version);
|
|
476
|
+
const uniqueSelected = [...new Set(selectedVersions)];
|
|
477
|
+
const currentVersion = uniqueSelected.length === 1
|
|
478
|
+
? uniqueSelected[0]
|
|
479
|
+
: discovery.resolvedVersion;
|
|
480
|
+
|
|
481
|
+
if (!currentVersion || !validateVersionFormat(currentVersion)) {
|
|
482
|
+
showError('Could not determine a valid semver version from selected files');
|
|
483
|
+
process.exit(1);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
showInfo(`Current version: ${currentVersion}`);
|
|
487
|
+
|
|
492
488
|
// Step 4: Calculate new version or apply suffix operation
|
|
493
489
|
logger.debug('bump-version', 'Step 4: Calculating new version');
|
|
494
490
|
let newVersion;
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
showWarning,
|
|
23
23
|
promptConfirmation
|
|
24
24
|
} from '../utils/interactive-ui.js';
|
|
25
|
-
import { getBranchPushStatus, pushBranch } from '../utils/git-operations.js';
|
|
25
|
+
import { getBranchPushStatus, pushBranch, stageFiles, createCommit, getRepoRoot } from '../utils/git-operations.js';
|
|
26
26
|
import logger from '../utils/logger.js';
|
|
27
27
|
import { resolveLabels } from '../utils/label-resolver.js';
|
|
28
28
|
import { CostTracker } from '../utils/cost-tracker.js';
|
|
@@ -364,7 +364,7 @@ export async function runCreatePr(args) {
|
|
|
364
364
|
// Step 5.6: Version alignment validation (Issue #44)
|
|
365
365
|
logger.debug('create-pr', 'Step 5.6: Validating version alignment');
|
|
366
366
|
const { validateVersionAlignment } = await import('../utils/version-manager.js');
|
|
367
|
-
const versionCheck = await validateVersionAlignment();
|
|
367
|
+
const versionCheck = await validateVersionAlignment(baseBranch);
|
|
368
368
|
|
|
369
369
|
if (!versionCheck.aligned) {
|
|
370
370
|
showWarning('Version misalignment detected:');
|
|
@@ -451,23 +451,135 @@ export async function runCreatePr(args) {
|
|
|
451
451
|
}
|
|
452
452
|
}
|
|
453
453
|
|
|
454
|
-
// Step 5.7:
|
|
455
|
-
|
|
454
|
+
// Step 5.7: Library maintenance pipeline (AUT-3764)
|
|
455
|
+
// Runs BEFORE tag push so that library regen is included in the tagged commit.
|
|
456
|
+
// Guard: only run if the current repo has its own .library/ setup.
|
|
457
|
+
// When claude-hooks is installed in a foreign repo (npm link / file: ref),
|
|
458
|
+
// the relative import resolves to git-hooks' .library/ — not this repo's.
|
|
459
|
+
let libraryCommitted = false;
|
|
460
|
+
const root = getRepoRoot();
|
|
461
|
+
const libraryResolverPath = path.join(root, '.library', 'resolver.yaml');
|
|
462
|
+
if (!fs.existsSync(libraryResolverPath)) {
|
|
463
|
+
logger.debug('create-pr', 'No .library/resolver.yaml in current repo — skipping Library pipeline');
|
|
464
|
+
} else {
|
|
465
|
+
logger.debug('create-pr', 'Step 5.7: Running Library maintenance pipeline');
|
|
466
|
+
try {
|
|
467
|
+
showInfo('Running Library maintenance pipeline...');
|
|
468
|
+
const { createPrPipeline } = await import('../../.library/librarian/index.js');
|
|
469
|
+
|
|
470
|
+
const pipelineSummary = await createPrPipeline({ repoRoot: root });
|
|
471
|
+
const {
|
|
472
|
+
modifiedFiles: libraryFiles,
|
|
473
|
+
perStep,
|
|
474
|
+
pendingDueToApiDown,
|
|
475
|
+
warnings: pipelineWarnings,
|
|
476
|
+
} = pipelineSummary;
|
|
477
|
+
|
|
478
|
+
// Surface pipeline summary
|
|
479
|
+
logger.debug('create-pr', 'Pipeline completed', {
|
|
480
|
+
modifiedCount: libraryFiles.length,
|
|
481
|
+
pendingDueToApiDown,
|
|
482
|
+
warningCount: pipelineWarnings.length,
|
|
483
|
+
});
|
|
484
|
+
showInfo(`Staleness: ${perStep.staleness.staleCount} stale, ${perStep.staleness.unbookedCount} unbooked`);
|
|
485
|
+
if (perStep.regen.changed > 0) {
|
|
486
|
+
showInfo(`Regenerated: ${perStep.regen.changed} book(s)`);
|
|
487
|
+
}
|
|
488
|
+
if (perStep.addRemoveRename.created > 0) {
|
|
489
|
+
showInfo(`Created: ${perStep.addRemoveRename.created} new book(s)`);
|
|
490
|
+
}
|
|
491
|
+
if (pendingDueToApiDown > 0) {
|
|
492
|
+
showWarning(`Gotchas pending (API down): ${pendingDueToApiDown}`);
|
|
493
|
+
}
|
|
494
|
+
for (const w of pipelineWarnings) {
|
|
495
|
+
showWarning(w);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (libraryFiles.length === 0) {
|
|
499
|
+
showInfo('Library already in sync');
|
|
500
|
+
} else {
|
|
501
|
+
// Stage all modified Library files (stageFiles expects absolute paths)
|
|
502
|
+
const absFiles = libraryFiles.map(f => path.join(root, f));
|
|
503
|
+
const stageResult = stageFiles(absFiles);
|
|
504
|
+
|
|
505
|
+
if (!stageResult.success) {
|
|
506
|
+
showWarning(`Failed to stage Library files: ${stageResult.error}`);
|
|
507
|
+
} else {
|
|
508
|
+
// Commit with deterministic message — separate commit, not amend
|
|
509
|
+
const libraryCommitMsg = `chore(library): sync books for ${currentBranch}`;
|
|
510
|
+
const commitResult = createCommit(libraryCommitMsg);
|
|
511
|
+
|
|
512
|
+
if (!commitResult.success) {
|
|
513
|
+
showWarning(`Failed to commit Library changes: ${commitResult.error}`);
|
|
514
|
+
} else {
|
|
515
|
+
libraryCommitted = true;
|
|
516
|
+
showSuccess(`Library committed: ${libraryCommitMsg} (${libraryFiles.length} file(s))`);
|
|
517
|
+
|
|
518
|
+
// Push the Library commit to the PR branch's remote
|
|
519
|
+
const libraryPushResult = pushBranch(currentBranch);
|
|
520
|
+
if (!libraryPushResult.success) {
|
|
521
|
+
showWarning(`Failed to push Library commit: ${libraryPushResult.error}`);
|
|
522
|
+
} else {
|
|
523
|
+
showSuccess('Library commit pushed');
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
} catch (pipelineErr) {
|
|
529
|
+
// Pipeline failure is non-blocking — log and continue with PR creation
|
|
530
|
+
logger.warning('create-pr', 'Library pipeline failed, continuing', {
|
|
531
|
+
error: pipelineErr.message,
|
|
532
|
+
});
|
|
533
|
+
showWarning(`Library pipeline unavailable: ${pipelineErr.message}`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Step 5.8: Smart tag pushing (Issue #44)
|
|
538
|
+
// Runs AFTER library maintenance so tags include the library commit.
|
|
539
|
+
logger.debug('create-pr', 'Step 5.8: Checking and pushing unpushed tags');
|
|
456
540
|
const {
|
|
457
541
|
compareLocalAndRemoteTags,
|
|
458
542
|
pushTags: pushTagsUtil,
|
|
459
|
-
|
|
460
|
-
|
|
543
|
+
createTag: createTagUtil,
|
|
544
|
+
getLatestRemoteTagOnBranch,
|
|
461
545
|
parseTagVersion
|
|
462
546
|
} = await import('../utils/git-tag-manager.js');
|
|
463
547
|
const { compareVersions } = await import('../utils/version-manager.js');
|
|
464
548
|
|
|
549
|
+
// If library was committed after bump-version created the tag,
|
|
550
|
+
// re-point unpushed tags to HEAD so the tag includes library books.
|
|
551
|
+
if (libraryCommitted) {
|
|
552
|
+
const unpushedTags = await compareLocalAndRemoteTags();
|
|
553
|
+
for (const tag of unpushedTags.localNewer) {
|
|
554
|
+
const version = parseTagVersion(tag);
|
|
555
|
+
if (version) {
|
|
556
|
+
logger.debug('create-pr', 'Re-pointing tag to include library commit', { tag });
|
|
557
|
+
let origTagMsg = '';
|
|
558
|
+
try { origTagMsg = execSync(`git tag -l --format="%(contents)" ${tag}`, { encoding: 'utf8' }).trim(); } catch { /* ignore */ }
|
|
559
|
+
const tagMsg = origTagMsg || `Release version ${version}`;
|
|
560
|
+
try {
|
|
561
|
+
await createTagUtil(version, tagMsg, { force: true });
|
|
562
|
+
showInfo(`Tag ${tag} moved to include library commit`);
|
|
563
|
+
} catch (tagErr) {
|
|
564
|
+
logger.warning('create-pr', 'Failed to re-point tag, continuing', { tag, error: tagErr.message });
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
465
570
|
const tagComparison = await compareLocalAndRemoteTags();
|
|
466
571
|
|
|
467
572
|
if (tagComparison.localNewer.length > 0) {
|
|
468
|
-
//
|
|
469
|
-
const
|
|
470
|
-
|
|
573
|
+
// Derive latest unpushed tag (scoped to what's actually being pushed)
|
|
574
|
+
const sortedUnpushed = tagComparison.localNewer
|
|
575
|
+
.map((t) => ({ tag: t, version: parseTagVersion(t) }))
|
|
576
|
+
.filter((t) => t.version !== null)
|
|
577
|
+
.sort((a, b) => compareVersions(a.version, b.version));
|
|
578
|
+
const latestLocalTag = sortedUnpushed.length > 0
|
|
579
|
+
? sortedUnpushed[sortedUnpushed.length - 1].tag
|
|
580
|
+
: tagComparison.localNewer[0];
|
|
581
|
+
// Compare against latest remote tag on the BASE branch (not global latest)
|
|
582
|
+
const latestRemoteTag = getLatestRemoteTagOnBranch(baseBranch);
|
|
471
583
|
|
|
472
584
|
const localVersion = latestLocalTag ? parseTagVersion(latestLocalTag) : null;
|
|
473
585
|
const remoteVersion = latestRemoteTag ? parseTagVersion(latestRemoteTag) : null;
|
|
@@ -483,7 +595,7 @@ export async function runCreatePr(args) {
|
|
|
483
595
|
let shouldPushTags = false;
|
|
484
596
|
let userChoice = null;
|
|
485
597
|
|
|
486
|
-
// Case 1: Local tag > Remote tag → Auto-push
|
|
598
|
+
// Case 1: Local tag > Remote tag → Auto-push (normal bump-version flow)
|
|
487
599
|
if (localVersion && remoteVersion && compareVersions(localVersion, remoteVersion) > 0) {
|
|
488
600
|
logger.debug('create-pr', 'Local version > remote version, auto-pushing', {
|
|
489
601
|
localVersion,
|
|
@@ -494,7 +606,7 @@ export async function runCreatePr(args) {
|
|
|
494
606
|
showInfo('Auto-pushing tag to remote...');
|
|
495
607
|
shouldPushTags = true;
|
|
496
608
|
|
|
497
|
-
// Case 2: Local tag = Remote tag → Prompt with warning
|
|
609
|
+
// Case 2: Local tag = Remote tag → Prompt with warning (may already be pushed)
|
|
498
610
|
} else if (
|
|
499
611
|
localVersion &&
|
|
500
612
|
remoteVersion &&
|
|
@@ -534,7 +646,7 @@ export async function runCreatePr(args) {
|
|
|
534
646
|
shouldPushTags = true;
|
|
535
647
|
}
|
|
536
648
|
|
|
537
|
-
// Case 3: Local tag < Remote tag → Prompt with
|
|
649
|
+
// Case 3: Local tag < Remote tag → Prompt with warning (someone pushed newer)
|
|
538
650
|
} else if (
|
|
539
651
|
localVersion &&
|
|
540
652
|
remoteVersion &&
|
|
@@ -765,38 +877,6 @@ export async function runCreatePr(args) {
|
|
|
765
877
|
finalBody = `> ⚠️ This PR must be merged with **merge commit** (not squash)\n\n${prBody}`;
|
|
766
878
|
}
|
|
767
879
|
|
|
768
|
-
// Library staleness section — informational, non-blocking
|
|
769
|
-
try {
|
|
770
|
-
showInfo('📚 Checking library staleness...');
|
|
771
|
-
const { checkBook } = await import('../../.library/tools/staleness.js');
|
|
772
|
-
const { getBooksDir } = await import('../../.library/paths.js');
|
|
773
|
-
const { getRepoRoot } = await import('../utils/git-operations.js');
|
|
774
|
-
const booksDir = getBooksDir();
|
|
775
|
-
const root = getRepoRoot();
|
|
776
|
-
const changedSourceFiles = (filesArray || [])
|
|
777
|
-
.map(f => f.path || f)
|
|
778
|
-
.filter(p => p.startsWith('lib/') && p.endsWith('.js'));
|
|
779
|
-
const staleBooks = [];
|
|
780
|
-
for (const srcPath of changedSourceFiles) {
|
|
781
|
-
const bookName = `${srcPath.replace(/^lib\/.*\//, '').replace(/\.js$/, '')}.md`;
|
|
782
|
-
try {
|
|
783
|
-
const result = await checkBook(path.join(booksDir, bookName), root);
|
|
784
|
-
if (result.status === 'stale') staleBooks.push(bookName);
|
|
785
|
-
} catch {
|
|
786
|
-
// skip
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
if (staleBooks.length > 0) {
|
|
790
|
-
const bookList = staleBooks.map(b => `- \`${b}\``).join('\n');
|
|
791
|
-
finalBody += `\n\n---\n\n### 📚 Library Staleness\n\nThe following library books may need regeneration:\n\n${bookList}\n\nRun: \`npm run library:regenerate\``;
|
|
792
|
-
showWarning(`📚 ${staleBooks.length} stale book(s) — section added to PR body`);
|
|
793
|
-
} else if (changedSourceFiles.length > 0) {
|
|
794
|
-
showSuccess('📚 Library books are current');
|
|
795
|
-
}
|
|
796
|
-
} catch {
|
|
797
|
-
showWarning('📚 Library staleness check unavailable — .library/ tools not found');
|
|
798
|
-
}
|
|
799
|
-
|
|
800
880
|
// Step 10: Get reviewers from team + config fallback
|
|
801
881
|
logger.debug('create-pr', 'Step 10: Selecting reviewers via team resolution');
|
|
802
882
|
const reviewers = await selectReviewers({
|