musubi-sdd 6.2.0 → 6.2.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/README.ja.md +60 -1
- package/README.md +60 -1
- package/bin/musubi-dashboard.js +340 -0
- package/package.json +3 -2
- package/src/cli/dashboard-cli.js +536 -0
- package/src/constitutional/checker.js +633 -0
- package/src/constitutional/ci-reporter.js +336 -0
- package/src/constitutional/index.js +22 -0
- package/src/constitutional/phase-minus-one.js +404 -0
- package/src/constitutional/steering-sync.js +473 -0
- package/src/dashboard/index.js +20 -0
- package/src/dashboard/sprint-planner.js +361 -0
- package/src/dashboard/sprint-reporter.js +378 -0
- package/src/dashboard/transition-recorder.js +209 -0
- package/src/dashboard/workflow-dashboard.js +434 -0
- package/src/enterprise/error-recovery.js +524 -0
- package/src/enterprise/experiment-report.js +573 -0
- package/src/enterprise/index.js +57 -4
- package/src/enterprise/rollback-manager.js +584 -0
- package/src/enterprise/tech-article.js +509 -0
- package/src/traceability/extractor.js +294 -0
- package/src/traceability/gap-detector.js +230 -0
- package/src/traceability/index.js +15 -0
- package/src/traceability/matrix-storage.js +368 -0
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rollback Manager
|
|
3
|
+
*
|
|
4
|
+
* Supports rollback to previous state with cleanup.
|
|
5
|
+
*
|
|
6
|
+
* Requirement: IMP-6.2-008-02
|
|
7
|
+
*
|
|
8
|
+
* @module enterprise/rollback-manager
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs').promises;
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { execSync } = require('child_process');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Rollback granularity levels
|
|
17
|
+
*/
|
|
18
|
+
const ROLLBACK_LEVEL = {
|
|
19
|
+
FILE: 'file',
|
|
20
|
+
COMMIT: 'commit',
|
|
21
|
+
STAGE: 'stage',
|
|
22
|
+
SPRINT: 'sprint'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Rollback status
|
|
27
|
+
*/
|
|
28
|
+
const ROLLBACK_STATUS = {
|
|
29
|
+
PENDING: 'pending',
|
|
30
|
+
IN_PROGRESS: 'in-progress',
|
|
31
|
+
COMPLETED: 'completed',
|
|
32
|
+
FAILED: 'failed',
|
|
33
|
+
CANCELLED: 'cancelled'
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Workflow stages
|
|
38
|
+
*/
|
|
39
|
+
const WORKFLOW_STAGE = {
|
|
40
|
+
REQUIREMENTS: 'requirements',
|
|
41
|
+
DESIGN: 'design',
|
|
42
|
+
TASKS: 'tasks',
|
|
43
|
+
IMPLEMENT: 'implement',
|
|
44
|
+
VALIDATE: 'validate'
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Rollback Manager
|
|
49
|
+
*/
|
|
50
|
+
class RollbackManager {
|
|
51
|
+
/**
|
|
52
|
+
* Create a new RollbackManager
|
|
53
|
+
* @param {Object} config - Configuration options
|
|
54
|
+
*/
|
|
55
|
+
constructor(config = {}) {
|
|
56
|
+
this.config = {
|
|
57
|
+
storageDir: config.storageDir || 'storage/rollbacks',
|
|
58
|
+
backupDir: config.backupDir || 'storage/backups',
|
|
59
|
+
maxHistory: config.maxHistory || 50,
|
|
60
|
+
requireConfirmation: config.requireConfirmation !== false,
|
|
61
|
+
gitEnabled: config.gitEnabled !== false,
|
|
62
|
+
...config
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
this.rollbackHistory = [];
|
|
66
|
+
this.checkpoints = new Map();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Create a checkpoint for rollback
|
|
71
|
+
* @param {string} name - Checkpoint name
|
|
72
|
+
* @param {Object} options - Checkpoint options
|
|
73
|
+
* @returns {Promise<Object>} Checkpoint info
|
|
74
|
+
*/
|
|
75
|
+
async createCheckpoint(name, options = {}) {
|
|
76
|
+
const checkpoint = {
|
|
77
|
+
id: this.generateId('ckpt'),
|
|
78
|
+
name,
|
|
79
|
+
timestamp: new Date().toISOString(),
|
|
80
|
+
level: options.level || ROLLBACK_LEVEL.COMMIT,
|
|
81
|
+
stage: options.stage || WORKFLOW_STAGE.IMPLEMENT,
|
|
82
|
+
description: options.description || '',
|
|
83
|
+
files: [],
|
|
84
|
+
gitRef: null,
|
|
85
|
+
metadata: options.metadata || {}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Capture current state
|
|
89
|
+
if (options.files && options.files.length > 0) {
|
|
90
|
+
checkpoint.files = await this.captureFiles(options.files);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Capture git ref if enabled
|
|
94
|
+
if (this.config.gitEnabled) {
|
|
95
|
+
checkpoint.gitRef = this.getCurrentGitRef();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Store checkpoint
|
|
99
|
+
this.checkpoints.set(checkpoint.id, checkpoint);
|
|
100
|
+
await this.saveCheckpoint(checkpoint);
|
|
101
|
+
|
|
102
|
+
return checkpoint;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Capture file contents for backup
|
|
107
|
+
* @param {Array<string>} files - File paths
|
|
108
|
+
* @returns {Promise<Array>} File backups
|
|
109
|
+
*/
|
|
110
|
+
async captureFiles(files) {
|
|
111
|
+
const backups = [];
|
|
112
|
+
|
|
113
|
+
for (const filePath of files) {
|
|
114
|
+
try {
|
|
115
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
116
|
+
const stats = await fs.stat(filePath);
|
|
117
|
+
|
|
118
|
+
backups.push({
|
|
119
|
+
path: filePath,
|
|
120
|
+
content,
|
|
121
|
+
mode: stats.mode,
|
|
122
|
+
mtime: stats.mtime.toISOString(),
|
|
123
|
+
size: stats.size
|
|
124
|
+
});
|
|
125
|
+
} catch (error) {
|
|
126
|
+
// File doesn't exist or can't be read
|
|
127
|
+
backups.push({
|
|
128
|
+
path: filePath,
|
|
129
|
+
exists: false,
|
|
130
|
+
error: error.message
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return backups;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get current git reference
|
|
140
|
+
* @returns {string|null} Git ref
|
|
141
|
+
*/
|
|
142
|
+
getCurrentGitRef() {
|
|
143
|
+
try {
|
|
144
|
+
return execSync('git rev-parse HEAD', { encoding: 'utf-8' }).trim();
|
|
145
|
+
} catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Rollback to checkpoint
|
|
152
|
+
* @param {string} checkpointId - Checkpoint ID
|
|
153
|
+
* @param {Object} options - Rollback options
|
|
154
|
+
* @returns {Promise<Object>} Rollback result
|
|
155
|
+
*/
|
|
156
|
+
async rollback(checkpointId, options = {}) {
|
|
157
|
+
const checkpoint = this.checkpoints.get(checkpointId) || await this.loadCheckpoint(checkpointId);
|
|
158
|
+
|
|
159
|
+
if (!checkpoint) {
|
|
160
|
+
throw new Error(`Checkpoint not found: ${checkpointId}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Create rollback record
|
|
164
|
+
const rollback = {
|
|
165
|
+
id: this.generateId('rb'),
|
|
166
|
+
checkpointId,
|
|
167
|
+
timestamp: new Date().toISOString(),
|
|
168
|
+
status: ROLLBACK_STATUS.PENDING,
|
|
169
|
+
level: checkpoint.level,
|
|
170
|
+
beforeState: null,
|
|
171
|
+
changes: [],
|
|
172
|
+
options
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// Require confirmation if enabled
|
|
176
|
+
if (this.config.requireConfirmation && !options.confirmed) {
|
|
177
|
+
return {
|
|
178
|
+
...rollback,
|
|
179
|
+
status: ROLLBACK_STATUS.PENDING,
|
|
180
|
+
requiresConfirmation: true,
|
|
181
|
+
checkpoint: this.summarizeCheckpoint(checkpoint)
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
rollback.status = ROLLBACK_STATUS.IN_PROGRESS;
|
|
187
|
+
|
|
188
|
+
// Capture current state before rollback
|
|
189
|
+
if (checkpoint.files.length > 0) {
|
|
190
|
+
rollback.beforeState = await this.captureFiles(
|
|
191
|
+
checkpoint.files.map(f => f.path)
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Perform rollback based on level
|
|
196
|
+
switch (checkpoint.level) {
|
|
197
|
+
case ROLLBACK_LEVEL.FILE:
|
|
198
|
+
rollback.changes = await this.rollbackFiles(checkpoint);
|
|
199
|
+
break;
|
|
200
|
+
case ROLLBACK_LEVEL.COMMIT:
|
|
201
|
+
rollback.changes = await this.rollbackToCommit(checkpoint);
|
|
202
|
+
break;
|
|
203
|
+
case ROLLBACK_LEVEL.STAGE:
|
|
204
|
+
rollback.changes = await this.rollbackToStage(checkpoint);
|
|
205
|
+
break;
|
|
206
|
+
case ROLLBACK_LEVEL.SPRINT:
|
|
207
|
+
rollback.changes = await this.rollbackToSprint(checkpoint);
|
|
208
|
+
break;
|
|
209
|
+
default:
|
|
210
|
+
throw new Error(`Unknown rollback level: ${checkpoint.level}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
rollback.status = ROLLBACK_STATUS.COMPLETED;
|
|
214
|
+
} catch (error) {
|
|
215
|
+
rollback.status = ROLLBACK_STATUS.FAILED;
|
|
216
|
+
rollback.error = error.message;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Record rollback
|
|
220
|
+
this.rollbackHistory.unshift(rollback);
|
|
221
|
+
await this.saveRollback(rollback);
|
|
222
|
+
|
|
223
|
+
return rollback;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Rollback files to checkpoint state
|
|
228
|
+
* @param {Object} checkpoint - Checkpoint data
|
|
229
|
+
* @returns {Promise<Array>} Changes made
|
|
230
|
+
*/
|
|
231
|
+
async rollbackFiles(checkpoint) {
|
|
232
|
+
const changes = [];
|
|
233
|
+
|
|
234
|
+
for (const file of checkpoint.files) {
|
|
235
|
+
try {
|
|
236
|
+
if (file.exists === false) {
|
|
237
|
+
// File didn't exist at checkpoint - delete if exists now
|
|
238
|
+
try {
|
|
239
|
+
await fs.unlink(file.path);
|
|
240
|
+
changes.push({ path: file.path, action: 'deleted' });
|
|
241
|
+
} catch {
|
|
242
|
+
// File doesn't exist, nothing to delete
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
// Restore file content
|
|
246
|
+
await fs.mkdir(path.dirname(file.path), { recursive: true });
|
|
247
|
+
await fs.writeFile(file.path, file.content, 'utf-8');
|
|
248
|
+
changes.push({ path: file.path, action: 'restored' });
|
|
249
|
+
}
|
|
250
|
+
} catch (error) {
|
|
251
|
+
changes.push({ path: file.path, action: 'failed', error: error.message });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return changes;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Rollback to git commit
|
|
260
|
+
* @param {Object} checkpoint - Checkpoint data
|
|
261
|
+
* @returns {Promise<Array>} Changes made
|
|
262
|
+
*/
|
|
263
|
+
async rollbackToCommit(checkpoint) {
|
|
264
|
+
const changes = [];
|
|
265
|
+
|
|
266
|
+
if (!this.config.gitEnabled || !checkpoint.gitRef) {
|
|
267
|
+
throw new Error('Git rollback not available');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
// Create backup branch
|
|
272
|
+
const backupBranch = `backup-${Date.now()}`;
|
|
273
|
+
execSync(`git branch ${backupBranch}`, { encoding: 'utf-8' });
|
|
274
|
+
changes.push({ action: 'branch-created', branch: backupBranch });
|
|
275
|
+
|
|
276
|
+
// Reset to checkpoint
|
|
277
|
+
execSync(`git reset --hard ${checkpoint.gitRef}`, { encoding: 'utf-8' });
|
|
278
|
+
changes.push({ action: 'reset', ref: checkpoint.gitRef });
|
|
279
|
+
} catch (error) {
|
|
280
|
+
changes.push({ action: 'failed', error: error.message });
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return changes;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Rollback to workflow stage
|
|
288
|
+
* @param {Object} checkpoint - Checkpoint data
|
|
289
|
+
* @returns {Promise<Array>} Changes made
|
|
290
|
+
*/
|
|
291
|
+
async rollbackToStage(checkpoint) {
|
|
292
|
+
const changes = [];
|
|
293
|
+
|
|
294
|
+
// Rollback files first
|
|
295
|
+
if (checkpoint.files.length > 0) {
|
|
296
|
+
const fileChanges = await this.rollbackFiles(checkpoint);
|
|
297
|
+
changes.push(...fileChanges);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Update workflow state
|
|
301
|
+
changes.push({ action: 'stage-reset', stage: checkpoint.stage });
|
|
302
|
+
|
|
303
|
+
return changes;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Rollback to sprint start
|
|
308
|
+
* @param {Object} checkpoint - Checkpoint data
|
|
309
|
+
* @returns {Promise<Array>} Changes made
|
|
310
|
+
*/
|
|
311
|
+
async rollbackToSprint(checkpoint) {
|
|
312
|
+
const changes = [];
|
|
313
|
+
|
|
314
|
+
// Full rollback including git and files
|
|
315
|
+
if (checkpoint.gitRef && this.config.gitEnabled) {
|
|
316
|
+
const commitChanges = await this.rollbackToCommit(checkpoint);
|
|
317
|
+
changes.push(...commitChanges);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (checkpoint.files.length > 0) {
|
|
321
|
+
const fileChanges = await this.rollbackFiles(checkpoint);
|
|
322
|
+
changes.push(...fileChanges);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
changes.push({ action: 'sprint-reset', metadata: checkpoint.metadata });
|
|
326
|
+
|
|
327
|
+
return changes;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Cancel pending rollback
|
|
332
|
+
* @param {string} rollbackId - Rollback ID
|
|
333
|
+
* @returns {Object} Updated rollback
|
|
334
|
+
*/
|
|
335
|
+
cancelRollback(rollbackId) {
|
|
336
|
+
const rollback = this.rollbackHistory.find(r => r.id === rollbackId);
|
|
337
|
+
|
|
338
|
+
if (!rollback) {
|
|
339
|
+
throw new Error(`Rollback not found: ${rollbackId}`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (rollback.status !== ROLLBACK_STATUS.PENDING) {
|
|
343
|
+
throw new Error(`Cannot cancel rollback in status: ${rollback.status}`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
rollback.status = ROLLBACK_STATUS.CANCELLED;
|
|
347
|
+
rollback.cancelledAt = new Date().toISOString();
|
|
348
|
+
|
|
349
|
+
return rollback;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Get rollback history
|
|
354
|
+
* @param {Object} filter - Filter options
|
|
355
|
+
* @returns {Array} Filtered history
|
|
356
|
+
*/
|
|
357
|
+
getHistory(filter = {}) {
|
|
358
|
+
let history = [...this.rollbackHistory];
|
|
359
|
+
|
|
360
|
+
if (filter.status) {
|
|
361
|
+
history = history.filter(r => r.status === filter.status);
|
|
362
|
+
}
|
|
363
|
+
if (filter.level) {
|
|
364
|
+
history = history.filter(r => r.level === filter.level);
|
|
365
|
+
}
|
|
366
|
+
if (filter.limit) {
|
|
367
|
+
history = history.slice(0, filter.limit);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return history;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* List checkpoints
|
|
375
|
+
* @param {Object} filter - Filter options
|
|
376
|
+
* @returns {Array} Checkpoints
|
|
377
|
+
*/
|
|
378
|
+
listCheckpoints(filter = {}) {
|
|
379
|
+
let checkpoints = Array.from(this.checkpoints.values());
|
|
380
|
+
|
|
381
|
+
if (filter.level) {
|
|
382
|
+
checkpoints = checkpoints.filter(c => c.level === filter.level);
|
|
383
|
+
}
|
|
384
|
+
if (filter.stage) {
|
|
385
|
+
checkpoints = checkpoints.filter(c => c.stage === filter.stage);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return checkpoints.map(c => this.summarizeCheckpoint(c));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Summarize checkpoint for display
|
|
393
|
+
* @param {Object} checkpoint - Checkpoint data
|
|
394
|
+
* @returns {Object} Summary
|
|
395
|
+
*/
|
|
396
|
+
summarizeCheckpoint(checkpoint) {
|
|
397
|
+
return {
|
|
398
|
+
id: checkpoint.id,
|
|
399
|
+
name: checkpoint.name,
|
|
400
|
+
timestamp: checkpoint.timestamp,
|
|
401
|
+
level: checkpoint.level,
|
|
402
|
+
stage: checkpoint.stage,
|
|
403
|
+
description: checkpoint.description,
|
|
404
|
+
fileCount: checkpoint.files.length,
|
|
405
|
+
hasGitRef: !!checkpoint.gitRef
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Save checkpoint to storage
|
|
411
|
+
* @param {Object} checkpoint - Checkpoint data
|
|
412
|
+
* @returns {Promise<string>} File path
|
|
413
|
+
*/
|
|
414
|
+
async saveCheckpoint(checkpoint) {
|
|
415
|
+
await this.ensureStorageDir();
|
|
416
|
+
|
|
417
|
+
const fileName = `checkpoint-${checkpoint.id}.json`;
|
|
418
|
+
const filePath = path.join(this.config.storageDir, fileName);
|
|
419
|
+
|
|
420
|
+
await fs.writeFile(filePath, JSON.stringify(checkpoint, null, 2), 'utf-8');
|
|
421
|
+
return filePath;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Load checkpoint from storage
|
|
426
|
+
* @param {string} id - Checkpoint ID
|
|
427
|
+
* @returns {Promise<Object|null>} Checkpoint or null
|
|
428
|
+
*/
|
|
429
|
+
async loadCheckpoint(id) {
|
|
430
|
+
const fileName = `checkpoint-${id}.json`;
|
|
431
|
+
const filePath = path.join(this.config.storageDir, fileName);
|
|
432
|
+
|
|
433
|
+
try {
|
|
434
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
435
|
+
const checkpoint = JSON.parse(content);
|
|
436
|
+
this.checkpoints.set(id, checkpoint);
|
|
437
|
+
return checkpoint;
|
|
438
|
+
} catch {
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Save rollback record
|
|
445
|
+
* @param {Object} rollback - Rollback data
|
|
446
|
+
* @returns {Promise<string>} File path
|
|
447
|
+
*/
|
|
448
|
+
async saveRollback(rollback) {
|
|
449
|
+
await this.ensureStorageDir();
|
|
450
|
+
|
|
451
|
+
const fileName = `rollback-${rollback.id}.json`;
|
|
452
|
+
const filePath = path.join(this.config.storageDir, fileName);
|
|
453
|
+
|
|
454
|
+
await fs.writeFile(filePath, JSON.stringify(rollback, null, 2), 'utf-8');
|
|
455
|
+
return filePath;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Ensure storage directory exists
|
|
460
|
+
* @returns {Promise<void>}
|
|
461
|
+
*/
|
|
462
|
+
async ensureStorageDir() {
|
|
463
|
+
await fs.mkdir(this.config.storageDir, { recursive: true });
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Generate unique ID
|
|
468
|
+
* @param {string} prefix - ID prefix
|
|
469
|
+
* @returns {string} Unique ID
|
|
470
|
+
*/
|
|
471
|
+
generateId(prefix = 'id') {
|
|
472
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Generate rollback report
|
|
477
|
+
* @param {string} rollbackId - Rollback ID
|
|
478
|
+
* @returns {string} Markdown report
|
|
479
|
+
*/
|
|
480
|
+
generateReport(rollbackId) {
|
|
481
|
+
const rollback = this.rollbackHistory.find(r => r.id === rollbackId);
|
|
482
|
+
|
|
483
|
+
if (!rollback) {
|
|
484
|
+
throw new Error(`Rollback not found: ${rollbackId}`);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const lines = [];
|
|
488
|
+
|
|
489
|
+
lines.push('# Rollback Report');
|
|
490
|
+
lines.push('');
|
|
491
|
+
lines.push(`**Rollback ID**: ${rollback.id}`);
|
|
492
|
+
lines.push(`**Checkpoint ID**: ${rollback.checkpointId}`);
|
|
493
|
+
lines.push(`**Timestamp**: ${rollback.timestamp}`);
|
|
494
|
+
lines.push(`**Status**: ${rollback.status}`);
|
|
495
|
+
lines.push(`**Level**: ${rollback.level}`);
|
|
496
|
+
lines.push('');
|
|
497
|
+
|
|
498
|
+
if (rollback.status === ROLLBACK_STATUS.COMPLETED) {
|
|
499
|
+
lines.push('## Changes Applied');
|
|
500
|
+
lines.push('');
|
|
501
|
+
lines.push('| Path/Action | Result |');
|
|
502
|
+
lines.push('|-------------|--------|');
|
|
503
|
+
for (const change of rollback.changes) {
|
|
504
|
+
if (change.path) {
|
|
505
|
+
lines.push(`| ${change.path} | ${change.action} |`);
|
|
506
|
+
} else {
|
|
507
|
+
lines.push(`| ${change.action} | ${change.ref || change.branch || 'OK'} |`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
lines.push('');
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (rollback.status === ROLLBACK_STATUS.FAILED) {
|
|
514
|
+
lines.push('## Error');
|
|
515
|
+
lines.push('');
|
|
516
|
+
lines.push(`\`\`\`\n${rollback.error}\n\`\`\``);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return lines.join('\n');
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Delete checkpoint
|
|
524
|
+
* @param {string} checkpointId - Checkpoint ID
|
|
525
|
+
* @returns {Promise<boolean>} Success
|
|
526
|
+
*/
|
|
527
|
+
async deleteCheckpoint(checkpointId) {
|
|
528
|
+
this.checkpoints.delete(checkpointId);
|
|
529
|
+
|
|
530
|
+
const fileName = `checkpoint-${checkpointId}.json`;
|
|
531
|
+
const filePath = path.join(this.config.storageDir, fileName);
|
|
532
|
+
|
|
533
|
+
try {
|
|
534
|
+
await fs.unlink(filePath);
|
|
535
|
+
return true;
|
|
536
|
+
} catch {
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Clean old checkpoints
|
|
543
|
+
* @param {number} maxAge - Max age in days
|
|
544
|
+
* @returns {Promise<number>} Number deleted
|
|
545
|
+
*/
|
|
546
|
+
async cleanOldCheckpoints(maxAge = 30) {
|
|
547
|
+
const cutoff = new Date();
|
|
548
|
+
cutoff.setDate(cutoff.getDate() - maxAge);
|
|
549
|
+
|
|
550
|
+
let deleted = 0;
|
|
551
|
+
const toDelete = [];
|
|
552
|
+
|
|
553
|
+
for (const [id, checkpoint] of this.checkpoints) {
|
|
554
|
+
if (new Date(checkpoint.timestamp) < cutoff) {
|
|
555
|
+
toDelete.push(id);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
for (const id of toDelete) {
|
|
560
|
+
if (await this.deleteCheckpoint(id)) {
|
|
561
|
+
deleted++;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return deleted;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Create a new RollbackManager instance
|
|
571
|
+
* @param {Object} config - Configuration options
|
|
572
|
+
* @returns {RollbackManager}
|
|
573
|
+
*/
|
|
574
|
+
function createRollbackManager(config = {}) {
|
|
575
|
+
return new RollbackManager(config);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
module.exports = {
|
|
579
|
+
RollbackManager,
|
|
580
|
+
createRollbackManager,
|
|
581
|
+
ROLLBACK_LEVEL,
|
|
582
|
+
ROLLBACK_STATUS,
|
|
583
|
+
WORKFLOW_STAGE
|
|
584
|
+
};
|