@claude-flow/deployment 3.0.0-alpha.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/.agentic-flow/intelligence.json +16 -0
- package/QUICK_START.md +281 -0
- package/README.md +333 -0
- package/__tests__/coverage/base.css +224 -0
- package/__tests__/coverage/block-navigation.js +87 -0
- package/__tests__/coverage/coverage-final.json +4 -0
- package/__tests__/coverage/favicon.png +0 -0
- package/__tests__/coverage/index.html +146 -0
- package/__tests__/coverage/lcov-report/base.css +224 -0
- package/__tests__/coverage/lcov-report/block-navigation.js +87 -0
- package/__tests__/coverage/lcov-report/favicon.png +0 -0
- package/__tests__/coverage/lcov-report/index.html +146 -0
- package/__tests__/coverage/lcov-report/prettify.css +1 -0
- package/__tests__/coverage/lcov-report/prettify.js +2 -0
- package/__tests__/coverage/lcov-report/publisher.ts.html +811 -0
- package/__tests__/coverage/lcov-report/release-manager.ts.html +1120 -0
- package/__tests__/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/__tests__/coverage/lcov-report/sorter.js +210 -0
- package/__tests__/coverage/lcov-report/validator.ts.html +940 -0
- package/__tests__/coverage/lcov.info +908 -0
- package/__tests__/coverage/prettify.css +1 -0
- package/__tests__/coverage/prettify.js +2 -0
- package/__tests__/coverage/publisher.ts.html +811 -0
- package/__tests__/coverage/release-manager.ts.html +1120 -0
- package/__tests__/coverage/sort-arrow-sprite.png +0 -0
- package/__tests__/coverage/sorter.js +210 -0
- package/__tests__/coverage/validator.ts.html +940 -0
- package/dist/__tests__/release-manager.test.d.ts +2 -0
- package/dist/__tests__/release-manager.test.d.ts.map +1 -0
- package/dist/__tests__/release-manager.test.js +62 -0
- package/dist/__tests__/release-manager.test.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +45 -0
- package/dist/index.js.map +1 -0
- package/dist/publisher.d.ts +58 -0
- package/dist/publisher.d.ts.map +1 -0
- package/dist/publisher.js +229 -0
- package/dist/publisher.js.map +1 -0
- package/dist/release-manager.d.ts +46 -0
- package/dist/release-manager.d.ts.map +1 -0
- package/dist/release-manager.js +282 -0
- package/dist/release-manager.js.map +1 -0
- package/dist/types.d.ts +168 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/validator.d.ts +46 -0
- package/dist/validator.d.ts.map +1 -0
- package/dist/validator.js +251 -0
- package/dist/validator.js.map +1 -0
- package/examples/basic-release.ts +92 -0
- package/examples/dry-run.ts +70 -0
- package/examples/prerelease-workflow.ts +98 -0
- package/package.json +27 -0
- package/src/__tests__/release-manager.test.ts +72 -0
- package/src/index.ts +88 -0
- package/src/publisher.ts +273 -0
- package/src/release-manager.ts +345 -0
- package/src/types.ts +159 -0
- package/src/validator.ts +285 -0
- package/tsconfig.json +9 -0
- package/tsconfig.tsbuildinfo +1 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @claude-flow/deployment
|
|
3
|
+
* Release management, CI/CD, and versioning module
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Export types
|
|
7
|
+
export type {
|
|
8
|
+
VersionBumpType,
|
|
9
|
+
ReleaseChannel,
|
|
10
|
+
ReleaseOptions,
|
|
11
|
+
ReleaseResult,
|
|
12
|
+
PublishOptions,
|
|
13
|
+
PublishResult,
|
|
14
|
+
ValidationOptions,
|
|
15
|
+
ValidationResult,
|
|
16
|
+
PackageInfo,
|
|
17
|
+
GitCommit,
|
|
18
|
+
ChangelogEntry
|
|
19
|
+
} from './types.js';
|
|
20
|
+
|
|
21
|
+
// Export classes
|
|
22
|
+
export { ReleaseManager } from './release-manager.js';
|
|
23
|
+
export { Publisher } from './publisher.js';
|
|
24
|
+
export { Validator } from './validator.js';
|
|
25
|
+
|
|
26
|
+
// Export convenience functions
|
|
27
|
+
export {
|
|
28
|
+
prepareRelease
|
|
29
|
+
} from './release-manager.js';
|
|
30
|
+
|
|
31
|
+
export {
|
|
32
|
+
publishToNpm,
|
|
33
|
+
checkVersionExists,
|
|
34
|
+
getLatestVersion
|
|
35
|
+
} from './publisher.js';
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
validate
|
|
39
|
+
} from './validator.js';
|
|
40
|
+
|
|
41
|
+
// Legacy exports for backward compatibility
|
|
42
|
+
export interface ReleaseConfig {
|
|
43
|
+
version: string;
|
|
44
|
+
channel: 'alpha' | 'beta' | 'stable';
|
|
45
|
+
changelog: boolean;
|
|
46
|
+
dryRun: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface DeploymentTarget {
|
|
50
|
+
name: string;
|
|
51
|
+
type: 'npm' | 'docker' | 'github-release';
|
|
52
|
+
config: Record<string, unknown>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Legacy prepare release function
|
|
57
|
+
* @deprecated Use prepareRelease from release-manager instead
|
|
58
|
+
*/
|
|
59
|
+
export async function prepare(config: ReleaseConfig): Promise<void> {
|
|
60
|
+
const { ReleaseManager } = await import('./release-manager.js');
|
|
61
|
+
const manager = new ReleaseManager();
|
|
62
|
+
|
|
63
|
+
await manager.prepareRelease({
|
|
64
|
+
version: config.version,
|
|
65
|
+
channel: config.channel as any,
|
|
66
|
+
generateChangelog: config.changelog,
|
|
67
|
+
dryRun: config.dryRun
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Legacy deploy function
|
|
73
|
+
* @deprecated Use publishToNpm from publisher instead
|
|
74
|
+
*/
|
|
75
|
+
export async function deploy(target: DeploymentTarget): Promise<void> {
|
|
76
|
+
if (target.type === 'npm') {
|
|
77
|
+
const { Publisher } = await import('./publisher.js');
|
|
78
|
+
const publisher = new Publisher();
|
|
79
|
+
|
|
80
|
+
await publisher.publishToNpm({
|
|
81
|
+
tag: (target.config.tag as string) || 'latest',
|
|
82
|
+
dryRun: (target.config.dryRun as boolean) || false
|
|
83
|
+
});
|
|
84
|
+
} else {
|
|
85
|
+
console.log(`Deploying to ${target.name} (${target.type})`);
|
|
86
|
+
throw new Error(`Deployment type ${target.type} not yet implemented`);
|
|
87
|
+
}
|
|
88
|
+
}
|
package/src/publisher.ts
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NPM Publisher
|
|
3
|
+
* Handles npm package publishing with tag support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execSync, execFileSync } from 'child_process';
|
|
7
|
+
import { readFileSync, existsSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import type { PublishOptions, PublishResult, PackageInfo } from './types.js';
|
|
10
|
+
|
|
11
|
+
export class Publisher {
|
|
12
|
+
private cwd: string;
|
|
13
|
+
|
|
14
|
+
constructor(cwd: string = process.cwd()) {
|
|
15
|
+
this.cwd = cwd;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Publish package to npm
|
|
20
|
+
*/
|
|
21
|
+
async publishToNpm(options: PublishOptions = {}): Promise<PublishResult> {
|
|
22
|
+
const {
|
|
23
|
+
tag = 'latest',
|
|
24
|
+
access,
|
|
25
|
+
dryRun = false,
|
|
26
|
+
registry,
|
|
27
|
+
otp,
|
|
28
|
+
skipBuild = false,
|
|
29
|
+
buildCommand = 'npm run build'
|
|
30
|
+
} = options;
|
|
31
|
+
|
|
32
|
+
const result: PublishResult = {
|
|
33
|
+
packageName: '',
|
|
34
|
+
version: '',
|
|
35
|
+
tag,
|
|
36
|
+
success: false
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
// Read package.json
|
|
41
|
+
const pkgPath = join(this.cwd, 'package.json');
|
|
42
|
+
if (!existsSync(pkgPath)) {
|
|
43
|
+
throw new Error('package.json not found');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const pkg: PackageInfo = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
47
|
+
|
|
48
|
+
if (pkg.private) {
|
|
49
|
+
throw new Error('Cannot publish private package');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
result.packageName = pkg.name;
|
|
53
|
+
result.version = pkg.version;
|
|
54
|
+
|
|
55
|
+
// Run build if not skipped
|
|
56
|
+
if (!skipBuild) {
|
|
57
|
+
console.log('Building package...');
|
|
58
|
+
this.execCommand(buildCommand);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Construct npm publish command arguments (without 'npm' prefix for execNpmCommand)
|
|
62
|
+
const publishArgs: string[] = ['publish'];
|
|
63
|
+
|
|
64
|
+
if (tag) {
|
|
65
|
+
publishArgs.push('--tag', tag);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (access) {
|
|
69
|
+
publishArgs.push('--access', access);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (registry) {
|
|
73
|
+
publishArgs.push('--registry', registry);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (otp) {
|
|
77
|
+
publishArgs.push('--otp', otp);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (dryRun) {
|
|
81
|
+
publishArgs.push('--dry-run');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Execute publish
|
|
85
|
+
console.log(`Publishing ${result.packageName}@${result.version} with tag '${tag}'...`);
|
|
86
|
+
|
|
87
|
+
if (dryRun) {
|
|
88
|
+
console.log('Dry run mode - no actual publish');
|
|
89
|
+
console.log('Command: npm', publishArgs.join(' '));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const output = this.execNpmCommand(publishArgs, true);
|
|
93
|
+
|
|
94
|
+
// Parse output for tarball URL
|
|
95
|
+
const tarballMatch = output.match(/https:\/\/[^\s]+\.tgz/);
|
|
96
|
+
if (tarballMatch) {
|
|
97
|
+
result.tarball = tarballMatch[0];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
result.publishedAt = new Date();
|
|
101
|
+
result.success = true;
|
|
102
|
+
|
|
103
|
+
console.log(`Successfully published ${result.packageName}@${result.version}`);
|
|
104
|
+
|
|
105
|
+
return result;
|
|
106
|
+
|
|
107
|
+
} catch (error) {
|
|
108
|
+
result.error = error instanceof Error ? error.message : String(error);
|
|
109
|
+
console.error('Publish failed:', result.error);
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Check if package version already exists on npm
|
|
116
|
+
*/
|
|
117
|
+
async checkVersionExists(packageName: string, version: string): Promise<boolean> {
|
|
118
|
+
try {
|
|
119
|
+
const output = this.execNpmCommand(['view', `${packageName}@${version}`, 'version'], true);
|
|
120
|
+
return output.trim() === version;
|
|
121
|
+
} catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get latest published version
|
|
128
|
+
*/
|
|
129
|
+
async getLatestVersion(packageName: string, tag = 'latest'): Promise<string | null> {
|
|
130
|
+
try {
|
|
131
|
+
const output = this.execNpmCommand(['view', `${packageName}@${tag}`, 'version'], true);
|
|
132
|
+
return output.trim();
|
|
133
|
+
} catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get package info from npm registry
|
|
140
|
+
*/
|
|
141
|
+
async getPackageInfo(packageName: string): Promise<PackageInfo | null> {
|
|
142
|
+
try {
|
|
143
|
+
const output = this.execNpmCommand(['view', packageName, '--json'], true);
|
|
144
|
+
return JSON.parse(output);
|
|
145
|
+
} catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Verify npm authentication
|
|
152
|
+
*/
|
|
153
|
+
async verifyAuth(): Promise<boolean> {
|
|
154
|
+
try {
|
|
155
|
+
const output = this.execNpmCommand(['whoami'], true);
|
|
156
|
+
return output.trim().length > 0;
|
|
157
|
+
} catch {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get npm registry URL
|
|
164
|
+
*/
|
|
165
|
+
async getRegistry(): Promise<string> {
|
|
166
|
+
try {
|
|
167
|
+
const output = this.execNpmCommand(['config', 'get', 'registry'], true);
|
|
168
|
+
return output.trim();
|
|
169
|
+
} catch {
|
|
170
|
+
return 'https://registry.npmjs.org/';
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Pack package to tarball
|
|
176
|
+
*/
|
|
177
|
+
async pack(outputDir?: string): Promise<string> {
|
|
178
|
+
try {
|
|
179
|
+
const packArgs = ['pack'];
|
|
180
|
+
if (outputDir) {
|
|
181
|
+
packArgs.push('--pack-destination', outputDir);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const output = this.execNpmCommand(packArgs, true);
|
|
185
|
+
const tarballName = output.trim().split('\n').pop() || '';
|
|
186
|
+
|
|
187
|
+
return outputDir ? join(outputDir, tarballName) : tarballName;
|
|
188
|
+
} catch (error) {
|
|
189
|
+
throw new Error(`Failed to pack: ${error}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Execute npm command safely using execFileSync
|
|
195
|
+
*/
|
|
196
|
+
private execNpmCommand(args: string[], returnOutput = false): string {
|
|
197
|
+
try {
|
|
198
|
+
// Validate args don't contain shell metacharacters
|
|
199
|
+
for (const arg of args) {
|
|
200
|
+
if (/[;&|`$()<>]/.test(arg)) {
|
|
201
|
+
throw new Error(`Invalid argument: contains shell metacharacters`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const output = execFileSync('npm', args, {
|
|
205
|
+
cwd: this.cwd,
|
|
206
|
+
encoding: 'utf-8',
|
|
207
|
+
shell: false,
|
|
208
|
+
stdio: returnOutput ? ['pipe', 'pipe', 'pipe'] : 'inherit'
|
|
209
|
+
});
|
|
210
|
+
return returnOutput ? output : '';
|
|
211
|
+
} catch (error) {
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Execute command (for build scripts only - validated)
|
|
218
|
+
*/
|
|
219
|
+
private execCommand(cmd: string, returnOutput = false): string {
|
|
220
|
+
// Only allow npm/npx build commands for safety
|
|
221
|
+
const allowedPrefixes = ['npm run ', 'npm ', 'npx ', 'pnpm ', 'yarn '];
|
|
222
|
+
const isAllowed = allowedPrefixes.some(prefix => cmd.startsWith(prefix));
|
|
223
|
+
if (!isAllowed) {
|
|
224
|
+
throw new Error(`Disallowed command: only npm/npx/pnpm/yarn commands are permitted`);
|
|
225
|
+
}
|
|
226
|
+
// Validate no dangerous shell metacharacters
|
|
227
|
+
if (/[;&|`$()<>]/.test(cmd)) {
|
|
228
|
+
throw new Error(`Invalid command: contains shell metacharacters`);
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
const output = execSync(cmd, {
|
|
232
|
+
cwd: this.cwd,
|
|
233
|
+
encoding: 'utf-8',
|
|
234
|
+
stdio: returnOutput ? 'pipe' : 'inherit'
|
|
235
|
+
});
|
|
236
|
+
return returnOutput ? output : '';
|
|
237
|
+
} catch (error) {
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Convenience function to publish to npm
|
|
245
|
+
*/
|
|
246
|
+
export async function publishToNpm(
|
|
247
|
+
options: PublishOptions = {}
|
|
248
|
+
): Promise<PublishResult> {
|
|
249
|
+
const publisher = new Publisher();
|
|
250
|
+
return publisher.publishToNpm(options);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Convenience function to check version exists
|
|
255
|
+
*/
|
|
256
|
+
export async function checkVersionExists(
|
|
257
|
+
packageName: string,
|
|
258
|
+
version: string
|
|
259
|
+
): Promise<boolean> {
|
|
260
|
+
const publisher = new Publisher();
|
|
261
|
+
return publisher.checkVersionExists(packageName, version);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Convenience function to get latest version
|
|
266
|
+
*/
|
|
267
|
+
export async function getLatestVersion(
|
|
268
|
+
packageName: string,
|
|
269
|
+
tag?: string
|
|
270
|
+
): Promise<string | null> {
|
|
271
|
+
const publisher = new Publisher();
|
|
272
|
+
return publisher.getLatestVersion(packageName, tag);
|
|
273
|
+
}
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Release Manager
|
|
3
|
+
* Handles version bumping, changelog generation, and git tagging
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import type {
|
|
10
|
+
ReleaseOptions,
|
|
11
|
+
ReleaseResult,
|
|
12
|
+
PackageInfo,
|
|
13
|
+
GitCommit,
|
|
14
|
+
ChangelogEntry,
|
|
15
|
+
VersionBumpType
|
|
16
|
+
} from './types.js';
|
|
17
|
+
|
|
18
|
+
export class ReleaseManager {
|
|
19
|
+
private cwd: string;
|
|
20
|
+
|
|
21
|
+
constructor(cwd: string = process.cwd()) {
|
|
22
|
+
this.cwd = cwd;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Prepare a release with version bumping, changelog, and git tagging
|
|
27
|
+
*/
|
|
28
|
+
async prepareRelease(options: ReleaseOptions = {}): Promise<ReleaseResult> {
|
|
29
|
+
const {
|
|
30
|
+
bumpType = 'patch',
|
|
31
|
+
version,
|
|
32
|
+
channel = 'latest',
|
|
33
|
+
generateChangelog = true,
|
|
34
|
+
createTag = true,
|
|
35
|
+
commit = true,
|
|
36
|
+
dryRun = false,
|
|
37
|
+
skipValidation = false,
|
|
38
|
+
tagPrefix = 'v',
|
|
39
|
+
changelogPath = 'CHANGELOG.md'
|
|
40
|
+
} = options;
|
|
41
|
+
|
|
42
|
+
const result: ReleaseResult = {
|
|
43
|
+
oldVersion: '',
|
|
44
|
+
newVersion: '',
|
|
45
|
+
success: false,
|
|
46
|
+
warnings: []
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
// Read package.json
|
|
51
|
+
const pkgPath = join(this.cwd, 'package.json');
|
|
52
|
+
if (!existsSync(pkgPath)) {
|
|
53
|
+
throw new Error('package.json not found');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const pkg: PackageInfo = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
57
|
+
result.oldVersion = pkg.version;
|
|
58
|
+
|
|
59
|
+
// Check for uncommitted changes
|
|
60
|
+
if (!skipValidation) {
|
|
61
|
+
const gitStatus = this.execCommand('git status --porcelain', true);
|
|
62
|
+
if (gitStatus && !dryRun) {
|
|
63
|
+
result.warnings?.push('Uncommitted changes detected');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Determine new version
|
|
68
|
+
result.newVersion = version || this.bumpVersion(pkg.version, bumpType, channel);
|
|
69
|
+
|
|
70
|
+
// Generate changelog if requested
|
|
71
|
+
if (generateChangelog) {
|
|
72
|
+
const commits = this.getCommitsSinceLastTag();
|
|
73
|
+
const changelogEntry = this.generateChangelogEntry(result.newVersion, commits);
|
|
74
|
+
result.changelog = this.formatChangelogEntry(changelogEntry);
|
|
75
|
+
|
|
76
|
+
if (!dryRun) {
|
|
77
|
+
this.updateChangelogFile(changelogPath, result.changelog);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Update package.json version
|
|
82
|
+
if (!dryRun) {
|
|
83
|
+
pkg.version = result.newVersion;
|
|
84
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Create git commit
|
|
88
|
+
if (commit && !dryRun) {
|
|
89
|
+
const commitMessage = `chore(release): ${result.newVersion}`;
|
|
90
|
+
|
|
91
|
+
// Stage changes
|
|
92
|
+
this.execCommand(`git add package.json ${changelogPath}`);
|
|
93
|
+
|
|
94
|
+
// Commit
|
|
95
|
+
this.execCommand(`git commit -m "${commitMessage}"`);
|
|
96
|
+
|
|
97
|
+
result.commitHash = this.execCommand('git rev-parse HEAD', true).trim();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Create git tag
|
|
101
|
+
if (createTag && !dryRun) {
|
|
102
|
+
result.tag = `${tagPrefix}${result.newVersion}`;
|
|
103
|
+
const tagMessage = `Release ${result.newVersion}`;
|
|
104
|
+
this.execCommand(`git tag -a ${result.tag} -m "${tagMessage}"`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
result.success = true;
|
|
108
|
+
return result;
|
|
109
|
+
|
|
110
|
+
} catch (error) {
|
|
111
|
+
result.error = error instanceof Error ? error.message : String(error);
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Bump version based on type
|
|
118
|
+
*/
|
|
119
|
+
private bumpVersion(
|
|
120
|
+
currentVersion: string,
|
|
121
|
+
bumpType: VersionBumpType,
|
|
122
|
+
channel: string
|
|
123
|
+
): string {
|
|
124
|
+
const versionMatch = currentVersion.match(/^(\d+)\.(\d+)\.(\d+)(?:-([a-z]+)\.(\d+))?$/);
|
|
125
|
+
|
|
126
|
+
if (!versionMatch) {
|
|
127
|
+
throw new Error(`Invalid version format: ${currentVersion}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let [, major, minor, patch, prerelease, prereleaseNum] = versionMatch;
|
|
131
|
+
let newMajor = parseInt(major);
|
|
132
|
+
let newMinor = parseInt(minor);
|
|
133
|
+
let newPatch = parseInt(patch);
|
|
134
|
+
let newPrerelease: string | undefined = prerelease;
|
|
135
|
+
let newPrereleaseNum = prereleaseNum ? parseInt(prereleaseNum) : 0;
|
|
136
|
+
|
|
137
|
+
switch (bumpType) {
|
|
138
|
+
case 'major':
|
|
139
|
+
newMajor++;
|
|
140
|
+
newMinor = 0;
|
|
141
|
+
newPatch = 0;
|
|
142
|
+
newPrerelease = undefined;
|
|
143
|
+
break;
|
|
144
|
+
|
|
145
|
+
case 'minor':
|
|
146
|
+
newMinor++;
|
|
147
|
+
newPatch = 0;
|
|
148
|
+
newPrerelease = undefined;
|
|
149
|
+
break;
|
|
150
|
+
|
|
151
|
+
case 'patch':
|
|
152
|
+
newPatch++;
|
|
153
|
+
newPrerelease = undefined;
|
|
154
|
+
break;
|
|
155
|
+
|
|
156
|
+
case 'prerelease':
|
|
157
|
+
if (newPrerelease && channel === newPrerelease) {
|
|
158
|
+
newPrereleaseNum++;
|
|
159
|
+
} else {
|
|
160
|
+
newPrereleaseNum = 1;
|
|
161
|
+
newPrerelease = channel;
|
|
162
|
+
}
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let version = `${newMajor}.${newMinor}.${newPatch}`;
|
|
167
|
+
if (newPrerelease && bumpType === 'prerelease') {
|
|
168
|
+
version += `-${newPrerelease}.${newPrereleaseNum}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return version;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get git commits since last tag
|
|
176
|
+
*/
|
|
177
|
+
private getCommitsSinceLastTag(): GitCommit[] {
|
|
178
|
+
try {
|
|
179
|
+
const lastTag = this.execCommand('git describe --tags --abbrev=0', true).trim();
|
|
180
|
+
const range = `${lastTag}..HEAD`;
|
|
181
|
+
return this.parseCommits(range);
|
|
182
|
+
} catch {
|
|
183
|
+
// No tags found, get all commits
|
|
184
|
+
return this.parseCommits('');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Parse git commits
|
|
190
|
+
*/
|
|
191
|
+
private parseCommits(range: string): GitCommit[] {
|
|
192
|
+
const format = '--pretty=format:%H%n%s%n%an%n%ai%n---COMMIT---';
|
|
193
|
+
const cmd = range
|
|
194
|
+
? `git log ${range} ${format}`
|
|
195
|
+
: `git log ${format}`;
|
|
196
|
+
|
|
197
|
+
const output = this.execCommand(cmd, true);
|
|
198
|
+
const commits: GitCommit[] = [];
|
|
199
|
+
|
|
200
|
+
const commitBlocks = output.split('---COMMIT---').filter(Boolean);
|
|
201
|
+
|
|
202
|
+
for (const block of commitBlocks) {
|
|
203
|
+
const lines = block.trim().split('\n');
|
|
204
|
+
if (lines.length < 4) continue;
|
|
205
|
+
|
|
206
|
+
const [hash, message, author, date] = lines;
|
|
207
|
+
|
|
208
|
+
// Parse conventional commit format
|
|
209
|
+
const conventionalMatch = message.match(/^(\w+)(?:\(([^)]+)\))?: (.+)$/);
|
|
210
|
+
|
|
211
|
+
commits.push({
|
|
212
|
+
hash: hash.trim(),
|
|
213
|
+
message: message.trim(),
|
|
214
|
+
author: author.trim(),
|
|
215
|
+
date: date.trim(),
|
|
216
|
+
type: conventionalMatch?.[1],
|
|
217
|
+
scope: conventionalMatch?.[2],
|
|
218
|
+
breaking: message.includes('BREAKING CHANGE')
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return commits;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Generate changelog entry from commits
|
|
227
|
+
*/
|
|
228
|
+
private generateChangelogEntry(version: string, commits: GitCommit[]): ChangelogEntry {
|
|
229
|
+
const entry: ChangelogEntry = {
|
|
230
|
+
version,
|
|
231
|
+
date: new Date().toISOString().split('T')[0],
|
|
232
|
+
changes: {
|
|
233
|
+
breaking: [],
|
|
234
|
+
features: [],
|
|
235
|
+
fixes: [],
|
|
236
|
+
chore: [],
|
|
237
|
+
docs: [],
|
|
238
|
+
other: []
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
for (const commit of commits) {
|
|
243
|
+
const message = commit.scope
|
|
244
|
+
? `**${commit.scope}**: ${commit.message.split(':').slice(1).join(':').trim()}`
|
|
245
|
+
: commit.message;
|
|
246
|
+
|
|
247
|
+
if (commit.breaking) {
|
|
248
|
+
entry.changes.breaking?.push(message);
|
|
249
|
+
} else if (commit.type === 'feat') {
|
|
250
|
+
entry.changes.features?.push(message);
|
|
251
|
+
} else if (commit.type === 'fix') {
|
|
252
|
+
entry.changes.fixes?.push(message);
|
|
253
|
+
} else if (commit.type === 'chore') {
|
|
254
|
+
entry.changes.chore?.push(message);
|
|
255
|
+
} else if (commit.type === 'docs') {
|
|
256
|
+
entry.changes.docs?.push(message);
|
|
257
|
+
} else {
|
|
258
|
+
entry.changes.other?.push(message);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return entry;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Format changelog entry as markdown
|
|
267
|
+
*/
|
|
268
|
+
private formatChangelogEntry(entry: ChangelogEntry): string {
|
|
269
|
+
let markdown = `## [${entry.version}] - ${entry.date}\n\n`;
|
|
270
|
+
|
|
271
|
+
const sections = [
|
|
272
|
+
{ title: 'BREAKING CHANGES', items: entry.changes.breaking },
|
|
273
|
+
{ title: 'Features', items: entry.changes.features },
|
|
274
|
+
{ title: 'Bug Fixes', items: entry.changes.fixes },
|
|
275
|
+
{ title: 'Documentation', items: entry.changes.docs },
|
|
276
|
+
{ title: 'Chores', items: entry.changes.chore },
|
|
277
|
+
{ title: 'Other Changes', items: entry.changes.other }
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
for (const section of sections) {
|
|
281
|
+
if (section.items && section.items.length > 0) {
|
|
282
|
+
markdown += `### ${section.title}\n\n`;
|
|
283
|
+
for (const item of section.items) {
|
|
284
|
+
markdown += `- ${item}\n`;
|
|
285
|
+
}
|
|
286
|
+
markdown += '\n';
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return markdown;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Update CHANGELOG.md file
|
|
295
|
+
*/
|
|
296
|
+
private updateChangelogFile(path: string, newEntry: string): void {
|
|
297
|
+
const changelogPath = join(this.cwd, path);
|
|
298
|
+
let content = '';
|
|
299
|
+
|
|
300
|
+
if (existsSync(changelogPath)) {
|
|
301
|
+
content = readFileSync(changelogPath, 'utf-8');
|
|
302
|
+
|
|
303
|
+
// Insert after header
|
|
304
|
+
const headerEnd = content.indexOf('\n\n') + 2;
|
|
305
|
+
if (headerEnd > 1) {
|
|
306
|
+
content = content.slice(0, headerEnd) + newEntry + content.slice(headerEnd);
|
|
307
|
+
} else {
|
|
308
|
+
content = newEntry + '\n' + content;
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
content = `# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n${newEntry}`;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
writeFileSync(changelogPath, content);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Execute command
|
|
319
|
+
*/
|
|
320
|
+
private execCommand(cmd: string, returnOutput = false): string {
|
|
321
|
+
try {
|
|
322
|
+
const output = execSync(cmd, {
|
|
323
|
+
cwd: this.cwd,
|
|
324
|
+
encoding: 'utf-8',
|
|
325
|
+
stdio: returnOutput ? 'pipe' : 'inherit'
|
|
326
|
+
});
|
|
327
|
+
return returnOutput ? output : '';
|
|
328
|
+
} catch (error) {
|
|
329
|
+
if (returnOutput && error instanceof Error) {
|
|
330
|
+
return '';
|
|
331
|
+
}
|
|
332
|
+
throw error;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Convenience function to prepare a release
|
|
339
|
+
*/
|
|
340
|
+
export async function prepareRelease(
|
|
341
|
+
options: ReleaseOptions = {}
|
|
342
|
+
): Promise<ReleaseResult> {
|
|
343
|
+
const manager = new ReleaseManager();
|
|
344
|
+
return manager.prepareRelease(options);
|
|
345
|
+
}
|