just-ship-it 0.0.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/LICENSE +201 -0
- package/README.md +431 -0
- package/bin/just-ship-it.js +2 -0
- package/dist/ai.d.ts +20 -0
- package/dist/ai.d.ts.map +1 -0
- package/dist/ai.js +210 -0
- package/dist/ai.js.map +1 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +49 -0
- package/dist/config.js.map +1 -0
- package/dist/git.d.ts +33 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +153 -0
- package/dist/git.js.map +1 -0
- package/dist/github.d.ts +22 -0
- package/dist/github.d.ts.map +1 -0
- package/dist/github.js +237 -0
- package/dist/github.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +187 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +17 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +54 -0
- package/dist/logger.js.map +1 -0
- package/dist/release.d.ts +3 -0
- package/dist/release.d.ts.map +1 -0
- package/dist/release.js +752 -0
- package/dist/release.js.map +1 -0
- package/dist/types.d.ts +67 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/workspace.d.ts +19 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +185 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +44 -0
package/dist/release.js
ADDED
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
import { confirm, select, text, isCancel, cancel } from '@clack/prompts';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { GitOperations } from './git.js';
|
|
4
|
+
import { GitHubOperations } from './github.js';
|
|
5
|
+
import { logger } from './logger.js';
|
|
6
|
+
import { generateChangesetMessage, suggestReleaseType, analyzePackageChange, generateMultiPackageChangesetMessage } from './ai.js';
|
|
7
|
+
import { analyzePackageChanges, findMaxVersionType } from './workspace.js';
|
|
8
|
+
const TOTAL_STEPS = 10;
|
|
9
|
+
export async function runRelease(config, options = {}) {
|
|
10
|
+
logger.header(`Release: ${config.repo}`);
|
|
11
|
+
const git = new GitOperations(config);
|
|
12
|
+
const github = new GitHubOperations(config);
|
|
13
|
+
let spinner = null;
|
|
14
|
+
try {
|
|
15
|
+
// Clone repository first to detect workspace type
|
|
16
|
+
logger.step(1, TOTAL_STEPS, 'Cloning repository from main...');
|
|
17
|
+
spinner = ora('Cloning...').start();
|
|
18
|
+
await git.clone();
|
|
19
|
+
spinner.succeed('Repository cloned');
|
|
20
|
+
// Detect workspace type
|
|
21
|
+
const workspace = await git.getWorkspaceInfo();
|
|
22
|
+
logger.detail(`Workspace type: ${workspace.type}`);
|
|
23
|
+
if (workspace.type === 'single') {
|
|
24
|
+
return runSinglePackageRelease(git, github, config, options);
|
|
25
|
+
}
|
|
26
|
+
return runMonorepoRelease(workspace, git, github, config, options);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
spinner?.fail();
|
|
30
|
+
logger.error(`Release failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
31
|
+
try {
|
|
32
|
+
await git.cleanup();
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Ignore cleanup errors
|
|
36
|
+
}
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Single package release flow (original logic)
|
|
42
|
+
*/
|
|
43
|
+
async function runSinglePackageRelease(git, github, config, options = {}) {
|
|
44
|
+
let spinner = null;
|
|
45
|
+
try {
|
|
46
|
+
// Step 1 already done (clone)
|
|
47
|
+
const packageName = await git.getPackageName();
|
|
48
|
+
const currentVersion = await git.getPackageVersion();
|
|
49
|
+
logger.detail(`Package: ${packageName} @ ${currentVersion}`);
|
|
50
|
+
// Find the latest version tag and get diff
|
|
51
|
+
spinner = ora('Finding latest version tag...').start();
|
|
52
|
+
const latestTag = await git.getLatestVersionTag();
|
|
53
|
+
if (!latestTag) {
|
|
54
|
+
spinner.fail('No version tags found');
|
|
55
|
+
logger.warn('Cannot find previous version tag. Using recent commits instead.');
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
spinner.succeed(`Latest version: ${latestTag}`);
|
|
59
|
+
}
|
|
60
|
+
// Build diff context
|
|
61
|
+
let diffContext;
|
|
62
|
+
if (latestTag) {
|
|
63
|
+
spinner = ora('Analyzing changes since last release...').start();
|
|
64
|
+
const [commits, diffSummary, diff] = await Promise.all([
|
|
65
|
+
git.getCommitsSinceTag(latestTag),
|
|
66
|
+
git.getDiffSummary(latestTag),
|
|
67
|
+
git.getFullDiffSinceTag(latestTag),
|
|
68
|
+
]);
|
|
69
|
+
spinner.succeed(`Found ${commits.length} commits, ${diffSummary.files.length} files changed`);
|
|
70
|
+
diffContext = {
|
|
71
|
+
commits,
|
|
72
|
+
diff,
|
|
73
|
+
filesChanged: diffSummary.files,
|
|
74
|
+
insertions: diffSummary.insertions,
|
|
75
|
+
deletions: diffSummary.deletions,
|
|
76
|
+
previousVersion: latestTag,
|
|
77
|
+
};
|
|
78
|
+
logger.detail(`Changes: +${diffContext.insertions}/-${diffContext.deletions} lines`);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
// Fallback to recent commits if no tag found
|
|
82
|
+
const recentCommits = await git.getRecentCommits(15);
|
|
83
|
+
diffContext = {
|
|
84
|
+
commits: recentCommits,
|
|
85
|
+
diff: '',
|
|
86
|
+
filesChanged: [],
|
|
87
|
+
insertions: 0,
|
|
88
|
+
deletions: 0,
|
|
89
|
+
previousVersion: 'unknown',
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// Determine release type
|
|
93
|
+
let releaseType;
|
|
94
|
+
if (options.type) {
|
|
95
|
+
releaseType = options.type;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
// Use AI to suggest release type based on diff
|
|
99
|
+
spinner = ora('Analyzing changes...').start();
|
|
100
|
+
const suggestedType = await suggestReleaseType(diffContext);
|
|
101
|
+
spinner.succeed(`AI suggests: ${suggestedType} release`);
|
|
102
|
+
const selectedType = await select({
|
|
103
|
+
message: 'What type of release is this?',
|
|
104
|
+
options: [
|
|
105
|
+
{ label: 'patch - Bug fixes, small changes', value: 'patch' },
|
|
106
|
+
{ label: 'minor - New features, backwards compatible', value: 'minor' },
|
|
107
|
+
{ label: 'major - Breaking changes', value: 'major' },
|
|
108
|
+
],
|
|
109
|
+
initialValue: suggestedType,
|
|
110
|
+
});
|
|
111
|
+
if (isCancel(selectedType)) {
|
|
112
|
+
cancel('Release cancelled');
|
|
113
|
+
await git.cleanup();
|
|
114
|
+
process.exit(0);
|
|
115
|
+
}
|
|
116
|
+
releaseType = selectedType;
|
|
117
|
+
}
|
|
118
|
+
// Generate release message with AI
|
|
119
|
+
let releaseMessage;
|
|
120
|
+
if (options.message) {
|
|
121
|
+
releaseMessage = options.message;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
spinner = ora('Generating changeset description with AI...').start();
|
|
125
|
+
try {
|
|
126
|
+
const aiMessage = await generateChangesetMessage(packageName, releaseType, diffContext);
|
|
127
|
+
spinner.succeed('AI generated description');
|
|
128
|
+
logger.blank();
|
|
129
|
+
logger.info('AI-generated changeset description:');
|
|
130
|
+
logger.divider();
|
|
131
|
+
console.log(aiMessage);
|
|
132
|
+
logger.divider();
|
|
133
|
+
logger.blank();
|
|
134
|
+
const useAiMessage = await confirm({
|
|
135
|
+
message: 'Use this description?',
|
|
136
|
+
initialValue: true,
|
|
137
|
+
});
|
|
138
|
+
if (isCancel(useAiMessage)) {
|
|
139
|
+
cancel('Release cancelled');
|
|
140
|
+
await git.cleanup();
|
|
141
|
+
process.exit(0);
|
|
142
|
+
}
|
|
143
|
+
if (useAiMessage) {
|
|
144
|
+
releaseMessage = aiMessage;
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
const customMessage = await text({
|
|
148
|
+
message: 'Enter your own description:',
|
|
149
|
+
validate: (value) => {
|
|
150
|
+
if (!value || value.length === 0)
|
|
151
|
+
return 'Message is required';
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
if (isCancel(customMessage)) {
|
|
155
|
+
cancel('Release cancelled');
|
|
156
|
+
await git.cleanup();
|
|
157
|
+
process.exit(0);
|
|
158
|
+
}
|
|
159
|
+
releaseMessage = customMessage;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
spinner.fail('AI generation failed');
|
|
164
|
+
logger.warn('Falling back to manual input');
|
|
165
|
+
const fallbackMessage = await text({
|
|
166
|
+
message: 'Describe the changes for this release:',
|
|
167
|
+
validate: (value) => {
|
|
168
|
+
if (!value || value.length === 0)
|
|
169
|
+
return 'Message is required';
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
if (isCancel(fallbackMessage)) {
|
|
173
|
+
cancel('Release cancelled');
|
|
174
|
+
await git.cleanup();
|
|
175
|
+
process.exit(0);
|
|
176
|
+
}
|
|
177
|
+
releaseMessage = fallbackMessage;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const fullOptions = {
|
|
181
|
+
type: releaseType,
|
|
182
|
+
message: releaseMessage,
|
|
183
|
+
skipConfirmations: options.skipConfirmations,
|
|
184
|
+
};
|
|
185
|
+
const branchName = `release/${fullOptions.type}-${Date.now()}`;
|
|
186
|
+
// Step 2: Create branch
|
|
187
|
+
logger.step(2, TOTAL_STEPS, 'Creating release branch...');
|
|
188
|
+
spinner = ora(`Creating branch ${branchName}...`).start();
|
|
189
|
+
await git.createBranch(branchName);
|
|
190
|
+
spinner.succeed(`Branch created: ${branchName}`);
|
|
191
|
+
// Step 3: Generate changeset
|
|
192
|
+
logger.step(3, TOTAL_STEPS, 'Generating changeset...');
|
|
193
|
+
spinner = ora('Generating changeset...').start();
|
|
194
|
+
const changesetId = await git.generateChangeset(fullOptions, { packageName });
|
|
195
|
+
spinner.succeed(`Changeset created: ${changesetId}.md`);
|
|
196
|
+
logger.blank();
|
|
197
|
+
logger.info('Changeset content:');
|
|
198
|
+
logger.divider();
|
|
199
|
+
console.log(`"${packageName}": ${fullOptions.type}`);
|
|
200
|
+
console.log();
|
|
201
|
+
console.log(fullOptions.message);
|
|
202
|
+
logger.divider();
|
|
203
|
+
logger.blank();
|
|
204
|
+
// Confirm before pushing
|
|
205
|
+
if (!fullOptions.skipConfirmations) {
|
|
206
|
+
const shouldContinue = await confirm({
|
|
207
|
+
message: 'Push changes and create PR?',
|
|
208
|
+
initialValue: true,
|
|
209
|
+
});
|
|
210
|
+
if (isCancel(shouldContinue)) {
|
|
211
|
+
cancel('Release cancelled');
|
|
212
|
+
await git.cleanup();
|
|
213
|
+
process.exit(0);
|
|
214
|
+
}
|
|
215
|
+
if (!shouldContinue) {
|
|
216
|
+
logger.warn('Release cancelled by user');
|
|
217
|
+
await git.cleanup();
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// Step 4: Commit changes
|
|
222
|
+
logger.step(4, TOTAL_STEPS, 'Committing changes...');
|
|
223
|
+
spinner = ora('Committing...').start();
|
|
224
|
+
await git.stageAndCommit(`chore: add ${fullOptions.type} changeset for release`);
|
|
225
|
+
spinner.succeed('Changes committed');
|
|
226
|
+
// Step 5: Push branch
|
|
227
|
+
logger.step(5, TOTAL_STEPS, 'Pushing branch to origin...');
|
|
228
|
+
spinner = ora('Pushing...').start();
|
|
229
|
+
await git.push(branchName);
|
|
230
|
+
spinner.succeed('Branch pushed');
|
|
231
|
+
// Step 6: Create PR
|
|
232
|
+
logger.step(6, TOTAL_STEPS, 'Creating pull request...');
|
|
233
|
+
spinner = ora('Creating PR...').start();
|
|
234
|
+
const pr = await github.createPullRequest(branchName, `chore: ${fullOptions.type} release - ${fullOptions.message.slice(0, 50)}`, `## Release Changeset
|
|
235
|
+
|
|
236
|
+
**Type:** ${fullOptions.type}
|
|
237
|
+
|
|
238
|
+
**Changes:**
|
|
239
|
+
${fullOptions.message}
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
*This PR was created automatically by autoship*`);
|
|
243
|
+
spinner.succeed(`PR created: #${pr.number}`);
|
|
244
|
+
logger.info(`PR URL: ${pr.html_url}`);
|
|
245
|
+
// Confirm before waiting for checks
|
|
246
|
+
if (!fullOptions.skipConfirmations) {
|
|
247
|
+
const shouldWait = await confirm({
|
|
248
|
+
message: 'Wait for CI checks to pass?',
|
|
249
|
+
initialValue: true,
|
|
250
|
+
});
|
|
251
|
+
if (isCancel(shouldWait)) {
|
|
252
|
+
cancel('Release cancelled');
|
|
253
|
+
await git.cleanup();
|
|
254
|
+
process.exit(0);
|
|
255
|
+
}
|
|
256
|
+
if (!shouldWait) {
|
|
257
|
+
logger.info('You can manually merge the PR when ready');
|
|
258
|
+
await git.cleanup();
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// Step 7: Wait for checks
|
|
263
|
+
logger.step(7, TOTAL_STEPS, 'Waiting for CI checks...');
|
|
264
|
+
logger.waiting('This may take several minutes...');
|
|
265
|
+
const { success: checksPass, checks } = await github.waitForChecks(pr.number);
|
|
266
|
+
if (!checksPass) {
|
|
267
|
+
logger.error('CI checks failed!');
|
|
268
|
+
logger.info(`Please check the PR: ${pr.html_url}`);
|
|
269
|
+
const failedChecks = checks.filter(c => c.conclusion === 'failure');
|
|
270
|
+
for (const check of failedChecks) {
|
|
271
|
+
logger.error(` Failed: ${check.name}`);
|
|
272
|
+
}
|
|
273
|
+
await git.cleanup();
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
logger.success('All CI checks passed!');
|
|
277
|
+
// Confirm before merging
|
|
278
|
+
if (!fullOptions.skipConfirmations) {
|
|
279
|
+
const shouldMerge = await confirm({
|
|
280
|
+
message: 'Merge the changeset PR?',
|
|
281
|
+
initialValue: true,
|
|
282
|
+
});
|
|
283
|
+
if (isCancel(shouldMerge)) {
|
|
284
|
+
cancel('Release cancelled');
|
|
285
|
+
await git.cleanup();
|
|
286
|
+
process.exit(0);
|
|
287
|
+
}
|
|
288
|
+
if (!shouldMerge) {
|
|
289
|
+
logger.info('You can manually merge the PR when ready');
|
|
290
|
+
await git.cleanup();
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Step 8: Merge PR
|
|
295
|
+
logger.step(8, TOTAL_STEPS, 'Merging changeset PR...');
|
|
296
|
+
spinner = ora('Merging...').start();
|
|
297
|
+
await github.mergePullRequest(pr.number);
|
|
298
|
+
await github.deleteBranch(branchName);
|
|
299
|
+
spinner.succeed('Changeset PR merged!');
|
|
300
|
+
// Step 9: Wait for Version Packages PR
|
|
301
|
+
logger.step(9, TOTAL_STEPS, 'Waiting for Version Packages PR...');
|
|
302
|
+
logger.waiting('Changesets action will create a Version Packages PR...');
|
|
303
|
+
const versionPr = await github.waitForVersionPackagesPR();
|
|
304
|
+
logger.success(`Version Packages PR found: #${versionPr.number}`);
|
|
305
|
+
logger.info(`PR URL: ${versionPr.html_url}`);
|
|
306
|
+
// Confirm before continuing
|
|
307
|
+
if (!fullOptions.skipConfirmations) {
|
|
308
|
+
const shouldContinueVersion = await confirm({
|
|
309
|
+
message: 'Wait for checks and merge the Version Packages PR?',
|
|
310
|
+
initialValue: true,
|
|
311
|
+
});
|
|
312
|
+
if (isCancel(shouldContinueVersion)) {
|
|
313
|
+
cancel('Release cancelled');
|
|
314
|
+
await git.cleanup();
|
|
315
|
+
process.exit(0);
|
|
316
|
+
}
|
|
317
|
+
if (!shouldContinueVersion) {
|
|
318
|
+
logger.info('You can manually merge the Version Packages PR when ready');
|
|
319
|
+
await git.cleanup();
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// Wait for checks on Version Packages PR
|
|
324
|
+
logger.waiting('Waiting for Version Packages PR checks...');
|
|
325
|
+
const { success: versionChecksPass } = await github.waitForChecks(versionPr.number);
|
|
326
|
+
if (!versionChecksPass) {
|
|
327
|
+
logger.error('Version Packages PR checks failed!');
|
|
328
|
+
logger.info(`Please check the PR: ${versionPr.html_url}`);
|
|
329
|
+
await git.cleanup();
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
logger.success('Version Packages PR checks passed!');
|
|
333
|
+
// Final confirmation
|
|
334
|
+
if (!fullOptions.skipConfirmations) {
|
|
335
|
+
const shouldMergeVersion = await confirm({
|
|
336
|
+
message: 'Merge the Version Packages PR to publish the release?',
|
|
337
|
+
initialValue: true,
|
|
338
|
+
});
|
|
339
|
+
if (isCancel(shouldMergeVersion)) {
|
|
340
|
+
cancel('Release cancelled');
|
|
341
|
+
await git.cleanup();
|
|
342
|
+
process.exit(0);
|
|
343
|
+
}
|
|
344
|
+
if (!shouldMergeVersion) {
|
|
345
|
+
logger.info('You can manually merge the Version Packages PR when ready');
|
|
346
|
+
await git.cleanup();
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// Step 10: Merge Version Packages PR
|
|
351
|
+
logger.step(10, TOTAL_STEPS, 'Merging Version Packages PR...');
|
|
352
|
+
spinner = ora('Merging and publishing...').start();
|
|
353
|
+
await github.mergePullRequest(versionPr.number);
|
|
354
|
+
spinner.succeed('Version Packages PR merged!');
|
|
355
|
+
logger.blank();
|
|
356
|
+
logger.header('Release Complete!');
|
|
357
|
+
logger.success(`The ${fullOptions.type} release has been published.`);
|
|
358
|
+
logger.info('The release workflow will now:');
|
|
359
|
+
logger.detail('- Build binaries for all platforms');
|
|
360
|
+
logger.detail('- Publish the package to npm');
|
|
361
|
+
logger.detail('- Create a GitHub release');
|
|
362
|
+
await git.cleanup();
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
spinner?.fail();
|
|
366
|
+
logger.error(`Single-package release failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
367
|
+
try {
|
|
368
|
+
await git.cleanup();
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
// Ignore cleanup errors
|
|
372
|
+
}
|
|
373
|
+
process.exit(1);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Monorepo release flow
|
|
378
|
+
*/
|
|
379
|
+
async function runMonorepoRelease(workspace, git, github, config, options = {}) {
|
|
380
|
+
let spinner = null;
|
|
381
|
+
try {
|
|
382
|
+
// Log package count
|
|
383
|
+
logger.info(`Found ${workspace.packages.length} packages in workspace`);
|
|
384
|
+
// Find latest tag (required for monorepo)
|
|
385
|
+
spinner = ora('Finding latest version tag...').start();
|
|
386
|
+
const latestTag = await git.getLatestVersionTag();
|
|
387
|
+
if (!latestTag) {
|
|
388
|
+
spinner.fail('No version tags found');
|
|
389
|
+
throw new Error('Monorepo releases require version tags. Please create an initial release first.');
|
|
390
|
+
}
|
|
391
|
+
spinner.succeed(`Latest version: ${latestTag}`);
|
|
392
|
+
// Analyze changes per package
|
|
393
|
+
spinner = ora('Analyzing changes per package...').start();
|
|
394
|
+
const packageChanges = await analyzePackageChanges(workspace, latestTag, git.getGit());
|
|
395
|
+
spinner.succeed(`Found changes in ${packageChanges.length} packages`);
|
|
396
|
+
if (packageChanges.length === 0) {
|
|
397
|
+
logger.info('No changes detected in any package');
|
|
398
|
+
await git.cleanup();
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
// Display changed packages
|
|
402
|
+
for (const pc of packageChanges) {
|
|
403
|
+
logger.detail(`${pc.package.name}: ${pc.filesChanged.length} files (+${pc.insertions}/-${pc.deletions})`);
|
|
404
|
+
}
|
|
405
|
+
// Apply mode logic
|
|
406
|
+
const monorepoConfig = config.monorepo || { mode: 'selective' };
|
|
407
|
+
let packagesToRelease = packageChanges;
|
|
408
|
+
if (monorepoConfig.mode === 'lockstep') {
|
|
409
|
+
logger.info('Using lockstep versioning mode - all packages will be released');
|
|
410
|
+
// Include ALL packages, even ones with no changes
|
|
411
|
+
packagesToRelease = workspace.packages.map((pkg) => {
|
|
412
|
+
const existing = packageChanges.find((pc) => pc.package.name === pkg.name);
|
|
413
|
+
return existing || {
|
|
414
|
+
package: pkg,
|
|
415
|
+
commits: [],
|
|
416
|
+
filesChanged: [],
|
|
417
|
+
insertions: 0,
|
|
418
|
+
deletions: 0,
|
|
419
|
+
diff: '',
|
|
420
|
+
suggestedType: 'patch'
|
|
421
|
+
};
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
logger.info('Using selective versioning mode - only changed packages will be released');
|
|
426
|
+
}
|
|
427
|
+
// AI analysis per package
|
|
428
|
+
spinner = ora('Analyzing version bumps with AI...').start();
|
|
429
|
+
for (const pc of packagesToRelease) {
|
|
430
|
+
if (pc.commits.length > 0) {
|
|
431
|
+
pc.suggestedType = await analyzePackageChange(pc);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
spinner.succeed('AI analysis complete');
|
|
435
|
+
// Interactive selection
|
|
436
|
+
if (monorepoConfig.mode === 'lockstep') {
|
|
437
|
+
// Single version bump for all
|
|
438
|
+
const maxType = findMaxVersionType(packagesToRelease);
|
|
439
|
+
const selectedType = await select({
|
|
440
|
+
message: 'What type of release for all packages?',
|
|
441
|
+
options: [
|
|
442
|
+
{ label: 'patch - Bug fixes, small changes', value: 'patch' },
|
|
443
|
+
{ label: 'minor - New features, backwards compatible', value: 'minor' },
|
|
444
|
+
{ label: 'major - Breaking changes', value: 'major' },
|
|
445
|
+
],
|
|
446
|
+
initialValue: maxType,
|
|
447
|
+
});
|
|
448
|
+
if (isCancel(selectedType)) {
|
|
449
|
+
cancel('Release cancelled');
|
|
450
|
+
await git.cleanup();
|
|
451
|
+
process.exit(0);
|
|
452
|
+
}
|
|
453
|
+
packagesToRelease.forEach((pc) => pc.suggestedType = selectedType);
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
// Per-package confirmation
|
|
457
|
+
logger.info('Review version bumps per package:');
|
|
458
|
+
const confirmedPackages = [];
|
|
459
|
+
for (const pc of packagesToRelease) {
|
|
460
|
+
logger.blank();
|
|
461
|
+
logger.info(`Package: ${pc.package.name}`);
|
|
462
|
+
logger.detail(`Suggested: ${pc.suggestedType}`);
|
|
463
|
+
logger.detail(`Changes: ${pc.filesChanged.length} files (+${pc.insertions}/-${pc.deletions})`);
|
|
464
|
+
const selectedType = await select({
|
|
465
|
+
message: `Version bump for ${pc.package.name}?`,
|
|
466
|
+
options: [
|
|
467
|
+
{ label: 'patch - Bug fixes', value: 'patch' },
|
|
468
|
+
{ label: 'minor - New features', value: 'minor' },
|
|
469
|
+
{ label: 'major - Breaking changes', value: 'major' },
|
|
470
|
+
{ label: 'skip - Don\'t release this package', value: 'skip' },
|
|
471
|
+
],
|
|
472
|
+
initialValue: pc.suggestedType,
|
|
473
|
+
});
|
|
474
|
+
if (isCancel(selectedType)) {
|
|
475
|
+
cancel('Release cancelled');
|
|
476
|
+
await git.cleanup();
|
|
477
|
+
process.exit(0);
|
|
478
|
+
}
|
|
479
|
+
if (selectedType !== 'skip') {
|
|
480
|
+
pc.suggestedType = selectedType;
|
|
481
|
+
confirmedPackages.push(pc);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
packagesToRelease = confirmedPackages;
|
|
485
|
+
}
|
|
486
|
+
if (packagesToRelease.length === 0) {
|
|
487
|
+
logger.info('No packages selected for release');
|
|
488
|
+
await git.cleanup();
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
// Generate multi-package description
|
|
492
|
+
spinner = ora('Generating changeset description...').start();
|
|
493
|
+
let description;
|
|
494
|
+
if (options.message) {
|
|
495
|
+
description = options.message;
|
|
496
|
+
spinner.succeed('Using provided description');
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
try {
|
|
500
|
+
description = await generateMultiPackageChangesetMessage(packagesToRelease);
|
|
501
|
+
spinner.succeed('AI generated description');
|
|
502
|
+
logger.blank();
|
|
503
|
+
logger.info('AI-generated changeset description:');
|
|
504
|
+
logger.divider();
|
|
505
|
+
console.log(description);
|
|
506
|
+
logger.divider();
|
|
507
|
+
logger.blank();
|
|
508
|
+
const useAiMessage = await confirm({
|
|
509
|
+
message: 'Use this description?',
|
|
510
|
+
initialValue: true,
|
|
511
|
+
});
|
|
512
|
+
if (isCancel(useAiMessage)) {
|
|
513
|
+
cancel('Release cancelled');
|
|
514
|
+
await git.cleanup();
|
|
515
|
+
process.exit(0);
|
|
516
|
+
}
|
|
517
|
+
if (!useAiMessage) {
|
|
518
|
+
const customMessage = await text({
|
|
519
|
+
message: 'Enter your own description:',
|
|
520
|
+
validate: (value) => {
|
|
521
|
+
if (!value || value.length === 0)
|
|
522
|
+
return 'Message is required';
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
if (isCancel(customMessage)) {
|
|
526
|
+
cancel('Release cancelled');
|
|
527
|
+
await git.cleanup();
|
|
528
|
+
process.exit(0);
|
|
529
|
+
}
|
|
530
|
+
description = customMessage;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
catch (error) {
|
|
534
|
+
spinner.fail('AI generation failed');
|
|
535
|
+
logger.warn('Falling back to manual input');
|
|
536
|
+
const fallbackMessage = await text({
|
|
537
|
+
message: 'Describe the changes for this release:',
|
|
538
|
+
validate: (value) => {
|
|
539
|
+
if (!value || value.length === 0)
|
|
540
|
+
return 'Message is required';
|
|
541
|
+
},
|
|
542
|
+
});
|
|
543
|
+
if (isCancel(fallbackMessage)) {
|
|
544
|
+
cancel('Release cancelled');
|
|
545
|
+
await git.cleanup();
|
|
546
|
+
process.exit(0);
|
|
547
|
+
}
|
|
548
|
+
description = fallbackMessage;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
// Build changeset data
|
|
552
|
+
const changesetData = {
|
|
553
|
+
packages: new Map(packagesToRelease.map((pc) => [pc.package.name, pc.suggestedType])),
|
|
554
|
+
message: description
|
|
555
|
+
};
|
|
556
|
+
const fullOptions = {
|
|
557
|
+
type: 'patch', // Not used for multi-package
|
|
558
|
+
message: description,
|
|
559
|
+
skipConfirmations: options.skipConfirmations,
|
|
560
|
+
};
|
|
561
|
+
// Create branch and changeset
|
|
562
|
+
const branchName = `release/${monorepoConfig.mode}-${Date.now()}`;
|
|
563
|
+
logger.step(2, TOTAL_STEPS, 'Creating release branch...');
|
|
564
|
+
spinner = ora(`Creating branch ${branchName}...`).start();
|
|
565
|
+
await git.createBranch(branchName);
|
|
566
|
+
spinner.succeed(`Branch created: ${branchName}`);
|
|
567
|
+
logger.step(3, TOTAL_STEPS, 'Generating changeset...');
|
|
568
|
+
spinner = ora('Generating changeset...').start();
|
|
569
|
+
await git.generateChangeset(fullOptions, changesetData);
|
|
570
|
+
spinner.succeed('Changeset created');
|
|
571
|
+
logger.blank();
|
|
572
|
+
logger.info('Changeset content:');
|
|
573
|
+
logger.divider();
|
|
574
|
+
for (const [pkgName, type] of changesetData.packages) {
|
|
575
|
+
console.log(`"${pkgName}": ${type}`);
|
|
576
|
+
}
|
|
577
|
+
console.log();
|
|
578
|
+
console.log(description);
|
|
579
|
+
logger.divider();
|
|
580
|
+
logger.blank();
|
|
581
|
+
// Confirm before pushing
|
|
582
|
+
if (!fullOptions.skipConfirmations) {
|
|
583
|
+
const shouldContinue = await confirm({
|
|
584
|
+
message: 'Push changes and create PR?',
|
|
585
|
+
initialValue: true,
|
|
586
|
+
});
|
|
587
|
+
if (isCancel(shouldContinue)) {
|
|
588
|
+
cancel('Release cancelled');
|
|
589
|
+
await git.cleanup();
|
|
590
|
+
process.exit(0);
|
|
591
|
+
}
|
|
592
|
+
if (!shouldContinue) {
|
|
593
|
+
logger.warn('Release cancelled by user');
|
|
594
|
+
await git.cleanup();
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
// Continue with PR flow (same as single-package)
|
|
599
|
+
logger.step(4, TOTAL_STEPS, 'Committing changes...');
|
|
600
|
+
spinner = ora('Committing...').start();
|
|
601
|
+
await git.stageAndCommit(`chore: add changeset for ${monorepoConfig.mode} release`);
|
|
602
|
+
spinner.succeed('Changes committed');
|
|
603
|
+
logger.step(5, TOTAL_STEPS, 'Pushing branch to origin...');
|
|
604
|
+
spinner = ora('Pushing...').start();
|
|
605
|
+
await git.push(branchName);
|
|
606
|
+
spinner.succeed('Branch pushed');
|
|
607
|
+
logger.step(6, TOTAL_STEPS, 'Creating pull request...');
|
|
608
|
+
spinner = ora('Creating PR...').start();
|
|
609
|
+
const pr = await github.createPullRequest(branchName, `chore: ${monorepoConfig.mode} release - ${description.slice(0, 50)}`, `## Release Changeset
|
|
610
|
+
|
|
611
|
+
**Mode:** ${monorepoConfig.mode}
|
|
612
|
+
**Packages:** ${packagesToRelease.length}
|
|
613
|
+
|
|
614
|
+
**Changes:**
|
|
615
|
+
${description}
|
|
616
|
+
|
|
617
|
+
---
|
|
618
|
+
*This PR was created automatically by just-ship-it*`);
|
|
619
|
+
spinner.succeed(`PR created: #${pr.number}`);
|
|
620
|
+
logger.info(`PR URL: ${pr.html_url}`);
|
|
621
|
+
// Confirm before waiting for checks
|
|
622
|
+
if (!fullOptions.skipConfirmations) {
|
|
623
|
+
const shouldWait = await confirm({
|
|
624
|
+
message: 'Wait for CI checks to pass?',
|
|
625
|
+
initialValue: true,
|
|
626
|
+
});
|
|
627
|
+
if (isCancel(shouldWait)) {
|
|
628
|
+
cancel('Release cancelled');
|
|
629
|
+
await git.cleanup();
|
|
630
|
+
process.exit(0);
|
|
631
|
+
}
|
|
632
|
+
if (!shouldWait) {
|
|
633
|
+
logger.info('You can manually merge the PR when ready');
|
|
634
|
+
await git.cleanup();
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
// Wait for checks
|
|
639
|
+
logger.step(7, TOTAL_STEPS, 'Waiting for CI checks...');
|
|
640
|
+
logger.waiting('This may take several minutes...');
|
|
641
|
+
const { success: checksPass, checks } = await github.waitForChecks(pr.number);
|
|
642
|
+
if (!checksPass) {
|
|
643
|
+
logger.error('CI checks failed!');
|
|
644
|
+
logger.info(`Please check the PR: ${pr.html_url}`);
|
|
645
|
+
const failedChecks = checks.filter((c) => c.conclusion === 'failure');
|
|
646
|
+
for (const check of failedChecks) {
|
|
647
|
+
logger.error(` Failed: ${check.name}`);
|
|
648
|
+
}
|
|
649
|
+
await git.cleanup();
|
|
650
|
+
process.exit(1);
|
|
651
|
+
}
|
|
652
|
+
logger.success('All CI checks passed!');
|
|
653
|
+
// Confirm before merging
|
|
654
|
+
if (!fullOptions.skipConfirmations) {
|
|
655
|
+
const shouldMerge = await confirm({
|
|
656
|
+
message: 'Merge the changeset PR?',
|
|
657
|
+
initialValue: true,
|
|
658
|
+
});
|
|
659
|
+
if (isCancel(shouldMerge)) {
|
|
660
|
+
cancel('Release cancelled');
|
|
661
|
+
await git.cleanup();
|
|
662
|
+
process.exit(0);
|
|
663
|
+
}
|
|
664
|
+
if (!shouldMerge) {
|
|
665
|
+
logger.info('You can manually merge the PR when ready');
|
|
666
|
+
await git.cleanup();
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
// Merge PR
|
|
671
|
+
logger.step(8, TOTAL_STEPS, 'Merging changeset PR...');
|
|
672
|
+
spinner = ora('Merging...').start();
|
|
673
|
+
await github.mergePullRequest(pr.number);
|
|
674
|
+
await github.deleteBranch(branchName);
|
|
675
|
+
spinner.succeed('Changeset PR merged!');
|
|
676
|
+
// Wait for Version Packages PR
|
|
677
|
+
logger.step(9, TOTAL_STEPS, 'Waiting for Version Packages PR...');
|
|
678
|
+
logger.waiting('Changesets action will create a Version Packages PR...');
|
|
679
|
+
const versionPr = await github.waitForVersionPackagesPR();
|
|
680
|
+
logger.success(`Version Packages PR found: #${versionPr.number}`);
|
|
681
|
+
logger.info(`PR URL: ${versionPr.html_url}`);
|
|
682
|
+
// Confirm before continuing
|
|
683
|
+
if (!fullOptions.skipConfirmations) {
|
|
684
|
+
const shouldContinueVersion = await confirm({
|
|
685
|
+
message: 'Wait for checks and merge the Version Packages PR?',
|
|
686
|
+
initialValue: true,
|
|
687
|
+
});
|
|
688
|
+
if (isCancel(shouldContinueVersion)) {
|
|
689
|
+
cancel('Release cancelled');
|
|
690
|
+
await git.cleanup();
|
|
691
|
+
process.exit(0);
|
|
692
|
+
}
|
|
693
|
+
if (!shouldContinueVersion) {
|
|
694
|
+
logger.info('You can manually merge the Version Packages PR when ready');
|
|
695
|
+
await git.cleanup();
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
// Wait for checks on Version Packages PR
|
|
700
|
+
logger.waiting('Waiting for Version Packages PR checks...');
|
|
701
|
+
const { success: versionChecksPass } = await github.waitForChecks(versionPr.number);
|
|
702
|
+
if (!versionChecksPass) {
|
|
703
|
+
logger.error('Version Packages PR checks failed!');
|
|
704
|
+
logger.info(`Please check the PR: ${versionPr.html_url}`);
|
|
705
|
+
await git.cleanup();
|
|
706
|
+
process.exit(1);
|
|
707
|
+
}
|
|
708
|
+
logger.success('Version Packages PR checks passed!');
|
|
709
|
+
// Final confirmation
|
|
710
|
+
if (!fullOptions.skipConfirmations) {
|
|
711
|
+
const shouldMergeVersion = await confirm({
|
|
712
|
+
message: 'Merge the Version Packages PR to publish the release?',
|
|
713
|
+
initialValue: true,
|
|
714
|
+
});
|
|
715
|
+
if (isCancel(shouldMergeVersion)) {
|
|
716
|
+
cancel('Release cancelled');
|
|
717
|
+
await git.cleanup();
|
|
718
|
+
process.exit(0);
|
|
719
|
+
}
|
|
720
|
+
if (!shouldMergeVersion) {
|
|
721
|
+
logger.info('You can manually merge the Version Packages PR when ready');
|
|
722
|
+
await git.cleanup();
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
// Merge Version Packages PR
|
|
727
|
+
logger.step(10, TOTAL_STEPS, 'Merging Version Packages PR...');
|
|
728
|
+
spinner = ora('Merging and publishing...').start();
|
|
729
|
+
await github.mergePullRequest(versionPr.number);
|
|
730
|
+
spinner.succeed('Version Packages PR merged!');
|
|
731
|
+
logger.blank();
|
|
732
|
+
logger.header('Release Complete!');
|
|
733
|
+
logger.success(`The ${monorepoConfig.mode} release has been published.`);
|
|
734
|
+
logger.info('The release workflow will now:');
|
|
735
|
+
logger.detail('- Build binaries for all platforms');
|
|
736
|
+
logger.detail('- Publish packages to npm');
|
|
737
|
+
logger.detail('- Create GitHub releases');
|
|
738
|
+
await git.cleanup();
|
|
739
|
+
}
|
|
740
|
+
catch (error) {
|
|
741
|
+
spinner?.fail();
|
|
742
|
+
logger.error(`Monorepo release failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
743
|
+
try {
|
|
744
|
+
await git.cleanup();
|
|
745
|
+
}
|
|
746
|
+
catch {
|
|
747
|
+
// Ignore cleanup errors
|
|
748
|
+
}
|
|
749
|
+
process.exit(1);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
//# sourceMappingURL=release.js.map
|