claude-git-hooks 2.21.0 → 2.30.2
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 +148 -6
- package/CLAUDE.md +469 -69
- package/README.md +5 -0
- package/bin/claude-hooks +101 -0
- package/lib/cli-metadata.js +68 -1
- package/lib/commands/analyze-pr.js +19 -24
- package/lib/commands/back-merge.js +740 -0
- package/lib/commands/check-coupling.js +209 -0
- package/lib/commands/close-release.js +485 -0
- package/lib/commands/create-pr.js +62 -3
- package/lib/commands/create-release.js +600 -0
- package/lib/commands/diff-batch-info.js +7 -13
- package/lib/commands/help.js +5 -7
- package/lib/commands/install.js +1 -5
- package/lib/commands/revert-feature.js +436 -0
- package/lib/commands/shadow.js +654 -0
- package/lib/config.js +1 -2
- package/lib/hooks/pre-commit.js +8 -6
- package/lib/utils/authorization.js +429 -0
- package/lib/utils/claude-client.js +14 -7
- package/lib/utils/coupling-detector.js +133 -0
- package/lib/utils/diff-analysis-orchestrator.js +7 -14
- package/lib/utils/git-operations.js +480 -1
- package/lib/utils/github-api.js +182 -0
- package/lib/utils/judge.js +66 -7
- package/lib/utils/linear-connector.js +1 -4
- package/lib/utils/package-info.js +0 -1
- package/lib/utils/token-store.js +5 -3
- package/package.json +69 -69
|
@@ -23,6 +23,21 @@ import { getBranchPushStatus, pushBranch } from '../utils/git-operations.js';
|
|
|
23
23
|
import logger from '../utils/logger.js';
|
|
24
24
|
import { error, checkGitRepo } from './helpers.js';
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Detect expected merge strategy from branch naming conventions
|
|
28
|
+
* @param {string} sourceBranch - Source (head) branch name
|
|
29
|
+
* @param {string} targetBranch - Target (base) branch name
|
|
30
|
+
* @returns {'squash'|'merge-commit'|'unknown'}
|
|
31
|
+
*/
|
|
32
|
+
function detectMergeStrategy(sourceBranch, targetBranch) {
|
|
33
|
+
if (targetBranch === 'main') return 'merge-commit';
|
|
34
|
+
if (sourceBranch.startsWith('feature/')) return 'squash';
|
|
35
|
+
if (sourceBranch.startsWith('release-fix/')) return 'squash';
|
|
36
|
+
if (sourceBranch.startsWith('release-candidate/')) return 'merge-commit';
|
|
37
|
+
if (sourceBranch.startsWith('hotfix/')) return 'merge-commit';
|
|
38
|
+
return 'unknown';
|
|
39
|
+
}
|
|
40
|
+
|
|
26
41
|
/**
|
|
27
42
|
* Create PR command (v2.5.0+ - Octokit-based)
|
|
28
43
|
* @param {Array<string>} args - Command arguments
|
|
@@ -48,8 +63,9 @@ export async function runCreatePr(args) {
|
|
|
48
63
|
|
|
49
64
|
// Import GitHub API module
|
|
50
65
|
logger.debug('create-pr', 'Importing GitHub API modules');
|
|
51
|
-
const { createPullRequest, validateToken, findExistingPR } =
|
|
52
|
-
|
|
66
|
+
const { createPullRequest, validateToken, findExistingPR } = await import(
|
|
67
|
+
'../utils/github-api.js'
|
|
68
|
+
);
|
|
53
69
|
const { parseGitHubRepo } = await import('../utils/github-client.js');
|
|
54
70
|
|
|
55
71
|
showInfo('🚀 Creating Pull Request...');
|
|
@@ -552,6 +568,39 @@ export async function runCreatePr(args) {
|
|
|
552
568
|
}
|
|
553
569
|
logger.debug('create-pr', 'Labels determined', { labels, preset: config.preset });
|
|
554
570
|
|
|
571
|
+
// Step 9.5: Detect merge strategy
|
|
572
|
+
logger.debug('create-pr', 'Step 9.5: Detecting merge strategy', {
|
|
573
|
+
sourceBranch: currentBranch,
|
|
574
|
+
targetBranch: baseBranch
|
|
575
|
+
});
|
|
576
|
+
let mergeStrategy = detectMergeStrategy(currentBranch, baseBranch);
|
|
577
|
+
|
|
578
|
+
if (mergeStrategy === 'unknown') {
|
|
579
|
+
showWarning('Unknown branch pattern — cannot auto-detect merge strategy');
|
|
580
|
+
console.log('');
|
|
581
|
+
const strategyChoice = await promptMenu(
|
|
582
|
+
'Select merge strategy:',
|
|
583
|
+
[
|
|
584
|
+
{ key: 's', label: 'Squash merge' },
|
|
585
|
+
{ key: 'm', label: 'Merge commit' },
|
|
586
|
+
{ key: 'k', label: 'Skip (no strategy label)' }
|
|
587
|
+
],
|
|
588
|
+
'k'
|
|
589
|
+
);
|
|
590
|
+
if (strategyChoice === 's') mergeStrategy = 'squash';
|
|
591
|
+
else if (strategyChoice === 'm') mergeStrategy = 'merge-commit';
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (mergeStrategy !== 'unknown') {
|
|
595
|
+
labels.push(`merge-strategy:${mergeStrategy}`);
|
|
596
|
+
logger.debug('create-pr', 'Merge strategy label added', { mergeStrategy });
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
let finalBody = prBody;
|
|
600
|
+
if (mergeStrategy === 'merge-commit') {
|
|
601
|
+
finalBody = `> ⚠️ This PR must be merged with **merge commit** (not squash)\n\n${prBody}`;
|
|
602
|
+
}
|
|
603
|
+
|
|
555
604
|
// Step 10: Get reviewers from CODEOWNERS and config
|
|
556
605
|
logger.debug('create-pr', 'Step 10: Getting reviewers from CODEOWNERS and config');
|
|
557
606
|
const reviewers = await getReviewersForFiles(filesArray, config.github?.pr);
|
|
@@ -563,7 +612,7 @@ export async function runCreatePr(args) {
|
|
|
563
612
|
// Step 11: Show PR preview
|
|
564
613
|
const prData = {
|
|
565
614
|
title: prTitle,
|
|
566
|
-
body:
|
|
615
|
+
body: finalBody,
|
|
567
616
|
head: currentBranch,
|
|
568
617
|
base: baseBranch,
|
|
569
618
|
labels,
|
|
@@ -572,6 +621,16 @@ export async function runCreatePr(args) {
|
|
|
572
621
|
|
|
573
622
|
showPRPreview(prData);
|
|
574
623
|
|
|
624
|
+
let strategyLabel;
|
|
625
|
+
if (mergeStrategy === 'squash') {
|
|
626
|
+
strategyLabel = 'SQUASH MERGE';
|
|
627
|
+
} else if (mergeStrategy === 'merge-commit') {
|
|
628
|
+
strategyLabel = 'MERGE COMMIT';
|
|
629
|
+
} else {
|
|
630
|
+
strategyLabel = 'UNKNOWN';
|
|
631
|
+
}
|
|
632
|
+
showInfo(`Merge strategy: ${strategyLabel} (${currentBranch} → ${baseBranch})`);
|
|
633
|
+
|
|
575
634
|
// Step 12: Prompt for confirmation
|
|
576
635
|
const action = await promptMenu(
|
|
577
636
|
'What would you like to do?',
|
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: create-release.js
|
|
3
|
+
* Purpose: create-release command — release-candidate branch creation from develop
|
|
4
|
+
*
|
|
5
|
+
* Workflow:
|
|
6
|
+
* 1. Validate preconditions (clean tree, on develop, up-to-date, no existing RC)
|
|
7
|
+
* 2. Discover version files — abort on mismatch
|
|
8
|
+
* 3. Calculate next version
|
|
9
|
+
* 4. [dry-run] Preview + return
|
|
10
|
+
* 5. Confirm with user
|
|
11
|
+
* 6. Create RC branch from develop
|
|
12
|
+
* 7. Bump version files + commit (--no-verify)
|
|
13
|
+
* 8. [optional] Generate CHANGELOG
|
|
14
|
+
* 9. Create Git tag (skip if already exists)
|
|
15
|
+
* 10. Push RC branch (unless --skip-push)
|
|
16
|
+
* 11. Deploy to shadow (unless --no-shadow or --skip-push)
|
|
17
|
+
* 12. Return to RC branch
|
|
18
|
+
* 13. Display summary
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { execSync } from 'child_process';
|
|
22
|
+
import fs from 'fs';
|
|
23
|
+
import path from 'path';
|
|
24
|
+
import {
|
|
25
|
+
discoverVersionFiles,
|
|
26
|
+
incrementVersion,
|
|
27
|
+
updateVersionFiles
|
|
28
|
+
} from '../utils/version-manager.js';
|
|
29
|
+
import { createTag, tagExists, formatTagName } from '../utils/git-tag-manager.js';
|
|
30
|
+
import {
|
|
31
|
+
generateChangelogEntry,
|
|
32
|
+
updateChangelogFile,
|
|
33
|
+
selectChangelogFile
|
|
34
|
+
} from '../utils/changelog-generator.js';
|
|
35
|
+
import {
|
|
36
|
+
getRepoRoot,
|
|
37
|
+
getCurrentBranch,
|
|
38
|
+
verifyRemoteExists,
|
|
39
|
+
getRemoteName,
|
|
40
|
+
fetchRemote,
|
|
41
|
+
getDivergence,
|
|
42
|
+
getRemoteBranches,
|
|
43
|
+
isWorkingDirectoryClean,
|
|
44
|
+
checkoutBranch,
|
|
45
|
+
pushBranch,
|
|
46
|
+
stageFiles,
|
|
47
|
+
createCommit
|
|
48
|
+
} from '../utils/git-operations.js';
|
|
49
|
+
import { runShadow } from './shadow.js';
|
|
50
|
+
import { getConfig } from '../config.js';
|
|
51
|
+
import {
|
|
52
|
+
showInfo,
|
|
53
|
+
showSuccess,
|
|
54
|
+
showError,
|
|
55
|
+
showWarning,
|
|
56
|
+
promptConfirmation
|
|
57
|
+
} from '../utils/interactive-ui.js';
|
|
58
|
+
import logger from '../utils/logger.js';
|
|
59
|
+
import { colors, error, checkGitRepo } from './helpers.js';
|
|
60
|
+
|
|
61
|
+
/** Source branch for RC creation */
|
|
62
|
+
const SOURCE_BRANCH = 'develop';
|
|
63
|
+
|
|
64
|
+
/** RC branch prefix */
|
|
65
|
+
const RC_PREFIX = 'release-candidate';
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Parses command line arguments
|
|
69
|
+
*
|
|
70
|
+
* @param {string[]} args - CLI arguments
|
|
71
|
+
* @returns {Object} Parsed options
|
|
72
|
+
*/
|
|
73
|
+
function parseArguments(args) {
|
|
74
|
+
const parsed = {
|
|
75
|
+
bumpType: null,
|
|
76
|
+
noShadow: false,
|
|
77
|
+
dryRun: false,
|
|
78
|
+
skipPush: false,
|
|
79
|
+
updateChangelog: false
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
if (args.length === 0) return parsed;
|
|
83
|
+
|
|
84
|
+
let i = 0;
|
|
85
|
+
const firstArg = args[0].toLowerCase();
|
|
86
|
+
if (['major', 'minor', 'patch'].includes(firstArg)) {
|
|
87
|
+
parsed.bumpType = firstArg;
|
|
88
|
+
i = 1;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (; i < args.length; i++) {
|
|
92
|
+
const arg = args[i];
|
|
93
|
+
if (arg === '--no-shadow') parsed.noShadow = true;
|
|
94
|
+
else if (arg === '--dry-run') parsed.dryRun = true;
|
|
95
|
+
else if (arg === '--skip-push') parsed.skipPush = true;
|
|
96
|
+
else if (arg === '--update-changelog') parsed.updateChangelog = true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return parsed;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Validates preconditions before creating a release-candidate
|
|
104
|
+
* Checks: git repo, clean tree, on develop, develop up-to-date, develop not behind main, no existing RC
|
|
105
|
+
*
|
|
106
|
+
* @returns {Promise<{ valid: boolean, errors: Array<{ type: string, message: string, fix?: string[] }> }>}
|
|
107
|
+
*/
|
|
108
|
+
async function validatePreconditions() {
|
|
109
|
+
const errors = [];
|
|
110
|
+
|
|
111
|
+
if (!checkGitRepo()) {
|
|
112
|
+
errors.push({ type: 'fatal', message: 'Not a git repository' });
|
|
113
|
+
return { valid: false, errors };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!isWorkingDirectoryClean()) {
|
|
117
|
+
errors.push({
|
|
118
|
+
type: 'fatal',
|
|
119
|
+
message: 'Working directory has uncommitted changes',
|
|
120
|
+
fix: [
|
|
121
|
+
'Commit your changes: git add . && git commit -m "..."',
|
|
122
|
+
'Or stash them: git stash'
|
|
123
|
+
]
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const currentBranch = getCurrentBranch();
|
|
128
|
+
if (currentBranch !== SOURCE_BRANCH) {
|
|
129
|
+
errors.push({
|
|
130
|
+
type: 'fatal',
|
|
131
|
+
message: `Must be on '${SOURCE_BRANCH}' branch (currently on '${currentBranch}')`,
|
|
132
|
+
fix: [`git checkout ${SOURCE_BRANCH}`]
|
|
133
|
+
});
|
|
134
|
+
// Return early: divergence checks require being on develop
|
|
135
|
+
return { valid: false, errors };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
fetchRemote();
|
|
140
|
+
} catch (e) {
|
|
141
|
+
errors.push({
|
|
142
|
+
type: 'warn',
|
|
143
|
+
message: `Could not fetch from remote: ${e.message} — results may be stale`
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const devSync = getDivergence(SOURCE_BRANCH, `origin/${SOURCE_BRANCH}`);
|
|
149
|
+
if (devSync.behind > 0) {
|
|
150
|
+
errors.push({
|
|
151
|
+
type: 'fatal',
|
|
152
|
+
message: `Local '${SOURCE_BRANCH}' is ${devSync.behind} commit(s) behind remote`,
|
|
153
|
+
fix: [`git pull origin ${SOURCE_BRANCH}`]
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
} catch (e) {
|
|
157
|
+
errors.push({
|
|
158
|
+
type: 'warn',
|
|
159
|
+
message: `Could not check if ${SOURCE_BRANCH} is up-to-date with remote: ${e.message}`
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const mainSync = getDivergence(`origin/${SOURCE_BRANCH}`, 'origin/main');
|
|
165
|
+
if (mainSync.behind > 0) {
|
|
166
|
+
errors.push({
|
|
167
|
+
type: 'fatal',
|
|
168
|
+
message: `'${SOURCE_BRANCH}' is missing ${mainSync.behind} commit(s) from 'main' — a back-merge is required`,
|
|
169
|
+
fix: [
|
|
170
|
+
'Merge main into develop first:',
|
|
171
|
+
` git checkout ${SOURCE_BRANCH} && git merge origin/main`,
|
|
172
|
+
' or: claude-hooks back-merge (when available)'
|
|
173
|
+
]
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
} catch (e) {
|
|
177
|
+
errors.push({
|
|
178
|
+
type: 'warn',
|
|
179
|
+
message: `Could not verify ${SOURCE_BRANCH} vs main sync: ${e.message}`
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const remoteBranches = getRemoteBranches();
|
|
184
|
+
const existingRCs = remoteBranches.filter((b) => b.startsWith(`${RC_PREFIX}/`));
|
|
185
|
+
if (existingRCs.length > 0) {
|
|
186
|
+
errors.push({
|
|
187
|
+
type: 'fatal',
|
|
188
|
+
message: `Existing release-candidate branch found: ${existingRCs.join(', ')}`,
|
|
189
|
+
fix: [
|
|
190
|
+
'Close the active release-candidate first:',
|
|
191
|
+
' claude-hooks close-release (when available)',
|
|
192
|
+
` or delete manually: git push origin --delete ${existingRCs[0]}`
|
|
193
|
+
]
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const remoteName = getRemoteName();
|
|
198
|
+
if (!verifyRemoteExists(remoteName)) {
|
|
199
|
+
errors.push({
|
|
200
|
+
type: 'fatal',
|
|
201
|
+
message: `Remote '${remoteName}' does not exist`,
|
|
202
|
+
fix: ['git remote add origin <url>']
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const fatalErrors = errors.filter((e) => e.type === 'fatal');
|
|
207
|
+
return { valid: fatalErrors.length === 0, errors };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Displays a dry-run preview of what would be executed
|
|
212
|
+
*
|
|
213
|
+
* @param {Object} info - Preview info
|
|
214
|
+
*/
|
|
215
|
+
function showDryRunPreview({ bumpType, currentVersion, nextVersion, rcBranch, options }) {
|
|
216
|
+
const tagName = formatTagName(nextVersion);
|
|
217
|
+
|
|
218
|
+
console.log('');
|
|
219
|
+
console.log(`${colors.yellow}═════════════════════════════════════════════════${colors.reset}`);
|
|
220
|
+
console.log(`${colors.yellow} DRY RUN — No changes will be made ${colors.reset}`);
|
|
221
|
+
console.log(`${colors.yellow}═════════════════════════════════════════════════${colors.reset}`);
|
|
222
|
+
console.log('');
|
|
223
|
+
console.log(`${colors.blue}Operation:${colors.reset} Bump ${bumpType}`);
|
|
224
|
+
console.log(`${colors.blue}Current version:${colors.reset} ${currentVersion}`);
|
|
225
|
+
console.log(`${colors.green}New version:${colors.reset} ${nextVersion}`);
|
|
226
|
+
console.log(`${colors.blue}Branch:${colors.reset} ${rcBranch}`);
|
|
227
|
+
console.log(`${colors.blue}Tag:${colors.reset} ${tagName}`);
|
|
228
|
+
console.log(
|
|
229
|
+
`${colors.blue}Push:${colors.reset} ${options.skipPush ? 'skipped (--skip-push)' : 'yes → origin'}`
|
|
230
|
+
);
|
|
231
|
+
console.log(
|
|
232
|
+
`${colors.blue}Shadow sync:${colors.reset} ${
|
|
233
|
+
options.noShadow
|
|
234
|
+
? 'skipped (--no-shadow)'
|
|
235
|
+
: options.skipPush
|
|
236
|
+
? 'skipped (branch not pushed)'
|
|
237
|
+
: 'yes'
|
|
238
|
+
}`
|
|
239
|
+
);
|
|
240
|
+
if (options.updateChangelog) {
|
|
241
|
+
console.log(`${colors.blue}CHANGELOG:${colors.reset} will be generated`);
|
|
242
|
+
}
|
|
243
|
+
console.log('');
|
|
244
|
+
console.log(`${colors.yellow}Run without --dry-run to apply these changes${colors.reset}`);
|
|
245
|
+
console.log('');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Restores version files from a snapshot map (rollback)
|
|
250
|
+
*
|
|
251
|
+
* @param {Map<string, string>} snapshot - Map of filePath → original content
|
|
252
|
+
*/
|
|
253
|
+
function restoreSnapshot(snapshot) {
|
|
254
|
+
snapshot.forEach((content, filePath) => {
|
|
255
|
+
try {
|
|
256
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
257
|
+
} catch (e) {
|
|
258
|
+
logger.error('create-release', 'Rollback failed during snapshot restoration', {
|
|
259
|
+
path: filePath,
|
|
260
|
+
operation: 'restoreSnapshot',
|
|
261
|
+
error: e.message
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* create-release command
|
|
269
|
+
* Creates a release-candidate branch from develop, bumps version, and deploys to shadow.
|
|
270
|
+
*
|
|
271
|
+
* @param {string[]} args - CLI arguments
|
|
272
|
+
*/
|
|
273
|
+
export async function runCreateRelease(args) {
|
|
274
|
+
logger.debug('create-release', 'Starting create-release command', { args });
|
|
275
|
+
|
|
276
|
+
const options = parseArguments(args);
|
|
277
|
+
|
|
278
|
+
if (!options.bumpType) {
|
|
279
|
+
error('Usage: claude-hooks create-release <major|minor|patch> [options]');
|
|
280
|
+
console.log('');
|
|
281
|
+
console.log('Options:');
|
|
282
|
+
console.log(' --no-shadow Skip shadow deployment');
|
|
283
|
+
console.log(' --dry-run Preview changes without executing');
|
|
284
|
+
console.log(' --skip-push Create branch locally only (also skips shadow)');
|
|
285
|
+
console.log(' --update-changelog Generate CHANGELOG entry');
|
|
286
|
+
console.log('');
|
|
287
|
+
console.log('Examples:');
|
|
288
|
+
console.log(' claude-hooks create-release minor');
|
|
289
|
+
console.log(' claude-hooks create-release minor --no-shadow');
|
|
290
|
+
console.log(' claude-hooks create-release minor --dry-run');
|
|
291
|
+
console.log(' claude-hooks create-release patch --update-changelog');
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
showInfo(`🚀 Creating release-candidate (${options.bumpType})...`);
|
|
296
|
+
console.log('');
|
|
297
|
+
|
|
298
|
+
// Step 1: Validate preconditions
|
|
299
|
+
logger.debug('create-release', 'Step 1: Validating preconditions');
|
|
300
|
+
const { valid, errors } = await validatePreconditions();
|
|
301
|
+
|
|
302
|
+
errors.filter((e) => e.type === 'warn').forEach((e) => showWarning(e.message));
|
|
303
|
+
|
|
304
|
+
if (!valid) {
|
|
305
|
+
const fatalErrors = errors.filter((e) => e.type === 'fatal');
|
|
306
|
+
showError('Precondition check failed:');
|
|
307
|
+
console.log('');
|
|
308
|
+
fatalErrors.forEach((e) => {
|
|
309
|
+
console.log(` ❌ ${e.message}`);
|
|
310
|
+
if (e.fix) {
|
|
311
|
+
e.fix.forEach((line) => console.log(` ${line}`));
|
|
312
|
+
}
|
|
313
|
+
console.log('');
|
|
314
|
+
});
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
showSuccess('Preconditions validated');
|
|
319
|
+
console.log('');
|
|
320
|
+
|
|
321
|
+
// Step 2: Discover version files
|
|
322
|
+
logger.debug('create-release', 'Step 2: Discovering version files');
|
|
323
|
+
const discovery = discoverVersionFiles();
|
|
324
|
+
|
|
325
|
+
if (discovery.files.length === 0) {
|
|
326
|
+
showError('No version files found');
|
|
327
|
+
console.log('');
|
|
328
|
+
console.log('This command requires at least one version file (package.json, pom.xml, etc.)');
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (discovery.mismatch) {
|
|
333
|
+
showError('Version mismatch detected — cannot proceed automatically');
|
|
334
|
+
console.log('');
|
|
335
|
+
console.log(` ${'File'.padEnd(40)} ${'Type'.padEnd(18)} Version`);
|
|
336
|
+
console.log(` ${'-'.repeat(40)} ${'-'.repeat(18)} -------`);
|
|
337
|
+
discovery.files.forEach((f) => {
|
|
338
|
+
const marker = f.version !== discovery.resolvedVersion ? '⚠️ ' : ' ';
|
|
339
|
+
console.log(
|
|
340
|
+
` ${marker}${f.relativePath.padEnd(37)} ${f.projectLabel.padEnd(18)} ${f.version || 'N/A'}`
|
|
341
|
+
);
|
|
342
|
+
});
|
|
343
|
+
console.log('');
|
|
344
|
+
console.log('Fix: align all version files to the same version, then re-run.');
|
|
345
|
+
console.log(' 1. Edit version files manually to match');
|
|
346
|
+
console.log(
|
|
347
|
+
' 2. Commit: git add . && git commit -m "chore(version): align versions"'
|
|
348
|
+
);
|
|
349
|
+
console.log(` 3. Re-run: claude-hooks create-release ${options.bumpType}`);
|
|
350
|
+
process.exit(1);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const currentVersion = discovery.resolvedVersion;
|
|
354
|
+
if (!currentVersion) {
|
|
355
|
+
showError('Could not determine current version from discovered files');
|
|
356
|
+
process.exit(1);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Step 3: Calculate next version
|
|
360
|
+
const nextVersion = incrementVersion(currentVersion, options.bumpType);
|
|
361
|
+
const rcBranch = `${RC_PREFIX}/V${nextVersion}`;
|
|
362
|
+
const tagName = formatTagName(nextVersion);
|
|
363
|
+
|
|
364
|
+
showInfo(`Current version: ${currentVersion}`);
|
|
365
|
+
showSuccess(`Next version: ${nextVersion}`);
|
|
366
|
+
showInfo(`Branch: ${rcBranch}`);
|
|
367
|
+
console.log('');
|
|
368
|
+
|
|
369
|
+
// Step 4: Dry-run
|
|
370
|
+
if (options.dryRun) {
|
|
371
|
+
showDryRunPreview({ bumpType: options.bumpType, currentVersion, nextVersion, rcBranch, options });
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Step 5: Confirm with user
|
|
376
|
+
const confirmed = await promptConfirmation(
|
|
377
|
+
`Create ${rcBranch} and bump version to ${nextVersion}?`,
|
|
378
|
+
true
|
|
379
|
+
);
|
|
380
|
+
if (!confirmed) {
|
|
381
|
+
showInfo('Release creation cancelled');
|
|
382
|
+
process.exit(0);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
console.log('');
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
const config = await getConfig();
|
|
389
|
+
const selectedFiles = discovery.files.filter((f) => f.selected);
|
|
390
|
+
|
|
391
|
+
// Step 6: Create RC branch from develop
|
|
392
|
+
logger.debug('create-release', 'Step 6: Creating RC branch', { rcBranch });
|
|
393
|
+
showInfo(`Creating branch ${rcBranch} from ${SOURCE_BRANCH}...`);
|
|
394
|
+
checkoutBranch(rcBranch, { create: true, startPoint: SOURCE_BRANCH });
|
|
395
|
+
showSuccess(`✓ Branch created: ${rcBranch}`);
|
|
396
|
+
console.log('');
|
|
397
|
+
|
|
398
|
+
// Snapshot for rollback
|
|
399
|
+
const snapshot = new Map();
|
|
400
|
+
selectedFiles.forEach((file) => {
|
|
401
|
+
try {
|
|
402
|
+
snapshot.set(file.path, fs.readFileSync(file.path, 'utf8'));
|
|
403
|
+
} catch (e) {
|
|
404
|
+
logger.error('create-release', 'Failed to snapshot file', {
|
|
405
|
+
path: file.path,
|
|
406
|
+
error: e.message
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// Step 7: Bump version files
|
|
412
|
+
logger.debug('create-release', 'Step 7: Bumping version files');
|
|
413
|
+
showInfo('Updating version files...');
|
|
414
|
+
try {
|
|
415
|
+
updateVersionFiles(selectedFiles, nextVersion);
|
|
416
|
+
selectedFiles.forEach((f) =>
|
|
417
|
+
showSuccess(`✓ Updated ${f.relativePath} → ${nextVersion}`)
|
|
418
|
+
);
|
|
419
|
+
console.log('');
|
|
420
|
+
} catch (e) {
|
|
421
|
+
showError(`Failed to update version files: ${e.message}`);
|
|
422
|
+
showWarning('Rolling back version changes and deleting branch...');
|
|
423
|
+
restoreSnapshot(snapshot);
|
|
424
|
+
try {
|
|
425
|
+
checkoutBranch(SOURCE_BRANCH);
|
|
426
|
+
execSync(`git branch -D "${rcBranch}"`, {
|
|
427
|
+
encoding: 'utf8',
|
|
428
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
429
|
+
});
|
|
430
|
+
} catch {
|
|
431
|
+
// best-effort cleanup
|
|
432
|
+
}
|
|
433
|
+
process.exit(1);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Step 8: Optional CHANGELOG
|
|
437
|
+
let selectedChangelogPath = null;
|
|
438
|
+
if (options.updateChangelog) {
|
|
439
|
+
logger.debug('create-release', 'Step 8: Generating CHANGELOG');
|
|
440
|
+
showInfo('Generating CHANGELOG entry...');
|
|
441
|
+
const changelogResult = await generateChangelogEntry({
|
|
442
|
+
version: nextVersion,
|
|
443
|
+
isReleaseVersion: true,
|
|
444
|
+
baseBranch: 'main',
|
|
445
|
+
config
|
|
446
|
+
});
|
|
447
|
+
if (changelogResult.content) {
|
|
448
|
+
selectedChangelogPath = await selectChangelogFile();
|
|
449
|
+
const updated = updateChangelogFile(changelogResult.content, selectedChangelogPath);
|
|
450
|
+
if (updated) {
|
|
451
|
+
showSuccess('✓ CHANGELOG.md updated');
|
|
452
|
+
} else {
|
|
453
|
+
showWarning('⚠ Failed to update CHANGELOG.md');
|
|
454
|
+
}
|
|
455
|
+
} else {
|
|
456
|
+
showWarning('⚠ No commits found for CHANGELOG');
|
|
457
|
+
}
|
|
458
|
+
console.log('');
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Stage and commit (--no-verify)
|
|
462
|
+
logger.debug('create-release', 'Staging and committing version bump');
|
|
463
|
+
showInfo('Staging and committing...');
|
|
464
|
+
const filesToStage = selectedFiles.map((f) => f.path);
|
|
465
|
+
if (options.updateChangelog) {
|
|
466
|
+
const changelogPath =
|
|
467
|
+
selectedChangelogPath || path.join(getRepoRoot(), 'CHANGELOG.md');
|
|
468
|
+
if (fs.existsSync(changelogPath)) {
|
|
469
|
+
filesToStage.push(changelogPath);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const stageResult = stageFiles(filesToStage);
|
|
474
|
+
if (!stageResult.success) {
|
|
475
|
+
showError(`Failed to stage files: ${stageResult.error}`);
|
|
476
|
+
showWarning('Rolling back...');
|
|
477
|
+
restoreSnapshot(snapshot);
|
|
478
|
+
process.exit(1);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const commitResult = createCommit(`chore(version): bump to ${nextVersion}`, {
|
|
482
|
+
noVerify: true
|
|
483
|
+
});
|
|
484
|
+
if (!commitResult.success) {
|
|
485
|
+
showError(`Failed to create commit: ${commitResult.error}`);
|
|
486
|
+
showWarning('Rolling back...');
|
|
487
|
+
restoreSnapshot(snapshot);
|
|
488
|
+
try {
|
|
489
|
+
execSync('git reset HEAD', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
490
|
+
} catch {
|
|
491
|
+
// best-effort
|
|
492
|
+
}
|
|
493
|
+
process.exit(1);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
showSuccess('✓ Version bump committed');
|
|
497
|
+
console.log('');
|
|
498
|
+
|
|
499
|
+
// Step 9: Create Git tag (skip silently if already exists)
|
|
500
|
+
logger.debug('create-release', 'Step 9: Creating Git tag', { tagName });
|
|
501
|
+
const tagAlreadyExists = await tagExists(tagName, 'local');
|
|
502
|
+
if (tagAlreadyExists) {
|
|
503
|
+
showWarning(`Tag ${tagName} already exists locally — skipping tag creation`);
|
|
504
|
+
} else {
|
|
505
|
+
const tagResult = createTag(nextVersion, `Release version ${nextVersion}`);
|
|
506
|
+
if (tagResult.success) {
|
|
507
|
+
showSuccess(`✓ Tag created: ${tagName}`);
|
|
508
|
+
} else {
|
|
509
|
+
showWarning(`⚠ Failed to create tag: ${tagResult.error}`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
console.log('');
|
|
513
|
+
|
|
514
|
+
// Step 10: Push RC branch (unless --skip-push)
|
|
515
|
+
let pushStatus = 'skipped (--skip-push)';
|
|
516
|
+
if (!options.skipPush) {
|
|
517
|
+
showInfo(`Pushing ${rcBranch} to remote...`);
|
|
518
|
+
const pushResult = pushBranch(rcBranch, { setUpstream: true });
|
|
519
|
+
if (pushResult.success) {
|
|
520
|
+
showSuccess(`✓ Pushed ${rcBranch} to remote`);
|
|
521
|
+
pushStatus = 'pushed';
|
|
522
|
+
} else {
|
|
523
|
+
showError(`Failed to push branch: ${pushResult.error}`);
|
|
524
|
+
console.log('');
|
|
525
|
+
console.log('You can push manually:');
|
|
526
|
+
console.log(` git push -u origin ${rcBranch}`);
|
|
527
|
+
pushStatus = 'failed';
|
|
528
|
+
}
|
|
529
|
+
console.log('');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Step 11: Deploy to shadow (unless --no-shadow or --skip-push)
|
|
533
|
+
let shadowStatus = 'skipped';
|
|
534
|
+
if (options.noShadow) {
|
|
535
|
+
showInfo('Shadow deployment skipped (--no-shadow)');
|
|
536
|
+
shadowStatus = 'skipped (--no-shadow)';
|
|
537
|
+
console.log('');
|
|
538
|
+
} else if (options.skipPush || pushStatus === 'failed') {
|
|
539
|
+
showInfo('Shadow deployment skipped — branch not pushed to remote');
|
|
540
|
+
shadowStatus = 'skipped (branch not pushed)';
|
|
541
|
+
console.log('');
|
|
542
|
+
} else {
|
|
543
|
+
showInfo('Deploying to shadow...');
|
|
544
|
+
console.log('');
|
|
545
|
+
try {
|
|
546
|
+
await runShadow(['sync', rcBranch]);
|
|
547
|
+
shadowStatus = 'synced';
|
|
548
|
+
} catch (e) {
|
|
549
|
+
showWarning(`Shadow sync encountered an error: ${e.message}`);
|
|
550
|
+
shadowStatus = 'failed';
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Step 12: Ensure we land on RC branch
|
|
555
|
+
const currentBranchAfter = getCurrentBranch();
|
|
556
|
+
if (currentBranchAfter !== rcBranch) {
|
|
557
|
+
try {
|
|
558
|
+
checkoutBranch(rcBranch);
|
|
559
|
+
} catch (e) {
|
|
560
|
+
showWarning(`Could not return to ${rcBranch}: ${e.message}`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Step 13: Summary
|
|
565
|
+
console.log('');
|
|
566
|
+
console.log(
|
|
567
|
+
`${colors.green}════════════════════════════════════════════════════${colors.reset}`
|
|
568
|
+
);
|
|
569
|
+
console.log(
|
|
570
|
+
`${colors.green} Release Candidate Created ✅ ${colors.reset}`
|
|
571
|
+
);
|
|
572
|
+
console.log(
|
|
573
|
+
`${colors.green}════════════════════════════════════════════════════${colors.reset}`
|
|
574
|
+
);
|
|
575
|
+
console.log('');
|
|
576
|
+
console.log(`${colors.blue}Branch:${colors.reset} ${rcBranch}`);
|
|
577
|
+
console.log(`${colors.blue}Version:${colors.reset} ${nextVersion}`);
|
|
578
|
+
console.log(
|
|
579
|
+
`${colors.blue}Tag:${colors.reset} ${tagAlreadyExists ? `${tagName} (pre-existing, skipped)` : tagName}`
|
|
580
|
+
);
|
|
581
|
+
console.log(`${colors.blue}Push:${colors.reset} ${pushStatus}`);
|
|
582
|
+
console.log(`${colors.blue}Shadow:${colors.reset} ${shadowStatus}`);
|
|
583
|
+
console.log('');
|
|
584
|
+
|
|
585
|
+
if (options.skipPush) {
|
|
586
|
+
console.log('Next steps:');
|
|
587
|
+
console.log(` 1. Push when ready: git push -u origin ${rcBranch}`);
|
|
588
|
+
console.log(` 2. Sync shadow: claude-hooks shadow sync ${rcBranch}`);
|
|
589
|
+
} else {
|
|
590
|
+
console.log('Next steps:');
|
|
591
|
+
console.log(' 1. Verify shadow deployment is running');
|
|
592
|
+
console.log(' 2. Run QA on shadow environment');
|
|
593
|
+
}
|
|
594
|
+
console.log('');
|
|
595
|
+
} catch (e) {
|
|
596
|
+
logger.error('create-release', 'Command failed unexpectedly', e);
|
|
597
|
+
showError(`create-release failed: ${e.message}`);
|
|
598
|
+
process.exit(1);
|
|
599
|
+
}
|
|
600
|
+
}
|