musubi-sdd 3.0.0 → 3.5.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/bin/musubi-browser.js +0 -0
- package/bin/musubi-change.js +623 -10
- package/bin/musubi-convert.js +0 -0
- package/bin/musubi-gui.js +0 -0
- package/bin/musubi-orchestrate.js +456 -0
- package/bin/musubi-trace.js +393 -0
- package/bin/musubi-validate.js +0 -10
- package/package.json +3 -2
- package/src/analyzers/impact-analyzer.js +682 -0
- package/src/integrations/cicd.js +782 -0
- package/src/integrations/documentation.js +740 -0
- package/src/integrations/examples.js +789 -0
- package/src/integrations/index.js +23 -0
- package/src/integrations/platforms.js +929 -0
- package/src/managers/delta-spec.js +484 -0
- package/src/monitoring/incident-manager.js +890 -0
- package/src/monitoring/index.js +633 -0
- package/src/monitoring/observability.js +938 -0
- package/src/monitoring/release-manager.js +622 -0
- package/src/orchestration/index.js +168 -0
- package/src/orchestration/orchestration-engine.js +409 -0
- package/src/orchestration/pattern-registry.js +319 -0
- package/src/orchestration/patterns/auto.js +386 -0
- package/src/orchestration/patterns/group-chat.js +395 -0
- package/src/orchestration/patterns/human-in-loop.js +506 -0
- package/src/orchestration/patterns/nested.js +322 -0
- package/src/orchestration/patterns/sequential.js +278 -0
- package/src/orchestration/patterns/swarm.js +395 -0
- package/src/orchestration/workflow-orchestrator.js +738 -0
- package/src/reporters/coverage-report.js +452 -0
- package/src/reporters/traceability-matrix-report.js +684 -0
- package/src/steering/advanced-validation.js +812 -0
- package/src/steering/auto-updater.js +670 -0
- package/src/steering/index.js +119 -0
- package/src/steering/quality-metrics.js +650 -0
- package/src/steering/template-constraints.js +789 -0
- package/src/templates/agents/claude-code/skills/agent-assistant/SKILL.md +22 -0
- package/src/templates/agents/claude-code/skills/ai-ml-engineer/mlops-guide.md +350 -0
- package/src/templates/agents/claude-code/skills/ai-ml-engineer/model-card-template.md +246 -0
- package/src/templates/agents/claude-code/skills/api-designer/api-patterns.md +336 -0
- package/src/templates/agents/claude-code/skills/api-designer/openapi-template.md +376 -0
- package/src/templates/agents/claude-code/skills/bug-hunter/root-cause-analysis.md +177 -0
- package/src/templates/agents/claude-code/skills/change-impact-analyzer/dependency-graph-patterns.md +348 -0
- package/src/templates/agents/claude-code/skills/change-impact-analyzer/impact-analysis-template.md +246 -0
- package/src/templates/agents/claude-code/skills/cloud-architect/aws-patterns.md +239 -0
- package/src/templates/agents/claude-code/skills/cloud-architect/azure-patterns.md +300 -0
- package/src/templates/agents/claude-code/skills/cloud-architect/terraform-templates/azure-webapp.tf +337 -0
- package/src/templates/agents/claude-code/skills/code-reviewer/best-practices.md +155 -0
- package/src/templates/agents/claude-code/skills/code-reviewer/review-checklist.md +184 -0
- package/src/templates/agents/claude-code/skills/code-reviewer/review-standards.md +272 -0
- package/src/templates/agents/claude-code/skills/constitution-enforcer/constitutional-articles.md +449 -0
- package/src/templates/agents/claude-code/skills/constitution-enforcer/phase-minus-one-gates.md +375 -0
- package/src/templates/agents/claude-code/skills/database-administrator/backup-recovery.md +331 -0
- package/src/templates/agents/claude-code/skills/database-administrator/tuning-guide.md +314 -0
- package/src/templates/agents/claude-code/skills/database-schema-designer/schema-patterns.md +335 -0
- package/src/templates/agents/claude-code/skills/devops-engineer/ci-cd-templates.md +443 -0
- package/src/templates/agents/claude-code/skills/devops-engineer/pipeline-templates/github-actions.yml +311 -0
- package/src/templates/agents/claude-code/skills/devops-engineer/pipeline-templates/gitlab-ci.yml +255 -0
- package/src/templates/agents/claude-code/skills/issue-resolver/SKILL.md +21 -0
- package/src/templates/agents/claude-code/skills/orchestrator/SKILL.md +90 -28
- package/src/templates/agents/claude-code/skills/orchestrator/patterns.md +266 -0
- package/src/templates/agents/claude-code/skills/orchestrator/selection-matrix.md +185 -0
- package/src/templates/agents/claude-code/skills/performance-engineer/optimization-playbook.md +306 -0
- package/src/templates/agents/claude-code/skills/performance-optimizer/benchmark-template.md +272 -0
- package/src/templates/agents/claude-code/skills/performance-optimizer/optimization-patterns.md +273 -0
- package/src/templates/agents/claude-code/skills/project-manager/SKILL.md +32 -0
- package/src/templates/agents/claude-code/skills/project-manager/agile-ceremonies.md +283 -0
- package/src/templates/agents/claude-code/skills/project-manager/project-templates.md +345 -0
- package/src/templates/agents/claude-code/skills/quality-assurance/qa-plan-template.md +219 -0
- package/src/templates/agents/claude-code/skills/release-coordinator/feature-flag-guide.md +312 -0
- package/src/templates/agents/claude-code/skills/release-coordinator/release-plan-template.md +230 -0
- package/src/templates/agents/claude-code/skills/requirements-analyst/ears-format.md +259 -0
- package/src/templates/agents/claude-code/skills/requirements-analyst/validation-rules.md +359 -0
- package/src/templates/agents/claude-code/skills/security-auditor/audit-checklists.md +243 -0
- package/src/templates/agents/claude-code/skills/security-auditor/owasp-top-10.md +349 -0
- package/src/templates/agents/claude-code/skills/security-auditor/vulnerability-patterns.md +295 -0
- package/src/templates/agents/claude-code/skills/site-reliability-engineer/SKILL.md +27 -0
- package/src/templates/agents/claude-code/skills/site-reliability-engineer/incident-response-template.md +286 -0
- package/src/templates/agents/claude-code/skills/site-reliability-engineer/observability-patterns.md +359 -0
- package/src/templates/agents/claude-code/skills/site-reliability-engineer/slo-sli-guide.md +302 -0
- package/src/templates/agents/claude-code/skills/software-developer/solid-principles.md +348 -0
- package/src/templates/agents/claude-code/skills/software-developer/test-first-workflow.md +370 -0
- package/src/templates/agents/claude-code/skills/steering/SKILL.md +30 -0
- package/src/templates/agents/claude-code/skills/steering/auto-update-rules.md +328 -0
- package/src/templates/agents/claude-code/skills/system-architect/adr-template.md +295 -0
- package/src/templates/agents/claude-code/skills/system-architect/c4-model-guide.md +328 -0
- package/src/templates/agents/claude-code/skills/technical-writer/doc-templates/documentation-templates.md +436 -0
- package/src/templates/agents/claude-code/skills/test-engineer/SKILL.md +21 -0
- package/src/templates/agents/claude-code/skills/test-engineer/ears-test-mapping.md +444 -0
- package/src/templates/agents/claude-code/skills/test-engineer/test-types.md +425 -0
- package/src/templates/agents/claude-code/skills/traceability-auditor/coverage-matrix-template.md +131 -0
- package/src/templates/agents/claude-code/skills/traceability-auditor/gap-detection-rules.md +227 -0
- package/src/templates/agents/claude-code/skills/ui-ux-designer/SKILL.md +27 -0
- package/src/templates/agents/claude-code/skills/ui-ux-designer/accessibility-guidelines.md +318 -0
- package/src/templates/agents/claude-code/skills/ui-ux-designer/design-system-components.md +345 -0
- package/src/templates/agents/codex/AGENTS.md +36 -1
- package/src/templates/agents/cursor/AGENTS.md +36 -1
- package/src/templates/agents/gemini-cli/GEMINI.md +36 -1
- package/src/templates/agents/github-copilot/AGENTS.md +65 -1
- package/src/templates/agents/qwen-code/QWEN.md +36 -1
- package/src/templates/agents/windsurf/AGENTS.md +36 -1
- package/src/templates/shared/delta-spec-template.md +246 -0
- package/src/validators/constitutional-validator.js +494 -0
- package/src/validators/delta-format.js +474 -0
- package/src/validators/traceability-validator.js +561 -0
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Release Manager - Release coordination and management
|
|
3
|
+
*
|
|
4
|
+
* Provides release management capabilities:
|
|
5
|
+
* - Release planning and tracking
|
|
6
|
+
* - Feature flag management
|
|
7
|
+
* - Rollback procedures
|
|
8
|
+
* - Release notes generation
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { EventEmitter } = require('events');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Release States
|
|
15
|
+
*/
|
|
16
|
+
const ReleaseState = {
|
|
17
|
+
PLANNING: 'planning',
|
|
18
|
+
DEVELOPMENT: 'development',
|
|
19
|
+
TESTING: 'testing',
|
|
20
|
+
STAGING: 'staging',
|
|
21
|
+
CANARY: 'canary',
|
|
22
|
+
PRODUCTION: 'production',
|
|
23
|
+
ROLLBACK: 'rollback',
|
|
24
|
+
COMPLETED: 'completed',
|
|
25
|
+
CANCELLED: 'cancelled'
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Release Types
|
|
30
|
+
*/
|
|
31
|
+
const ReleaseType = {
|
|
32
|
+
MAJOR: 'major',
|
|
33
|
+
MINOR: 'minor',
|
|
34
|
+
PATCH: 'patch',
|
|
35
|
+
HOTFIX: 'hotfix',
|
|
36
|
+
CANARY: 'canary'
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Feature Flag Status
|
|
41
|
+
*/
|
|
42
|
+
const FeatureFlagStatus = {
|
|
43
|
+
ENABLED: 'enabled',
|
|
44
|
+
DISABLED: 'disabled',
|
|
45
|
+
PERCENTAGE: 'percentage',
|
|
46
|
+
USER_LIST: 'user-list'
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Release definition
|
|
51
|
+
*/
|
|
52
|
+
class Release {
|
|
53
|
+
constructor(options) {
|
|
54
|
+
this.id = options.id || this._generateId();
|
|
55
|
+
this.version = options.version;
|
|
56
|
+
this.type = options.type || ReleaseType.MINOR;
|
|
57
|
+
this.name = options.name || `Release ${options.version}`;
|
|
58
|
+
this.description = options.description || '';
|
|
59
|
+
this.state = options.state || ReleaseState.PLANNING;
|
|
60
|
+
this.targetDate = options.targetDate || null;
|
|
61
|
+
this.createdAt = options.createdAt || new Date();
|
|
62
|
+
this.updatedAt = options.updatedAt || new Date();
|
|
63
|
+
this.completedAt = null;
|
|
64
|
+
|
|
65
|
+
this.features = options.features || [];
|
|
66
|
+
this.bugFixes = options.bugFixes || [];
|
|
67
|
+
this.breakingChanges = options.breakingChanges || [];
|
|
68
|
+
this.dependencies = options.dependencies || [];
|
|
69
|
+
|
|
70
|
+
this.rolloutStrategy = options.rolloutStrategy || {
|
|
71
|
+
type: 'percentage',
|
|
72
|
+
stages: [
|
|
73
|
+
{ percentage: 1, duration: '1h' },
|
|
74
|
+
{ percentage: 10, duration: '2h' },
|
|
75
|
+
{ percentage: 50, duration: '4h' },
|
|
76
|
+
{ percentage: 100, duration: null }
|
|
77
|
+
]
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
this.rollbackPlan = options.rollbackPlan || {
|
|
81
|
+
automatic: true,
|
|
82
|
+
triggers: ['error_rate > 5%', 'latency_p99 > 2s'],
|
|
83
|
+
procedure: []
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
this.metrics = {
|
|
87
|
+
errorsBefore: null,
|
|
88
|
+
errorsAfter: null,
|
|
89
|
+
latencyBefore: null,
|
|
90
|
+
latencyAfter: null
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
this.history = [];
|
|
94
|
+
this._addHistory('created', { version: this.version });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Transition to a new state
|
|
99
|
+
*/
|
|
100
|
+
transitionTo(newState, metadata = {}) {
|
|
101
|
+
const validTransitions = {
|
|
102
|
+
[ReleaseState.PLANNING]: [ReleaseState.DEVELOPMENT, ReleaseState.CANCELLED],
|
|
103
|
+
[ReleaseState.DEVELOPMENT]: [ReleaseState.TESTING, ReleaseState.CANCELLED],
|
|
104
|
+
[ReleaseState.TESTING]: [ReleaseState.STAGING, ReleaseState.DEVELOPMENT, ReleaseState.CANCELLED],
|
|
105
|
+
[ReleaseState.STAGING]: [ReleaseState.CANARY, ReleaseState.PRODUCTION, ReleaseState.TESTING, ReleaseState.CANCELLED],
|
|
106
|
+
[ReleaseState.CANARY]: [ReleaseState.PRODUCTION, ReleaseState.ROLLBACK],
|
|
107
|
+
[ReleaseState.PRODUCTION]: [ReleaseState.COMPLETED, ReleaseState.ROLLBACK],
|
|
108
|
+
[ReleaseState.ROLLBACK]: [ReleaseState.TESTING, ReleaseState.CANCELLED],
|
|
109
|
+
[ReleaseState.COMPLETED]: [],
|
|
110
|
+
[ReleaseState.CANCELLED]: []
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const allowed = validTransitions[this.state] || [];
|
|
114
|
+
if (!allowed.includes(newState)) {
|
|
115
|
+
throw new Error(`Invalid transition from ${this.state} to ${newState}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const previousState = this.state;
|
|
119
|
+
this.state = newState;
|
|
120
|
+
this.updatedAt = new Date();
|
|
121
|
+
|
|
122
|
+
if (newState === ReleaseState.COMPLETED) {
|
|
123
|
+
this.completedAt = new Date();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this._addHistory('transition', {
|
|
127
|
+
from: previousState,
|
|
128
|
+
to: newState,
|
|
129
|
+
...metadata
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return this;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Add a feature to the release
|
|
137
|
+
*/
|
|
138
|
+
addFeature(feature) {
|
|
139
|
+
this.features.push({
|
|
140
|
+
id: feature.id || `feat-${this.features.length + 1}`,
|
|
141
|
+
title: feature.title,
|
|
142
|
+
description: feature.description || '',
|
|
143
|
+
jiraId: feature.jiraId || null,
|
|
144
|
+
breaking: feature.breaking || false
|
|
145
|
+
});
|
|
146
|
+
this._addHistory('featureAdded', { feature: feature.title });
|
|
147
|
+
return this;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Add a bug fix to the release
|
|
152
|
+
*/
|
|
153
|
+
addBugFix(bugFix) {
|
|
154
|
+
this.bugFixes.push({
|
|
155
|
+
id: bugFix.id || `fix-${this.bugFixes.length + 1}`,
|
|
156
|
+
title: bugFix.title,
|
|
157
|
+
description: bugFix.description || '',
|
|
158
|
+
jiraId: bugFix.jiraId || null,
|
|
159
|
+
severity: bugFix.severity || 'medium'
|
|
160
|
+
});
|
|
161
|
+
this._addHistory('bugFixAdded', { bugFix: bugFix.title });
|
|
162
|
+
return this;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Generate release notes
|
|
167
|
+
*/
|
|
168
|
+
generateReleaseNotes(format = 'markdown') {
|
|
169
|
+
const notes = {
|
|
170
|
+
version: this.version,
|
|
171
|
+
date: this.completedAt || new Date(),
|
|
172
|
+
features: this.features,
|
|
173
|
+
bugFixes: this.bugFixes,
|
|
174
|
+
breakingChanges: this.breakingChanges
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
if (format === 'markdown') {
|
|
178
|
+
return this._toMarkdown(notes);
|
|
179
|
+
} else if (format === 'json') {
|
|
180
|
+
return JSON.stringify(notes, null, 2);
|
|
181
|
+
}
|
|
182
|
+
return notes;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Generate markdown release notes
|
|
187
|
+
* @private
|
|
188
|
+
*/
|
|
189
|
+
_toMarkdown(notes) {
|
|
190
|
+
let md = `# ${this.name}\n\n`;
|
|
191
|
+
md += `**Version:** ${notes.version} \n`;
|
|
192
|
+
md += `**Date:** ${notes.date.toISOString().split('T')[0]} \n`;
|
|
193
|
+
md += `**Type:** ${this.type} \n\n`;
|
|
194
|
+
|
|
195
|
+
if (this.description) {
|
|
196
|
+
md += `${this.description}\n\n`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (notes.breakingChanges.length > 0) {
|
|
200
|
+
md += `## ⚠️ Breaking Changes\n\n`;
|
|
201
|
+
for (const change of notes.breakingChanges) {
|
|
202
|
+
md += `- ${change.title}\n`;
|
|
203
|
+
if (change.description) md += ` ${change.description}\n`;
|
|
204
|
+
}
|
|
205
|
+
md += '\n';
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (notes.features.length > 0) {
|
|
209
|
+
md += `## ✨ New Features\n\n`;
|
|
210
|
+
for (const feature of notes.features) {
|
|
211
|
+
md += `- **${feature.title}**`;
|
|
212
|
+
if (feature.jiraId) md += ` (${feature.jiraId})`;
|
|
213
|
+
md += '\n';
|
|
214
|
+
if (feature.description) md += ` ${feature.description}\n`;
|
|
215
|
+
}
|
|
216
|
+
md += '\n';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (notes.bugFixes.length > 0) {
|
|
220
|
+
md += `## 🐛 Bug Fixes\n\n`;
|
|
221
|
+
for (const fix of notes.bugFixes) {
|
|
222
|
+
md += `- ${fix.title}`;
|
|
223
|
+
if (fix.jiraId) md += ` (${fix.jiraId})`;
|
|
224
|
+
md += '\n';
|
|
225
|
+
}
|
|
226
|
+
md += '\n';
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return md;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Add history entry
|
|
234
|
+
* @private
|
|
235
|
+
*/
|
|
236
|
+
_addHistory(action, data) {
|
|
237
|
+
this.history.push({
|
|
238
|
+
action,
|
|
239
|
+
timestamp: new Date(),
|
|
240
|
+
data
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Generate unique ID
|
|
246
|
+
* @private
|
|
247
|
+
*/
|
|
248
|
+
_generateId() {
|
|
249
|
+
return `rel-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
toJSON() {
|
|
253
|
+
return {
|
|
254
|
+
id: this.id,
|
|
255
|
+
version: this.version,
|
|
256
|
+
type: this.type,
|
|
257
|
+
name: this.name,
|
|
258
|
+
description: this.description,
|
|
259
|
+
state: this.state,
|
|
260
|
+
targetDate: this.targetDate,
|
|
261
|
+
createdAt: this.createdAt,
|
|
262
|
+
updatedAt: this.updatedAt,
|
|
263
|
+
completedAt: this.completedAt,
|
|
264
|
+
features: this.features,
|
|
265
|
+
bugFixes: this.bugFixes,
|
|
266
|
+
breakingChanges: this.breakingChanges,
|
|
267
|
+
rolloutStrategy: this.rolloutStrategy,
|
|
268
|
+
rollbackPlan: this.rollbackPlan,
|
|
269
|
+
history: this.history
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Feature Flag definition
|
|
276
|
+
*/
|
|
277
|
+
class FeatureFlag {
|
|
278
|
+
constructor(options) {
|
|
279
|
+
this.key = options.key;
|
|
280
|
+
this.name = options.name || options.key;
|
|
281
|
+
this.description = options.description || '';
|
|
282
|
+
this.status = options.status || FeatureFlagStatus.DISABLED;
|
|
283
|
+
this.percentage = options.percentage || 0;
|
|
284
|
+
this.userList = options.userList || [];
|
|
285
|
+
this.createdAt = options.createdAt || new Date();
|
|
286
|
+
this.updatedAt = options.updatedAt || new Date();
|
|
287
|
+
this.metadata = options.metadata || {};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Enable the feature flag
|
|
292
|
+
*/
|
|
293
|
+
enable() {
|
|
294
|
+
this.status = FeatureFlagStatus.ENABLED;
|
|
295
|
+
this.percentage = 100;
|
|
296
|
+
this.updatedAt = new Date();
|
|
297
|
+
return this;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Disable the feature flag
|
|
302
|
+
*/
|
|
303
|
+
disable() {
|
|
304
|
+
this.status = FeatureFlagStatus.DISABLED;
|
|
305
|
+
this.percentage = 0;
|
|
306
|
+
this.updatedAt = new Date();
|
|
307
|
+
return this;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Set percentage rollout
|
|
312
|
+
*/
|
|
313
|
+
setPercentage(pct) {
|
|
314
|
+
if (pct < 0 || pct > 100) {
|
|
315
|
+
throw new Error('Percentage must be between 0 and 100');
|
|
316
|
+
}
|
|
317
|
+
this.status = FeatureFlagStatus.PERCENTAGE;
|
|
318
|
+
this.percentage = pct;
|
|
319
|
+
this.updatedAt = new Date();
|
|
320
|
+
return this;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Check if feature is enabled for a user
|
|
325
|
+
*/
|
|
326
|
+
isEnabledFor(userId) {
|
|
327
|
+
switch (this.status) {
|
|
328
|
+
case FeatureFlagStatus.ENABLED:
|
|
329
|
+
return true;
|
|
330
|
+
|
|
331
|
+
case FeatureFlagStatus.DISABLED:
|
|
332
|
+
return false;
|
|
333
|
+
|
|
334
|
+
case FeatureFlagStatus.USER_LIST:
|
|
335
|
+
return this.userList.includes(userId);
|
|
336
|
+
|
|
337
|
+
case FeatureFlagStatus.PERCENTAGE:
|
|
338
|
+
// Consistent hashing based on userId
|
|
339
|
+
const hash = this._hashString(`${this.key}:${userId}`);
|
|
340
|
+
return (hash % 100) < this.percentage;
|
|
341
|
+
|
|
342
|
+
default:
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Simple string hash
|
|
349
|
+
* @private
|
|
350
|
+
*/
|
|
351
|
+
_hashString(str) {
|
|
352
|
+
let hash = 0;
|
|
353
|
+
for (let i = 0; i < str.length; i++) {
|
|
354
|
+
const char = str.charCodeAt(i);
|
|
355
|
+
hash = ((hash << 5) - hash) + char;
|
|
356
|
+
hash = hash & hash;
|
|
357
|
+
}
|
|
358
|
+
return Math.abs(hash);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
toJSON() {
|
|
362
|
+
return {
|
|
363
|
+
key: this.key,
|
|
364
|
+
name: this.name,
|
|
365
|
+
description: this.description,
|
|
366
|
+
status: this.status,
|
|
367
|
+
percentage: this.percentage,
|
|
368
|
+
userList: this.userList,
|
|
369
|
+
createdAt: this.createdAt,
|
|
370
|
+
updatedAt: this.updatedAt,
|
|
371
|
+
metadata: this.metadata
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Release Manager
|
|
378
|
+
*/
|
|
379
|
+
class ReleaseManager extends EventEmitter {
|
|
380
|
+
constructor(options = {}) {
|
|
381
|
+
super();
|
|
382
|
+
this.releases = new Map();
|
|
383
|
+
this.featureFlags = new Map();
|
|
384
|
+
this.options = {
|
|
385
|
+
autoGenerateNotes: options.autoGenerateNotes !== false,
|
|
386
|
+
...options
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Create a new release
|
|
392
|
+
*/
|
|
393
|
+
createRelease(options) {
|
|
394
|
+
const release = options instanceof Release ? options : new Release(options);
|
|
395
|
+
this.releases.set(release.id, release);
|
|
396
|
+
this.emit('releaseCreated', release);
|
|
397
|
+
return release;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Get a release by ID
|
|
402
|
+
*/
|
|
403
|
+
getRelease(id) {
|
|
404
|
+
return this.releases.get(id);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Get release by version
|
|
409
|
+
*/
|
|
410
|
+
getReleaseByVersion(version) {
|
|
411
|
+
for (const release of this.releases.values()) {
|
|
412
|
+
if (release.version === version) {
|
|
413
|
+
return release;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* List all releases
|
|
421
|
+
*/
|
|
422
|
+
listReleases(filter = {}) {
|
|
423
|
+
let releases = [...this.releases.values()];
|
|
424
|
+
|
|
425
|
+
if (filter.state) {
|
|
426
|
+
releases = releases.filter(r => r.state === filter.state);
|
|
427
|
+
}
|
|
428
|
+
if (filter.type) {
|
|
429
|
+
releases = releases.filter(r => r.type === filter.type);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return releases.map(r => r.toJSON());
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Transition a release to a new state
|
|
437
|
+
*/
|
|
438
|
+
transitionRelease(releaseId, newState, metadata = {}) {
|
|
439
|
+
const release = this.releases.get(releaseId);
|
|
440
|
+
if (!release) {
|
|
441
|
+
throw new Error(`Release not found: ${releaseId}`);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
release.transitionTo(newState, metadata);
|
|
445
|
+
this.emit('releaseTransitioned', { release, newState });
|
|
446
|
+
return release;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Create a feature flag
|
|
451
|
+
*/
|
|
452
|
+
createFeatureFlag(options) {
|
|
453
|
+
const flag = options instanceof FeatureFlag ? options : new FeatureFlag(options);
|
|
454
|
+
this.featureFlags.set(flag.key, flag);
|
|
455
|
+
this.emit('featureFlagCreated', flag);
|
|
456
|
+
return flag;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Get a feature flag
|
|
461
|
+
*/
|
|
462
|
+
getFeatureFlag(key) {
|
|
463
|
+
return this.featureFlags.get(key);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* List all feature flags
|
|
468
|
+
*/
|
|
469
|
+
listFeatureFlags() {
|
|
470
|
+
return [...this.featureFlags.values()].map(f => f.toJSON());
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Check if a feature is enabled for a user
|
|
475
|
+
*/
|
|
476
|
+
isFeatureEnabled(flagKey, userId = null) {
|
|
477
|
+
const flag = this.featureFlags.get(flagKey);
|
|
478
|
+
if (!flag) return false;
|
|
479
|
+
|
|
480
|
+
if (userId) {
|
|
481
|
+
return flag.isEnabledFor(userId);
|
|
482
|
+
}
|
|
483
|
+
return flag.status === FeatureFlagStatus.ENABLED;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Enable a feature flag
|
|
488
|
+
*/
|
|
489
|
+
enableFeatureFlag(key) {
|
|
490
|
+
const flag = this.featureFlags.get(key);
|
|
491
|
+
if (!flag) throw new Error(`Feature flag not found: ${key}`);
|
|
492
|
+
flag.enable();
|
|
493
|
+
this.emit('featureFlagEnabled', flag);
|
|
494
|
+
return flag;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Disable a feature flag
|
|
499
|
+
*/
|
|
500
|
+
disableFeatureFlag(key) {
|
|
501
|
+
const flag = this.featureFlags.get(key);
|
|
502
|
+
if (!flag) throw new Error(`Feature flag not found: ${key}`);
|
|
503
|
+
flag.disable();
|
|
504
|
+
this.emit('featureFlagDisabled', flag);
|
|
505
|
+
return flag;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Set feature flag percentage
|
|
510
|
+
*/
|
|
511
|
+
setFeatureFlagPercentage(key, percentage) {
|
|
512
|
+
const flag = this.featureFlags.get(key);
|
|
513
|
+
if (!flag) throw new Error(`Feature flag not found: ${key}`);
|
|
514
|
+
flag.setPercentage(percentage);
|
|
515
|
+
this.emit('featureFlagUpdated', flag);
|
|
516
|
+
return flag;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Generate rollback procedure for a release
|
|
521
|
+
*/
|
|
522
|
+
generateRollbackProcedure(releaseId) {
|
|
523
|
+
const release = this.releases.get(releaseId);
|
|
524
|
+
if (!release) throw new Error(`Release not found: ${releaseId}`);
|
|
525
|
+
|
|
526
|
+
return {
|
|
527
|
+
releaseId: release.id,
|
|
528
|
+
version: release.version,
|
|
529
|
+
steps: [
|
|
530
|
+
{
|
|
531
|
+
order: 1,
|
|
532
|
+
action: 'notify',
|
|
533
|
+
description: 'Notify team of rollback initiation',
|
|
534
|
+
command: null
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
order: 2,
|
|
538
|
+
action: 'disable-flags',
|
|
539
|
+
description: 'Disable all new feature flags',
|
|
540
|
+
command: 'musubi release disable-flags --version ' + release.version
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
order: 3,
|
|
544
|
+
action: 'scale-down',
|
|
545
|
+
description: 'Scale down new deployment',
|
|
546
|
+
command: 'kubectl scale deployment app-v' + release.version + ' --replicas=0'
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
order: 4,
|
|
550
|
+
action: 'traffic-shift',
|
|
551
|
+
description: 'Shift traffic to previous version',
|
|
552
|
+
command: 'kubectl rollout undo deployment/app'
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
order: 5,
|
|
556
|
+
action: 'verify',
|
|
557
|
+
description: 'Verify rollback success',
|
|
558
|
+
command: 'curl -f http://app/health'
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
order: 6,
|
|
562
|
+
action: 'notify-complete',
|
|
563
|
+
description: 'Notify team of rollback completion',
|
|
564
|
+
command: null
|
|
565
|
+
}
|
|
566
|
+
],
|
|
567
|
+
automaticTriggers: release.rollbackPlan.triggers,
|
|
568
|
+
estimatedDuration: '5-10 minutes'
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Get summary statistics
|
|
574
|
+
*/
|
|
575
|
+
getSummary() {
|
|
576
|
+
const releases = [...this.releases.values()];
|
|
577
|
+
const flags = [...this.featureFlags.values()];
|
|
578
|
+
|
|
579
|
+
return {
|
|
580
|
+
totalReleases: releases.length,
|
|
581
|
+
releasesByState: this._countBy(releases, 'state'),
|
|
582
|
+
releasesByType: this._countBy(releases, 'type'),
|
|
583
|
+
totalFeatureFlags: flags.length,
|
|
584
|
+
enabledFlags: flags.filter(f => f.status === FeatureFlagStatus.ENABLED).length,
|
|
585
|
+
disabledFlags: flags.filter(f => f.status === FeatureFlagStatus.DISABLED).length,
|
|
586
|
+
percentageFlags: flags.filter(f => f.status === FeatureFlagStatus.PERCENTAGE).length
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Count items by property
|
|
592
|
+
* @private
|
|
593
|
+
*/
|
|
594
|
+
_countBy(items, prop) {
|
|
595
|
+
return items.reduce((acc, item) => {
|
|
596
|
+
acc[item[prop]] = (acc[item[prop]] || 0) + 1;
|
|
597
|
+
return acc;
|
|
598
|
+
}, {});
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Create a release manager
|
|
604
|
+
*/
|
|
605
|
+
function createReleaseManager(options = {}) {
|
|
606
|
+
return new ReleaseManager(options);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
module.exports = {
|
|
610
|
+
// Classes
|
|
611
|
+
Release,
|
|
612
|
+
FeatureFlag,
|
|
613
|
+
ReleaseManager,
|
|
614
|
+
|
|
615
|
+
// Constants
|
|
616
|
+
ReleaseState,
|
|
617
|
+
ReleaseType,
|
|
618
|
+
FeatureFlagStatus,
|
|
619
|
+
|
|
620
|
+
// Factory
|
|
621
|
+
createReleaseManager
|
|
622
|
+
};
|