@manojkmfsi/monodog 1.1.44 → 1.1.48
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/dist/controllers/publish-controller.js +37 -37
- package/dist/middleware/server-startup.js +2 -0
- package/dist/quick-start.js +283 -0
- package/dist/routes/release-api.js +353 -0
- package/dist/services/change-tracker-service.js +227 -0
- package/dist/services/changelog-generator.js +216 -0
- package/dist/services/changeset-service.js +10 -10
- package/dist/services/github-actions-service.js +22 -12
- package/dist/services/npm-publish-service.js +293 -0
- package/dist/services/publish-controller.js +255 -0
- package/dist/services/publish-runners.js +282 -0
- package/dist/services/release-readiness-service.js +249 -0
- package/dist/services/secure-token-service.js +209 -0
- package/dist/services/semver-engine.js +227 -0
- package/dist/setup.js +0 -0
- package/monodog-dashboard/dist/assets/{index-Dc2vaUOq.css → index-BaDCAFTq.css} +1 -1
- package/monodog-dashboard/dist/assets/index-DhnTuHQO.js +20 -0
- package/monodog-dashboard/dist/index.html +2 -2
- package/package.json +19 -18
- package/prisma/schema/change-track.prisma +128 -0
- package/prisma/schema/monodog.db +0 -0
- package/prisma/schema/publish-pipeline.prisma +179 -0
- package/monodog-dashboard/README.md +0 -58
- package/monodog-dashboard/dist/assets/index-C08ciT3A.js +0 -19
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Release API Routes
|
|
4
|
+
* HTTP endpoints for the independent release system
|
|
5
|
+
* Exposes change detection, readiness checks, and publishing operations
|
|
6
|
+
*/
|
|
7
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
8
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
9
|
+
};
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
const express_1 = require("express");
|
|
12
|
+
const publish_controller_1 = require("../services/publish-controller");
|
|
13
|
+
const release_readiness_service_1 = require("../services/release-readiness-service");
|
|
14
|
+
const change_tracker_service_1 = require("../services/change-tracker-service");
|
|
15
|
+
const npm_publish_service_1 = require("../services/npm-publish-service");
|
|
16
|
+
const fs_1 = __importDefault(require("fs"));
|
|
17
|
+
const path_1 = __importDefault(require("path"));
|
|
18
|
+
const router = (0, express_1.Router)();
|
|
19
|
+
/**
|
|
20
|
+
* GET /api/releases/packages
|
|
21
|
+
* Get list of packages available in workspace
|
|
22
|
+
*/
|
|
23
|
+
router.get('/packages', async (req, res) => {
|
|
24
|
+
try {
|
|
25
|
+
const packages = publish_controller_1.publishController.getAvailablePackages(process.cwd());
|
|
26
|
+
res.json({
|
|
27
|
+
success: true,
|
|
28
|
+
packages,
|
|
29
|
+
count: packages.length,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
res.status(500).json({
|
|
34
|
+
success: false,
|
|
35
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
/**
|
|
40
|
+
* POST /api/releases/analyze
|
|
41
|
+
* Analyze changes for packages without publishing
|
|
42
|
+
*/
|
|
43
|
+
router.post('/analyze', async (req, res) => {
|
|
44
|
+
try {
|
|
45
|
+
const { packageNames, packagePaths } = req.body;
|
|
46
|
+
if (!packageNames || !packagePaths) {
|
|
47
|
+
return res.status(400).json({
|
|
48
|
+
success: false,
|
|
49
|
+
error: 'Missing packageNames or packagePaths',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const analysis = {};
|
|
53
|
+
for (const packageName of packageNames) {
|
|
54
|
+
try {
|
|
55
|
+
const packagePath = packagePaths[packageName];
|
|
56
|
+
const currentVersion = getCurrentVersion(packageName, packagePath);
|
|
57
|
+
const changes = await change_tracker_service_1.changeTrackerService.analyzeChanges(packageName, packagePath, currentVersion, '');
|
|
58
|
+
analysis[packageName] = changes;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
analysis[packageName] = {
|
|
62
|
+
error: error instanceof Error ? error.message : 'Analysis failed',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
res.json({
|
|
67
|
+
success: true,
|
|
68
|
+
analysis,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
res.status(500).json({
|
|
73
|
+
success: false,
|
|
74
|
+
error: error instanceof Error ? error.message : 'Analysis failed',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
/**
|
|
79
|
+
* POST /api/releases/check-readiness
|
|
80
|
+
* Check if packages are ready for publishing
|
|
81
|
+
*/
|
|
82
|
+
router.post('/check-readiness', async (req, res) => {
|
|
83
|
+
try {
|
|
84
|
+
const { packageNames, packagePaths } = req.body;
|
|
85
|
+
if (!packageNames || !packagePaths) {
|
|
86
|
+
return res.status(400).json({
|
|
87
|
+
success: false,
|
|
88
|
+
error: 'Missing packageNames or packagePaths',
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
const packages = packageNames.map((name) => ({
|
|
92
|
+
name,
|
|
93
|
+
path: packagePaths[name],
|
|
94
|
+
currentVersion: getCurrentVersion(name, packagePaths[name]),
|
|
95
|
+
}));
|
|
96
|
+
const readiness = await release_readiness_service_1.releaseReadinessService.checkReleaseReadiness(packages);
|
|
97
|
+
res.json({
|
|
98
|
+
success: true,
|
|
99
|
+
canProceed: readiness.canProceed,
|
|
100
|
+
summary: readiness.summary,
|
|
101
|
+
checks: readiness.allChecks,
|
|
102
|
+
globalBlockers: readiness.globalBlockers,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
res.status(500).json({
|
|
107
|
+
success: false,
|
|
108
|
+
error: error instanceof Error ? error.message : 'Readiness check failed',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
/**
|
|
113
|
+
* POST /api/releases/prepare
|
|
114
|
+
* Prepare packages for publishing (validate and analyze)
|
|
115
|
+
*/
|
|
116
|
+
router.post('/prepare', async (req, res) => {
|
|
117
|
+
try {
|
|
118
|
+
const { packageNames, packagePaths, dryRun = false } = req.body;
|
|
119
|
+
if (!packageNames || !packagePaths) {
|
|
120
|
+
return res.status(400).json({
|
|
121
|
+
success: false,
|
|
122
|
+
error: 'Missing packageNames or packagePaths',
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
const result = await publish_controller_1.publishController.preparePublish({
|
|
126
|
+
packageNames,
|
|
127
|
+
packagePaths,
|
|
128
|
+
dryRun,
|
|
129
|
+
});
|
|
130
|
+
res.json({
|
|
131
|
+
success: result.valid,
|
|
132
|
+
canProceed: result.valid,
|
|
133
|
+
readiness: result.readiness,
|
|
134
|
+
analysis: result.analysis,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
res.status(500).json({
|
|
139
|
+
success: false,
|
|
140
|
+
error: error instanceof Error ? error.message : 'Preparation failed',
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
/**
|
|
145
|
+
* GET /api/releases/status/:pipelineId
|
|
146
|
+
* Get status of a publish pipeline
|
|
147
|
+
*/
|
|
148
|
+
router.get('/status/:pipelineId', async (req, res) => {
|
|
149
|
+
try {
|
|
150
|
+
const { pipelineId } = req.params;
|
|
151
|
+
const status = publish_controller_1.publishController.getPipelineStatus(pipelineId);
|
|
152
|
+
if (!status) {
|
|
153
|
+
return res.status(404).json({
|
|
154
|
+
success: false,
|
|
155
|
+
error: `Pipeline not found: ${pipelineId}`,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
res.json({
|
|
159
|
+
success: true,
|
|
160
|
+
pipeline: status,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
res.status(500).json({
|
|
165
|
+
success: false,
|
|
166
|
+
error: error instanceof Error ? error.message : 'Failed to get status',
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
/**
|
|
171
|
+
* GET /api/releases/details/:pipelineId
|
|
172
|
+
* Get detailed information about a pipeline
|
|
173
|
+
*/
|
|
174
|
+
router.get('/details/:pipelineId', async (req, res) => {
|
|
175
|
+
try {
|
|
176
|
+
const { pipelineId } = req.params;
|
|
177
|
+
const details = await publish_controller_1.publishController.getPipelineDetails(pipelineId);
|
|
178
|
+
res.json({
|
|
179
|
+
success: true,
|
|
180
|
+
details,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
res.status(404).json({
|
|
185
|
+
success: false,
|
|
186
|
+
error: error instanceof Error ? error.message : 'Pipeline not found',
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
/**
|
|
191
|
+
* GET /api/releases/pipelines
|
|
192
|
+
* Get all pipelines (with optional filtering)
|
|
193
|
+
*/
|
|
194
|
+
router.get('/pipelines', async (req, res) => {
|
|
195
|
+
try {
|
|
196
|
+
const { status, limit = 50 } = req.query;
|
|
197
|
+
let pipelines = publish_controller_1.publishController.getAllPipelines();
|
|
198
|
+
if (status) {
|
|
199
|
+
pipelines = pipelines.filter(p => p.status === status);
|
|
200
|
+
}
|
|
201
|
+
pipelines = pipelines.slice(0, Number(limit));
|
|
202
|
+
res.json({
|
|
203
|
+
success: true,
|
|
204
|
+
pipelines,
|
|
205
|
+
count: pipelines.length,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
res.status(500).json({
|
|
210
|
+
success: false,
|
|
211
|
+
error: error instanceof Error ? error.message : 'Failed to list pipelines',
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
/**
|
|
216
|
+
* POST /api/releases/start
|
|
217
|
+
* Start a new publish pipeline
|
|
218
|
+
*/
|
|
219
|
+
router.post('/start', async (req, res) => {
|
|
220
|
+
try {
|
|
221
|
+
const { packageNames, packagePaths, versionMap, method = 'auto', dryRun = false, autoTag = true, createReleases = false, } = req.body;
|
|
222
|
+
if (!packageNames || !packagePaths) {
|
|
223
|
+
return res.status(400).json({
|
|
224
|
+
success: false,
|
|
225
|
+
error: 'Missing packageNames or packagePaths',
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
console.info(`📦 Starting publish pipeline for: ${packageNames.join(', ')}`);
|
|
229
|
+
const pipeline = await publish_controller_1.publishController.publish({
|
|
230
|
+
packageNames,
|
|
231
|
+
packagePaths,
|
|
232
|
+
versionMap,
|
|
233
|
+
method,
|
|
234
|
+
dryRun,
|
|
235
|
+
autoTag,
|
|
236
|
+
createReleases,
|
|
237
|
+
});
|
|
238
|
+
res.status(201).json({
|
|
239
|
+
success: pipeline.status !== 'failed',
|
|
240
|
+
pipeline,
|
|
241
|
+
pipelineId: pipeline.pipelineId,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
res.status(500).json({
|
|
246
|
+
success: false,
|
|
247
|
+
error: error instanceof Error ? error.message : 'Failed to start publish',
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
/**
|
|
252
|
+
* POST /api/releases/cancel/:pipelineId
|
|
253
|
+
* Cancel a publishing pipeline
|
|
254
|
+
*/
|
|
255
|
+
router.post('/cancel/:pipelineId', async (req, res) => {
|
|
256
|
+
try {
|
|
257
|
+
const { pipelineId } = req.params;
|
|
258
|
+
await publish_controller_1.publishController.cancelPipeline(pipelineId);
|
|
259
|
+
res.json({
|
|
260
|
+
success: true,
|
|
261
|
+
message: `Pipeline ${pipelineId} cancelled`,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
res.status(400).json({
|
|
266
|
+
success: false,
|
|
267
|
+
error: error instanceof Error ? error.message : 'Failed to cancel pipeline',
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
/**
|
|
272
|
+
* GET /api/releases/npm/:packageName/versions
|
|
273
|
+
* Get published versions for a package
|
|
274
|
+
*/
|
|
275
|
+
router.get('/npm/:packageName/versions', async (req, res) => {
|
|
276
|
+
try {
|
|
277
|
+
const { packageName } = req.params;
|
|
278
|
+
const versions = await npm_publish_service_1.npmPublishService.getPublishedVersions(packageName);
|
|
279
|
+
res.json({
|
|
280
|
+
success: true,
|
|
281
|
+
packageName,
|
|
282
|
+
versions,
|
|
283
|
+
count: versions.length,
|
|
284
|
+
latest: versions[0] || null,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
res.status(500).json({
|
|
289
|
+
success: false,
|
|
290
|
+
error: error instanceof Error ? error.message : 'Failed to get versions',
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
/**
|
|
295
|
+
* GET /api/releases/npm/:packageName/:version/available
|
|
296
|
+
* Check if a version is available on npm
|
|
297
|
+
*/
|
|
298
|
+
router.get('/npm/:packageName/:version/available', async (req, res) => {
|
|
299
|
+
try {
|
|
300
|
+
const { packageName, version } = req.params;
|
|
301
|
+
const available = await npm_publish_service_1.npmPublishService.isVersionAvailable(packageName, version);
|
|
302
|
+
res.json({
|
|
303
|
+
success: true,
|
|
304
|
+
packageName,
|
|
305
|
+
version,
|
|
306
|
+
available,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
res.status(500).json({
|
|
311
|
+
success: false,
|
|
312
|
+
error: error instanceof Error ? error.message : 'Failed to check version',
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
/**
|
|
317
|
+
* GET /api/releases/health
|
|
318
|
+
* Health check for release system
|
|
319
|
+
*/
|
|
320
|
+
router.get('/health', async (req, res) => {
|
|
321
|
+
try {
|
|
322
|
+
const pipelines = publish_controller_1.publishController.getAllPipelines();
|
|
323
|
+
const activePipelines = pipelines.filter(p => p.status === 'publishing' || p.status === 'validating');
|
|
324
|
+
res.json({
|
|
325
|
+
success: true,
|
|
326
|
+
status: 'healthy',
|
|
327
|
+
activePipelines: activePipelines.length,
|
|
328
|
+
totalPipelines: pipelines.length,
|
|
329
|
+
timestamp: new Date().toISOString(),
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
res.status(500).json({
|
|
334
|
+
success: false,
|
|
335
|
+
error: 'Release system health check failed',
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
/**
|
|
340
|
+
* Helper function to get current version
|
|
341
|
+
*/
|
|
342
|
+
function getCurrentVersion(packageName, packagePath) {
|
|
343
|
+
try {
|
|
344
|
+
const pkgJsonPath = path_1.default.join(packagePath, 'package.json');
|
|
345
|
+
const content = fs_1.default.readFileSync(pkgJsonPath, 'utf8');
|
|
346
|
+
const pkgJson = JSON.parse(content);
|
|
347
|
+
return pkgJson.version || '0.0.0';
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
return '0.0.0';
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
exports.default = router;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Change Tracker Service
|
|
4
|
+
* Detects changes in packages using Conventional Commits and Git diffs
|
|
5
|
+
* Replaces dependency on .changeset/*.md files
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.changeTrackerService = exports.ChangeTrackerService = void 0;
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const util_1 = require("util");
|
|
11
|
+
// NOTE: ChangeTrack and CommitChange types will be imported from @prisma/client after DB integration
|
|
12
|
+
// Currently using inline types below for service implementation
|
|
13
|
+
const execAsync = (0, util_1.promisify)(child_process_1.exec);
|
|
14
|
+
class ChangeTrackerService {
|
|
15
|
+
/**
|
|
16
|
+
* Analyze changes for a package since last release
|
|
17
|
+
*/
|
|
18
|
+
async analyzeChanges(packageName, packagePath, currentVersion, lastTagName = `${packageName}@${currentVersion}`) {
|
|
19
|
+
try {
|
|
20
|
+
// 1. Get commits since last tag
|
|
21
|
+
const commits = await this.getCommitsSinceTag(lastTagName, packagePath);
|
|
22
|
+
// 2. Get file diffs
|
|
23
|
+
const filesChanged = await this.getFileDiffsSinceTag(lastTagName, packagePath);
|
|
24
|
+
// 3. Determine change type from commits
|
|
25
|
+
const changeType = this.determineChangeType(commits);
|
|
26
|
+
// 4. Identify affected dependents
|
|
27
|
+
const affectedDependents = await this.identifyAffectedDependents(packageName);
|
|
28
|
+
return {
|
|
29
|
+
packageName,
|
|
30
|
+
currentVersion,
|
|
31
|
+
changeType,
|
|
32
|
+
commits,
|
|
33
|
+
filesChanged,
|
|
34
|
+
affectedDependents,
|
|
35
|
+
proposedVersion: this.calculateNextVersion(currentVersion, changeType),
|
|
36
|
+
isReleaseReady: changeType !== 'none',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
console.error(`Failed to analyze changes for ${packageName}:`, error);
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Parse commit message using Conventional Commits format
|
|
46
|
+
* https://www.conventionalcommits.org/
|
|
47
|
+
*
|
|
48
|
+
* Format: type(scope)!: subject
|
|
49
|
+
* BREAKING CHANGE: description
|
|
50
|
+
*/
|
|
51
|
+
parseConventionalCommit(message, body) {
|
|
52
|
+
const conventionalRegex = /^(feat|fix|docs|style|refactor|test|chore|perf|ci|revert|build)(\(.+\))?(!)?:\s(.+)/;
|
|
53
|
+
const match = message.match(conventionalRegex);
|
|
54
|
+
if (!match) {
|
|
55
|
+
return {
|
|
56
|
+
type: 'chore',
|
|
57
|
+
scope: undefined,
|
|
58
|
+
isBreaking: false,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const [, type, scopeMatch, breakingIndicator, subject] = match;
|
|
62
|
+
const scope = scopeMatch ? scopeMatch.slice(1, -1) : undefined;
|
|
63
|
+
// Check for BREAKING CHANGE keyword in body
|
|
64
|
+
const isBreaking = !!breakingIndicator || (body ? /^BREAKING[\s-]CHANGE:/m.test(body) : false);
|
|
65
|
+
return {
|
|
66
|
+
type: type,
|
|
67
|
+
scope,
|
|
68
|
+
isBreaking,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Get commits since last tag
|
|
73
|
+
*/
|
|
74
|
+
async getCommitsSinceTag(tagName, packagePath) {
|
|
75
|
+
try {
|
|
76
|
+
// Get commits since tag, limited to this package path
|
|
77
|
+
const { stdout } = await execAsync(`cd ${packagePath} && git log ${tagName}..HEAD --format='%H|||%ae|||%an|||%ai|||%s|||%b' --`, { shell: '/bin/bash' });
|
|
78
|
+
if (!stdout.trim()) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
const commits = stdout
|
|
82
|
+
.split('\n')
|
|
83
|
+
.filter(line => line.trim())
|
|
84
|
+
.map(line => {
|
|
85
|
+
const [hash, email, author, timestamp, subject, body] = line.split('|||');
|
|
86
|
+
const parsed = this.parseConventionalCommit(subject, body);
|
|
87
|
+
return {
|
|
88
|
+
hash,
|
|
89
|
+
message: subject,
|
|
90
|
+
author: author || email,
|
|
91
|
+
authorEmail: email,
|
|
92
|
+
committedAt: new Date(timestamp),
|
|
93
|
+
type: parsed.type,
|
|
94
|
+
scope: parsed.scope,
|
|
95
|
+
isBreaking: parsed.isBreaking,
|
|
96
|
+
body: body?.trim() || undefined,
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
return commits;
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
// If tag doesn't exist, get all commits (first release)
|
|
103
|
+
console.warn(`Tag ${tagName} not found, analyzing all commits`);
|
|
104
|
+
return this.getAllCommits(packagePath);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Get all commits for a package
|
|
109
|
+
*/
|
|
110
|
+
async getAllCommits(packagePath) {
|
|
111
|
+
try {
|
|
112
|
+
const { stdout } = await execAsync(`cd ${packagePath} && git log --format='%H|||%ae|||%an|||%ai|||%s|||%b' --`, { shell: '/bin/bash' });
|
|
113
|
+
if (!stdout.trim()) {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
return stdout
|
|
117
|
+
.split('\n')
|
|
118
|
+
.filter(line => line.trim())
|
|
119
|
+
.map(line => {
|
|
120
|
+
const [hash, email, author, timestamp, subject, body] = line.split('|||');
|
|
121
|
+
const parsed = this.parseConventionalCommit(subject, body);
|
|
122
|
+
return {
|
|
123
|
+
hash,
|
|
124
|
+
message: subject,
|
|
125
|
+
author: author || email,
|
|
126
|
+
authorEmail: email,
|
|
127
|
+
committedAt: new Date(timestamp),
|
|
128
|
+
type: parsed.type,
|
|
129
|
+
scope: parsed.scope,
|
|
130
|
+
isBreaking: parsed.isBreaking,
|
|
131
|
+
body: body?.trim() || undefined,
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
console.error(`Failed to get commits for ${packagePath}:`, error);
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Get file diffs since last tag
|
|
142
|
+
*/
|
|
143
|
+
async getFileDiffsSinceTag(tagName, packagePath) {
|
|
144
|
+
try {
|
|
145
|
+
// Get file stats in diff format
|
|
146
|
+
const { stdout } = await execAsync(`cd ${packagePath} && git diff --no-renames --numstat ${tagName}..HEAD --`, { shell: '/bin/bash' });
|
|
147
|
+
if (!stdout.trim()) {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
return stdout
|
|
151
|
+
.split('\n')
|
|
152
|
+
.filter(line => line.trim())
|
|
153
|
+
.map(line => {
|
|
154
|
+
const [added, removed, filePath] = line.split('\t');
|
|
155
|
+
return {
|
|
156
|
+
path: filePath,
|
|
157
|
+
added: false,
|
|
158
|
+
removed: false,
|
|
159
|
+
modified: true,
|
|
160
|
+
linesAdded: parseInt(added) || 0,
|
|
161
|
+
linesRemoved: parseInt(removed) || 0,
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
console.warn(`Failed to get diffs for ${packagePath}:`, error);
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Determine change type based on commits
|
|
172
|
+
* Rules:
|
|
173
|
+
* - BREAKING CHANGE or ! → major
|
|
174
|
+
* - feat: → minor
|
|
175
|
+
* - fix:, refactor:, etc → patch
|
|
176
|
+
* - docs:, chore:, etc → none
|
|
177
|
+
*/
|
|
178
|
+
determineChangeType(commits) {
|
|
179
|
+
if (commits.length === 0) {
|
|
180
|
+
return 'none';
|
|
181
|
+
}
|
|
182
|
+
// Check for breaking changes first
|
|
183
|
+
if (commits.some(c => c.isBreaking)) {
|
|
184
|
+
return 'major';
|
|
185
|
+
}
|
|
186
|
+
// Check for features
|
|
187
|
+
if (commits.some(c => c.type === 'feat')) {
|
|
188
|
+
return 'minor';
|
|
189
|
+
}
|
|
190
|
+
// Check for fixes and other meaningful changes
|
|
191
|
+
if (commits.some(c => ['fix', 'refactor', 'perf'].includes(c.type))) {
|
|
192
|
+
return 'patch';
|
|
193
|
+
}
|
|
194
|
+
// Everything else (docs, chore, style, test, etc) doesn't warrant a release
|
|
195
|
+
return 'none';
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Calculate next semantic version
|
|
199
|
+
*/
|
|
200
|
+
calculateNextVersion(currentVersion, changeType) {
|
|
201
|
+
if (changeType === 'none') {
|
|
202
|
+
return currentVersion;
|
|
203
|
+
}
|
|
204
|
+
const parts = currentVersion.split('.');
|
|
205
|
+
const [major, minor, patch] = parts.map(p => parseInt(p));
|
|
206
|
+
switch (changeType) {
|
|
207
|
+
case 'major':
|
|
208
|
+
return `${major + 1}.0.0`;
|
|
209
|
+
case 'minor':
|
|
210
|
+
return `${major}.${minor + 1}.0`;
|
|
211
|
+
case 'patch':
|
|
212
|
+
return `${major}.${minor}.${patch + 1}`;
|
|
213
|
+
default:
|
|
214
|
+
return currentVersion;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Identify packages that depend on this package
|
|
219
|
+
*/
|
|
220
|
+
async identifyAffectedDependents(packageName) {
|
|
221
|
+
// TODO: Implement dependency graph analysis
|
|
222
|
+
// For now, return empty array - can be enhanced with dependency resolver
|
|
223
|
+
return [];
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
exports.ChangeTrackerService = ChangeTrackerService;
|
|
227
|
+
exports.changeTrackerService = new ChangeTrackerService();
|