@howlil/ez-agents 3.4.1 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +84 -20
- package/agents/ez-observer-agent.md +260 -0
- package/agents/ez-release-agent.md +333 -0
- package/agents/ez-requirements-agent.md +377 -0
- package/agents/ez-scrum-master-agent.md +242 -0
- package/agents/ez-tech-lead-agent.md +267 -0
- package/bin/install.js +3221 -3230
- package/commands/ez/arch-review.md +102 -0
- package/commands/ez/execute-phase.md +11 -0
- package/commands/ez/export-session.md +79 -0
- package/commands/ez/gather-requirements.md +117 -0
- package/commands/ez/git-workflow.md +72 -0
- package/commands/ez/hotfix.md +120 -0
- package/commands/ez/import-session.md +82 -0
- package/commands/ez/join-discord.md +18 -18
- package/commands/ez/list-sessions.md +96 -0
- package/commands/ez/package-manager.md +316 -0
- package/commands/ez/plan-phase.md +9 -1
- package/commands/ez/preflight.md +79 -0
- package/commands/ez/progress.md +13 -1
- package/commands/ez/release.md +153 -0
- package/commands/ez/resume.md +107 -0
- package/commands/ez/standup.md +85 -0
- package/ez-agents/bin/ez-tools.cjs +1095 -716
- package/ez-agents/bin/lib/assistant-adapter.cjs +264 -264
- package/ez-agents/bin/lib/audit-exec.cjs +7 -2
- package/ez-agents/bin/lib/bdd-validator.cjs +622 -0
- package/ez-agents/bin/lib/circuit-breaker.cjs +118 -118
- package/ez-agents/bin/lib/config.cjs +190 -190
- package/ez-agents/bin/lib/content-scanner.cjs +238 -0
- package/ez-agents/bin/lib/context-cache.cjs +154 -0
- package/ez-agents/bin/lib/context-errors.cjs +71 -0
- package/ez-agents/bin/lib/context-manager.cjs +220 -0
- package/ez-agents/bin/lib/discussion-synthesizer.cjs +458 -0
- package/ez-agents/bin/lib/file-access.cjs +207 -0
- package/ez-agents/bin/lib/file-lock.cjs +236 -236
- package/ez-agents/bin/lib/frontmatter.cjs +299 -299
- package/ez-agents/bin/lib/fs-utils.cjs +153 -153
- package/ez-agents/bin/lib/git-errors.cjs +83 -0
- package/ez-agents/bin/lib/git-utils.cjs +118 -0
- package/ez-agents/bin/lib/git-workflow-engine.cjs +1157 -0
- package/ez-agents/bin/lib/index.cjs +157 -113
- package/ez-agents/bin/lib/init.cjs +757 -757
- package/ez-agents/bin/lib/lockfile-validator.cjs +227 -0
- package/ez-agents/bin/lib/logger.cjs +124 -124
- package/ez-agents/bin/lib/memory-compression.cjs +256 -0
- package/ez-agents/bin/lib/metrics-tracker.cjs +406 -0
- package/ez-agents/bin/lib/milestone.cjs +241 -241
- package/ez-agents/bin/lib/model-provider.cjs +241 -241
- package/ez-agents/bin/lib/package-manager-detector.cjs +203 -0
- package/ez-agents/bin/lib/package-manager-executor.cjs +385 -0
- package/ez-agents/bin/lib/package-manager-service.cjs +216 -0
- package/ez-agents/bin/lib/phase.cjs +925 -925
- package/ez-agents/bin/lib/planning-write.cjs +107 -107
- package/ez-agents/bin/lib/release-validator.cjs +614 -0
- package/ez-agents/bin/lib/retry.cjs +119 -119
- package/ez-agents/bin/lib/roadmap.cjs +306 -306
- package/ez-agents/bin/lib/safe-exec.cjs +128 -128
- package/ez-agents/bin/lib/safe-path.cjs +130 -130
- package/ez-agents/bin/lib/session-chain.cjs +304 -0
- package/ez-agents/bin/lib/session-errors.cjs +81 -0
- package/ez-agents/bin/lib/session-export.cjs +251 -0
- package/ez-agents/bin/lib/session-import.cjs +262 -0
- package/ez-agents/bin/lib/session-manager.cjs +280 -0
- package/ez-agents/bin/lib/state.cjs +736 -736
- package/ez-agents/bin/lib/temp-file.cjs +239 -239
- package/ez-agents/bin/lib/template.cjs +223 -223
- package/ez-agents/bin/lib/test-file-lock.cjs +112 -112
- package/ez-agents/bin/lib/test-graceful.cjs +93 -93
- package/ez-agents/bin/lib/test-logger.cjs +60 -60
- package/ez-agents/bin/lib/test-safe-exec.cjs +38 -38
- package/ez-agents/bin/lib/test-safe-path.cjs +33 -33
- package/ez-agents/bin/lib/test-temp-file.cjs +125 -125
- package/ez-agents/bin/lib/tier-manager.cjs +428 -0
- package/ez-agents/bin/lib/timeout-exec.cjs +63 -63
- package/ez-agents/bin/lib/url-fetch.cjs +170 -0
- package/ez-agents/bin/lib/verify.cjs +15 -1
- package/ez-agents/references/checkpoints.md +776 -776
- package/ez-agents/references/continuation-format.md +249 -249
- package/ez-agents/references/metrics-schema.md +118 -0
- package/ez-agents/references/planning-config.md +140 -0
- package/ez-agents/references/questioning.md +162 -162
- package/ez-agents/references/tdd.md +263 -263
- package/ez-agents/references/tier-strategy.md +103 -0
- package/ez-agents/templates/bdd-feature.md +173 -0
- package/ez-agents/templates/codebase/concerns.md +310 -310
- package/ez-agents/templates/codebase/conventions.md +307 -307
- package/ez-agents/templates/codebase/integrations.md +280 -280
- package/ez-agents/templates/codebase/stack.md +186 -186
- package/ez-agents/templates/codebase/testing.md +480 -480
- package/ez-agents/templates/config.json +37 -37
- package/ez-agents/templates/continue-here.md +78 -78
- package/ez-agents/templates/discussion.md +68 -0
- package/ez-agents/templates/incident-runbook.md +205 -0
- package/ez-agents/templates/milestone-archive.md +123 -123
- package/ez-agents/templates/milestone.md +115 -115
- package/ez-agents/templates/release-checklist.md +133 -0
- package/ez-agents/templates/requirements.md +231 -231
- package/ez-agents/templates/research-project/ARCHITECTURE.md +204 -204
- package/ez-agents/templates/research-project/FEATURES.md +147 -147
- package/ez-agents/templates/research-project/PITFALLS.md +200 -200
- package/ez-agents/templates/research-project/STACK.md +120 -120
- package/ez-agents/templates/research-project/SUMMARY.md +170 -170
- package/ez-agents/templates/retrospective.md +54 -54
- package/ez-agents/templates/roadmap.md +202 -202
- package/ez-agents/templates/rollback-plan.md +201 -0
- package/ez-agents/templates/summary-minimal.md +41 -41
- package/ez-agents/templates/summary-standard.md +48 -48
- package/ez-agents/templates/summary.md +248 -248
- package/ez-agents/templates/user-setup.md +311 -311
- package/ez-agents/templates/verification-report.md +322 -322
- package/ez-agents/workflows/add-phase.md +112 -112
- package/ez-agents/workflows/add-tests.md +351 -351
- package/ez-agents/workflows/add-todo.md +158 -158
- package/ez-agents/workflows/arch-review.md +54 -0
- package/ez-agents/workflows/audit-milestone.md +332 -332
- package/ez-agents/workflows/autonomous.md +131 -30
- package/ez-agents/workflows/check-todos.md +177 -177
- package/ez-agents/workflows/cleanup.md +152 -152
- package/ez-agents/workflows/complete-milestone.md +766 -766
- package/ez-agents/workflows/diagnose-issues.md +219 -219
- package/ez-agents/workflows/discovery-phase.md +289 -289
- package/ez-agents/workflows/discuss-phase.md +762 -762
- package/ez-agents/workflows/execute-phase.md +513 -468
- package/ez-agents/workflows/execute-plan.md +483 -483
- package/ez-agents/workflows/export-session.md +255 -0
- package/ez-agents/workflows/gather-requirements.md +206 -0
- package/ez-agents/workflows/health.md +159 -159
- package/ez-agents/workflows/help.md +584 -492
- package/ez-agents/workflows/hotfix.md +291 -0
- package/ez-agents/workflows/import-session.md +303 -0
- package/ez-agents/workflows/insert-phase.md +130 -130
- package/ez-agents/workflows/list-phase-assumptions.md +178 -178
- package/ez-agents/workflows/map-codebase.md +316 -316
- package/ez-agents/workflows/new-milestone.md +339 -10
- package/ez-agents/workflows/new-project.md +293 -299
- package/ez-agents/workflows/node-repair.md +92 -92
- package/ez-agents/workflows/pause-work.md +122 -122
- package/ez-agents/workflows/plan-milestone-gaps.md +274 -274
- package/ez-agents/workflows/plan-phase.md +673 -651
- package/ez-agents/workflows/progress.md +372 -382
- package/ez-agents/workflows/quick.md +610 -610
- package/ez-agents/workflows/release.md +253 -0
- package/ez-agents/workflows/remove-phase.md +155 -155
- package/ez-agents/workflows/research-phase.md +74 -74
- package/ez-agents/workflows/resume-project.md +307 -307
- package/ez-agents/workflows/resume-session.md +215 -0
- package/ez-agents/workflows/set-profile.md +81 -81
- package/ez-agents/workflows/settings.md +242 -242
- package/ez-agents/workflows/standup.md +64 -0
- package/ez-agents/workflows/stats.md +57 -57
- package/ez-agents/workflows/transition.md +544 -544
- package/ez-agents/workflows/ui-phase.md +290 -290
- package/ez-agents/workflows/ui-review.md +157 -157
- package/ez-agents/workflows/update.md +320 -320
- package/ez-agents/workflows/validate-phase.md +167 -167
- package/ez-agents/workflows/verify-phase.md +243 -243
- package/ez-agents/workflows/verify-work.md +584 -584
- package/package.json +10 -4
- package/scripts/build-hooks.js +43 -43
- package/scripts/run-tests.cjs +29 -29
|
@@ -0,0 +1,1157 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Git Workflow Engine
|
|
5
|
+
*
|
|
6
|
+
* Enterprise-grade Git workflow management with branch hierarchy,
|
|
7
|
+
* validation gates, and automated merging.
|
|
8
|
+
*
|
|
9
|
+
* Branch Hierarchy:
|
|
10
|
+
* main (production) ← develop (staging) ← phase/* ← {feature,fix,docs,refactor}/*
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const GitUtils = require('./git-utils.cjs');
|
|
14
|
+
const Logger = require('./logger.cjs');
|
|
15
|
+
const {
|
|
16
|
+
GitWorkflowError,
|
|
17
|
+
BranchExistsError,
|
|
18
|
+
BranchNotFoundError,
|
|
19
|
+
MergeConflictError,
|
|
20
|
+
ValidationFailedError
|
|
21
|
+
} = require('./git-errors.cjs');
|
|
22
|
+
|
|
23
|
+
class GitWorkflowEngine {
|
|
24
|
+
constructor(config = {}) {
|
|
25
|
+
this.git = new GitUtils(process.cwd());
|
|
26
|
+
this.logger = new Logger();
|
|
27
|
+
this.config = config;
|
|
28
|
+
this.validationLevels = {
|
|
29
|
+
minimal: ['format', 'lint'],
|
|
30
|
+
standard: ['format', 'lint', 'test'],
|
|
31
|
+
full: ['format', 'lint', 'test', 'security', 'performance']
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Detect branch type from name
|
|
37
|
+
*/
|
|
38
|
+
detectBranchType(branchName) {
|
|
39
|
+
const patterns = {
|
|
40
|
+
feature: /^feature\//,
|
|
41
|
+
fix: /^fix\//,
|
|
42
|
+
docs: /^docs\//,
|
|
43
|
+
refactor: /^refactor\//,
|
|
44
|
+
phase: /^phase\//,
|
|
45
|
+
release: /^release\//,
|
|
46
|
+
hotfix: /^hotfix\//
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
for (const [type, pattern] of Object.entries(patterns)) {
|
|
50
|
+
if (pattern.test(branchName)) {
|
|
51
|
+
return type;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Validate branch naming convention
|
|
59
|
+
*/
|
|
60
|
+
validateBranchNaming(branchName, type) {
|
|
61
|
+
const patterns = {
|
|
62
|
+
feature: /^feature\/[a-zA-Z0-9\-_]+$/,
|
|
63
|
+
fix: /^fix\/[a-zA-Z0-9\-_]+$/,
|
|
64
|
+
docs: /^docs\/[a-zA-Z0-9\-_]+$/,
|
|
65
|
+
refactor: /^refactor\/[a-zA-Z0-9\-_]+$/,
|
|
66
|
+
phase: /^phase\/\d+-[a-zA-Z0-9\-_]+$/,
|
|
67
|
+
release: /^release\/v\d+\.\d+\.\d+$/,
|
|
68
|
+
hotfix: /^hotfix\/[a-zA-Z0-9\-_]+$/
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const pattern = patterns[type];
|
|
72
|
+
if (!pattern) {
|
|
73
|
+
throw new ValidationFailedError('branch_type', [`Unknown branch type: ${type}`]);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!pattern.test(branchName)) {
|
|
77
|
+
throw new ValidationFailedError('branch_naming', [
|
|
78
|
+
`Branch '${branchName}' does not match ${type} pattern: ${pattern}`
|
|
79
|
+
]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Validate merge strategy
|
|
87
|
+
*/
|
|
88
|
+
_validateStrategy(strategy) {
|
|
89
|
+
const validStrategies = ['merge', 'squash', 'rebase'];
|
|
90
|
+
if (!validStrategies.includes(strategy)) {
|
|
91
|
+
throw new ValidationFailedError('merge_strategy', [
|
|
92
|
+
`Invalid merge strategy: ${strategy}. Must be one of: ${validStrategies.join(', ')}`
|
|
93
|
+
]);
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Create phase branch from develop
|
|
100
|
+
* PHASE-GIT-01: Auto-create phase branch from develop
|
|
101
|
+
*/
|
|
102
|
+
async createPhaseBranch(phaseNumber, phaseSlug) {
|
|
103
|
+
const branchName = `phase/${phaseNumber}-${phaseSlug}`;
|
|
104
|
+
|
|
105
|
+
// Validate naming convention
|
|
106
|
+
this.validateBranchNaming(branchName, 'phase');
|
|
107
|
+
|
|
108
|
+
// Check if branch exists
|
|
109
|
+
if (await this.git.branchExists(branchName)) {
|
|
110
|
+
throw new BranchExistsError(branchName);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Determine source branch (develop or main)
|
|
114
|
+
let sourceBranch = 'develop';
|
|
115
|
+
if (!(await this.git.branchExists('develop'))) {
|
|
116
|
+
this.logger.warn('develop branch not found, using main', { sourceBranch: 'main' });
|
|
117
|
+
sourceBranch = 'main';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Create branch
|
|
121
|
+
await this.git.createBranch(branchName, sourceBranch);
|
|
122
|
+
|
|
123
|
+
this.logger.info('Phase branch created', {
|
|
124
|
+
branch: branchName,
|
|
125
|
+
phaseNumber,
|
|
126
|
+
phaseSlug,
|
|
127
|
+
source: sourceBranch
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return branchName;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Create feature/fix/docs/refactor branch
|
|
135
|
+
* PHASE-GIT-02: Auto-create feature/fix/docs/refactor branches within phase
|
|
136
|
+
*/
|
|
137
|
+
async createWorkBranch(type, ticketId = null, slug) {
|
|
138
|
+
const validTypes = ['feature', 'fix', 'docs', 'refactor'];
|
|
139
|
+
|
|
140
|
+
if (!validTypes.includes(type)) {
|
|
141
|
+
throw new ValidationFailedError('branch_type', [
|
|
142
|
+
`Invalid branch type: ${type}. Must be one of: ${validTypes.join(', ')}`
|
|
143
|
+
]);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Build branch name
|
|
147
|
+
let branchName;
|
|
148
|
+
if (ticketId && ['feature', 'fix'].includes(type)) {
|
|
149
|
+
branchName = `${type}/${ticketId}-${slug}`;
|
|
150
|
+
} else {
|
|
151
|
+
branchName = `${type}/${slug}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Validate naming convention
|
|
155
|
+
this.validateBranchNaming(branchName, type);
|
|
156
|
+
|
|
157
|
+
// Check if branch exists
|
|
158
|
+
if (await this.git.branchExists(branchName)) {
|
|
159
|
+
throw new BranchExistsError(branchName);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Source from current branch (should be phase branch or develop)
|
|
163
|
+
const sourceBranch = await this.git.getCurrentBranch();
|
|
164
|
+
|
|
165
|
+
// Create branch
|
|
166
|
+
await this.git.createBranch(branchName, sourceBranch);
|
|
167
|
+
|
|
168
|
+
this.logger.info('Work branch created', {
|
|
169
|
+
branch: branchName,
|
|
170
|
+
type,
|
|
171
|
+
ticketId,
|
|
172
|
+
slug,
|
|
173
|
+
source: sourceBranch
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return branchName;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Create atomic commit for task completion
|
|
181
|
+
* PHASE-GIT-03: Auto-commit after each task completion
|
|
182
|
+
* PHASE-GIT-04: Commit message format: <type>(scope): <description> [TASK-XX]
|
|
183
|
+
*/
|
|
184
|
+
async commitTask(taskDescription, taskId, files = []) {
|
|
185
|
+
// Parse task ID to number for formatting
|
|
186
|
+
const taskNum = parseInt(taskId.replace('TASK-', ''), 10);
|
|
187
|
+
const formattedTaskId = `TASK-${String(taskNum).padStart(2, '0')}`;
|
|
188
|
+
|
|
189
|
+
// Extract commit type from task description
|
|
190
|
+
const typePatterns = {
|
|
191
|
+
feat: /add|implement|create|new|enable/i,
|
|
192
|
+
fix: /fix|resolve|patch|correct/i,
|
|
193
|
+
docs: /document|update docs|readme/i,
|
|
194
|
+
refactor: /refactor|restructure|reorganize/i,
|
|
195
|
+
test: /test|add tests/i,
|
|
196
|
+
chore: /update|configure|setup|clean/i
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
let commitType = 'chore';
|
|
200
|
+
for (const [type, pattern] of Object.entries(typePatterns)) {
|
|
201
|
+
if (pattern.test(taskDescription)) {
|
|
202
|
+
commitType = type;
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Extract scope (optional)
|
|
208
|
+
const scopePattern = /\[([^\]]+)\]/;
|
|
209
|
+
const scopeMatch = taskDescription.match(scopePattern);
|
|
210
|
+
const scope = scopeMatch ? `(${scopeMatch[1]})` : '';
|
|
211
|
+
|
|
212
|
+
// Clean description
|
|
213
|
+
let cleanDescription = taskDescription
|
|
214
|
+
.replace(scopePattern, '')
|
|
215
|
+
.replace(/^(add|implement|create|fix|update|resolve)\s+/i, '')
|
|
216
|
+
.trim();
|
|
217
|
+
|
|
218
|
+
// Build commit message
|
|
219
|
+
const commitMessage = `${commitType}${scope}: ${cleanDescription} [${formattedTaskId}]`;
|
|
220
|
+
|
|
221
|
+
// Create commit
|
|
222
|
+
const commitHash = await this.git.commitAtomic(commitMessage, files);
|
|
223
|
+
|
|
224
|
+
this.logger.info('Task commit created', {
|
|
225
|
+
hash: commitHash,
|
|
226
|
+
message: commitMessage,
|
|
227
|
+
taskId: formattedTaskId
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
return commitHash;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Validate planning file consistency
|
|
235
|
+
*/
|
|
236
|
+
async _validatePlanningFiles() {
|
|
237
|
+
const fs = require('fs');
|
|
238
|
+
const path = require('path');
|
|
239
|
+
const errors = [];
|
|
240
|
+
|
|
241
|
+
// Check STATE.md exists
|
|
242
|
+
const statePath = path.join(process.cwd(), '.planning', 'STATE.md');
|
|
243
|
+
if (!fs.existsSync(statePath)) {
|
|
244
|
+
errors.push('STATE.md not found');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Check STATE.md has required frontmatter
|
|
248
|
+
if (fs.existsSync(statePath)) {
|
|
249
|
+
const stateContent = fs.readFileSync(statePath, 'utf-8');
|
|
250
|
+
if (!stateContent.includes('current_phase:') || !stateContent.includes('status:')) {
|
|
251
|
+
errors.push('STATE.md missing required frontmatter fields');
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
name: 'planning_consistency',
|
|
257
|
+
passed: errors.length === 0,
|
|
258
|
+
message: errors.length === 0 ? 'Planning files consistent' : errors.join('; ')
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Run format check (Prettier)
|
|
264
|
+
*/
|
|
265
|
+
async _runFormatCheck() {
|
|
266
|
+
const { execFile } = require('child_process');
|
|
267
|
+
const { promisify } = require('util');
|
|
268
|
+
const execFileAsync = promisify(execFile);
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
await execFileAsync('npx', ['prettier', '--check', '.'], { cwd: process.cwd() });
|
|
272
|
+
return { name: 'format', passed: true, message: 'Format check passed' };
|
|
273
|
+
} catch (err) {
|
|
274
|
+
return { name: 'format', passed: false, message: 'Format check failed' };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Run lint check (ESLint)
|
|
280
|
+
*/
|
|
281
|
+
async _runLintCheck() {
|
|
282
|
+
const { execFile } = require('child_process');
|
|
283
|
+
const { promisify } = require('util');
|
|
284
|
+
const execFileAsync = promisify(execFile);
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
await execFileAsync('npx', ['eslint', '.'], { cwd: process.cwd() });
|
|
288
|
+
return { name: 'lint', passed: true, message: 'Lint check passed' };
|
|
289
|
+
} catch (err) {
|
|
290
|
+
return { name: 'lint', passed: false, message: 'Lint check failed' };
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Run test check
|
|
296
|
+
*/
|
|
297
|
+
async _runTestCheck() {
|
|
298
|
+
const { execFile } = require('child_process');
|
|
299
|
+
const { promisify } = require('util');
|
|
300
|
+
const execFileAsync = promisify(execFile);
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
await execFileAsync('npm', ['test'], { cwd: process.cwd() });
|
|
304
|
+
return { name: 'test', passed: true, message: 'Test check passed' };
|
|
305
|
+
} catch (err) {
|
|
306
|
+
return { name: 'test', passed: false, message: 'Test check failed' };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Run security check (npm audit)
|
|
312
|
+
*/
|
|
313
|
+
async _runSecurityCheck() {
|
|
314
|
+
const { execFile } = require('child_process');
|
|
315
|
+
const { promisify } = require('util');
|
|
316
|
+
const execFileAsync = promisify(execFile);
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
await execFileAsync('npm', ['audit', '--audit-level=critical'], { cwd: process.cwd() });
|
|
320
|
+
return { name: 'security', passed: true, message: 'Security check passed' };
|
|
321
|
+
} catch (err) {
|
|
322
|
+
return { name: 'security', passed: false, message: 'Security check failed: critical vulnerabilities found' };
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Validate branch before merge
|
|
328
|
+
* PHASE-GIT-05: Validate feature/fix branches before merge to phase
|
|
329
|
+
*/
|
|
330
|
+
async validateBeforeMerge(branch, validationLevel = 'standard') {
|
|
331
|
+
const currentBranch = await this.git.getCurrentBranch();
|
|
332
|
+
const checks = [];
|
|
333
|
+
let passed = true;
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
// Switch to branch for validation
|
|
337
|
+
await this.git.checkout(branch);
|
|
338
|
+
|
|
339
|
+
// Get validation checks based on level
|
|
340
|
+
const requiredChecks = this.validationLevels[validationLevel] || this.validationLevels.standard;
|
|
341
|
+
|
|
342
|
+
// Planning file consistency check (always run)
|
|
343
|
+
const planningCheck = await this._validatePlanningFiles();
|
|
344
|
+
checks.push(planningCheck);
|
|
345
|
+
if (!planningCheck.passed) passed = false;
|
|
346
|
+
|
|
347
|
+
// Format check
|
|
348
|
+
if (requiredChecks.includes('format')) {
|
|
349
|
+
const formatCheck = await this._runFormatCheck();
|
|
350
|
+
checks.push(formatCheck);
|
|
351
|
+
if (!formatCheck.passed) passed = false;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Lint check
|
|
355
|
+
if (requiredChecks.includes('lint')) {
|
|
356
|
+
const lintCheck = await this._runLintCheck();
|
|
357
|
+
checks.push(lintCheck);
|
|
358
|
+
if (!lintCheck.passed) passed = false;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Test check
|
|
362
|
+
if (requiredChecks.includes('test')) {
|
|
363
|
+
const testCheck = await this._runTestCheck();
|
|
364
|
+
checks.push(testCheck);
|
|
365
|
+
if (!testCheck.passed) passed = false;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Security check (full level only)
|
|
369
|
+
if (requiredChecks.includes('security')) {
|
|
370
|
+
const securityCheck = await this._runSecurityCheck();
|
|
371
|
+
checks.push(securityCheck);
|
|
372
|
+
if (!securityCheck.passed) passed = false;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const result = {
|
|
376
|
+
branch,
|
|
377
|
+
validationLevel,
|
|
378
|
+
passed,
|
|
379
|
+
checks,
|
|
380
|
+
timestamp: new Date().toISOString()
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
this.logger.info('Validation completed', result);
|
|
384
|
+
|
|
385
|
+
if (!passed) {
|
|
386
|
+
throw new ValidationFailedError('pre_merge', checks.filter(c => !c.passed).map(c => c.message));
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return result;
|
|
390
|
+
} finally {
|
|
391
|
+
// Restore original branch
|
|
392
|
+
if (currentBranch !== branch) {
|
|
393
|
+
await this.git.checkout(currentBranch);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Merge feature/fix branch to phase branch
|
|
400
|
+
* PHASE-GIT-06: Auto-merge feature/fix branches to phase branch after validation
|
|
401
|
+
*/
|
|
402
|
+
async mergeToPhase(featureBranch, phaseBranch) {
|
|
403
|
+
const branchType = this.detectBranchType(featureBranch);
|
|
404
|
+
const strategy = this.config.git?.merge_strategies?.[branchType] || 'squash';
|
|
405
|
+
|
|
406
|
+
this.logger.info('Merging to phase', {
|
|
407
|
+
source: featureBranch,
|
|
408
|
+
target: phaseBranch,
|
|
409
|
+
strategy
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// Validate before merge
|
|
413
|
+
await this.validateBeforeMerge(featureBranch, 'standard');
|
|
414
|
+
|
|
415
|
+
// Check for conflicts
|
|
416
|
+
const hasConflicts = await this.git.hasConflicts(featureBranch, phaseBranch);
|
|
417
|
+
if (hasConflicts) {
|
|
418
|
+
throw new MergeConflictError(featureBranch, phaseBranch);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Perform merge
|
|
422
|
+
await this.git.mergeWithStrategy(featureBranch, phaseBranch, strategy);
|
|
423
|
+
|
|
424
|
+
this.logger.info('Merge to phase completed', {
|
|
425
|
+
source: featureBranch,
|
|
426
|
+
target: phaseBranch
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
return { success: true, source: featureBranch, target: phaseBranch };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Merge phase branch to develop
|
|
434
|
+
* PHASE-GIT-08: Auto-merge phase branch to develop after validation
|
|
435
|
+
*/
|
|
436
|
+
async mergePhaseToDevelop(phaseBranch) {
|
|
437
|
+
const strategy = this.config.git?.merge_strategies?.phase || 'merge';
|
|
438
|
+
const targetBranch = 'develop';
|
|
439
|
+
|
|
440
|
+
this.logger.info('Merging phase to develop', {
|
|
441
|
+
source: phaseBranch,
|
|
442
|
+
target: targetBranch,
|
|
443
|
+
strategy
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// Validate before merge
|
|
447
|
+
await this.validateBeforeMerge(phaseBranch, 'full');
|
|
448
|
+
|
|
449
|
+
// Check for conflicts
|
|
450
|
+
const hasConflicts = await this.git.hasConflicts(phaseBranch, targetBranch);
|
|
451
|
+
if (hasConflicts) {
|
|
452
|
+
throw new MergeConflictError(phaseBranch, targetBranch);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Perform merge
|
|
456
|
+
await this.git.mergeWithStrategy(phaseBranch, targetBranch, strategy);
|
|
457
|
+
|
|
458
|
+
this.logger.info('Phase merge to develop completed', {
|
|
459
|
+
source: phaseBranch,
|
|
460
|
+
target: targetBranch
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
return { success: true, source: phaseBranch, target: targetBranch };
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Create release branch from develop
|
|
468
|
+
* PHASE-GIT-09: Create release branch from develop for stabilization
|
|
469
|
+
*/
|
|
470
|
+
async createReleaseBranch(version) {
|
|
471
|
+
const semver = require('semver');
|
|
472
|
+
|
|
473
|
+
// Validate version format
|
|
474
|
+
const validVersion = semver.valid(version);
|
|
475
|
+
if (!validVersion) {
|
|
476
|
+
throw new ValidationFailedError('version_format', [
|
|
477
|
+
`Invalid version format: ${version}. Must be valid semver (e.g., 2.0.0)`
|
|
478
|
+
]);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const branchName = `release/v${validVersion}`;
|
|
482
|
+
|
|
483
|
+
// Check if branch exists
|
|
484
|
+
if (await this.git.branchExists(branchName)) {
|
|
485
|
+
throw new BranchExistsError(branchName);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Source from develop
|
|
489
|
+
if (!(await this.git.branchExists('develop'))) {
|
|
490
|
+
throw new BranchNotFoundError('develop');
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Create branch
|
|
494
|
+
await this.git.createBranch(branchName, 'develop');
|
|
495
|
+
|
|
496
|
+
this.logger.info('Release branch created', {
|
|
497
|
+
branch: branchName,
|
|
498
|
+
version: validVersion
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
return branchName;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Run integration tests
|
|
506
|
+
*/
|
|
507
|
+
async _runIntegrationTestCheck() {
|
|
508
|
+
const { execFile } = require('child_process');
|
|
509
|
+
const { promisify } = require('util');
|
|
510
|
+
const execFileAsync = promisify(execFile);
|
|
511
|
+
|
|
512
|
+
try {
|
|
513
|
+
// Try to run integration tests if they exist
|
|
514
|
+
await execFileAsync('npm', ['run', 'test:integration'], { cwd: process.cwd() });
|
|
515
|
+
return { name: 'integration_tests', passed: true, message: 'Integration tests passed' };
|
|
516
|
+
} catch (err) {
|
|
517
|
+
// If integration test script doesn't exist, skip
|
|
518
|
+
if (err.code === 1 || err.stderr?.includes('Missing script')) {
|
|
519
|
+
return { name: 'integration_tests', passed: true, message: 'Integration tests not configured, skipped' };
|
|
520
|
+
}
|
|
521
|
+
return { name: 'integration_tests', passed: false, message: 'Integration tests failed' };
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Run dependency audit
|
|
527
|
+
*/
|
|
528
|
+
async _runDependencyAudit() {
|
|
529
|
+
const { execFile } = require('child_process');
|
|
530
|
+
const { promisify } = require('util');
|
|
531
|
+
const execFileAsync = promisify(execFile);
|
|
532
|
+
|
|
533
|
+
try {
|
|
534
|
+
await execFileAsync('npm', ['audit', '--production', '--audit-level=high'], { cwd: process.cwd() });
|
|
535
|
+
return { name: 'dependency_audit', passed: true, message: 'Dependency audit passed' };
|
|
536
|
+
} catch (err) {
|
|
537
|
+
return { name: 'dependency_audit', passed: false, message: 'Dependency audit failed: high/critical vulnerabilities' };
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Run critical bug detection
|
|
543
|
+
*/
|
|
544
|
+
async _runCriticalBugDetection() {
|
|
545
|
+
// Check for common critical bug patterns in code
|
|
546
|
+
const fs = require('fs');
|
|
547
|
+
const path = require('path');
|
|
548
|
+
const errors = [];
|
|
549
|
+
|
|
550
|
+
// Example: Check for console.log in production code (excluding tests)
|
|
551
|
+
const srcDir = path.join(process.cwd(), 'ez-agents', 'bin', 'lib');
|
|
552
|
+
if (fs.existsSync(srcDir)) {
|
|
553
|
+
const files = fs.readdirSync(srcDir).filter(f => f.endsWith('.cjs'));
|
|
554
|
+
for (const file of files) {
|
|
555
|
+
const content = fs.readFileSync(path.join(srcDir, file), 'utf-8');
|
|
556
|
+
if (content.includes('console.log(') && !content.includes('// console.log')) {
|
|
557
|
+
errors.push(`Potential debug logging in ${file}`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return {
|
|
563
|
+
name: 'critical_bug_detection',
|
|
564
|
+
passed: errors.length === 0,
|
|
565
|
+
message: errors.length === 0 ? 'No critical bugs detected' : errors.join('; ')
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Validate release branch stability
|
|
571
|
+
* PHASE-GIT-10: Run full test suite, integration tests, security scans on release branch
|
|
572
|
+
* PHASE-GIT-11: Validate release branch stability (zero critical bugs, all tests green)
|
|
573
|
+
*/
|
|
574
|
+
async validateReleaseBranch(releaseBranch) {
|
|
575
|
+
const currentBranch = await this.git.getCurrentBranch();
|
|
576
|
+
const checks = [];
|
|
577
|
+
let passed = true;
|
|
578
|
+
let criticalFailures = 0;
|
|
579
|
+
|
|
580
|
+
try {
|
|
581
|
+
// Switch to release branch
|
|
582
|
+
await this.git.checkout(releaseBranch);
|
|
583
|
+
|
|
584
|
+
// Full test suite
|
|
585
|
+
this.logger.info('Running full test suite', { branch: releaseBranch });
|
|
586
|
+
const testCheck = await this._runTestCheck();
|
|
587
|
+
testCheck.name = 'full_test_suite';
|
|
588
|
+
testCheck.critical = true;
|
|
589
|
+
checks.push(testCheck);
|
|
590
|
+
if (!testCheck.passed) {
|
|
591
|
+
passed = false;
|
|
592
|
+
criticalFailures++;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Integration tests
|
|
596
|
+
this.logger.info('Running integration tests', { branch: releaseBranch });
|
|
597
|
+
const integrationCheck = await this._runIntegrationTestCheck();
|
|
598
|
+
integrationCheck.critical = true;
|
|
599
|
+
checks.push(integrationCheck);
|
|
600
|
+
if (!integrationCheck.passed) {
|
|
601
|
+
passed = false;
|
|
602
|
+
criticalFailures++;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Security scan - npm audit
|
|
606
|
+
this.logger.info('Running security audit', { branch: releaseBranch });
|
|
607
|
+
const securityCheck = await this._runSecurityCheck();
|
|
608
|
+
securityCheck.critical = true;
|
|
609
|
+
checks.push(securityCheck);
|
|
610
|
+
if (!securityCheck.passed) {
|
|
611
|
+
passed = false;
|
|
612
|
+
criticalFailures++;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Dependency vulnerability scan
|
|
616
|
+
this.logger.info('Scanning dependencies', { branch: releaseBranch });
|
|
617
|
+
const dependencyCheck = await this._runDependencyAudit();
|
|
618
|
+
dependencyCheck.critical = true;
|
|
619
|
+
checks.push(dependencyCheck);
|
|
620
|
+
if (!dependencyCheck.passed) {
|
|
621
|
+
passed = false;
|
|
622
|
+
criticalFailures++;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Critical bug detection (check for known critical patterns)
|
|
626
|
+
this.logger.info('Checking for critical bugs', { branch: releaseBranch });
|
|
627
|
+
const criticalBugCheck = await this._runCriticalBugDetection();
|
|
628
|
+
criticalBugCheck.critical = true;
|
|
629
|
+
checks.push(criticalBugCheck);
|
|
630
|
+
if (!criticalBugCheck.passed) {
|
|
631
|
+
passed = false;
|
|
632
|
+
criticalFailures++;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const result = {
|
|
636
|
+
branch: releaseBranch,
|
|
637
|
+
passed,
|
|
638
|
+
criticalFailures,
|
|
639
|
+
checks,
|
|
640
|
+
timestamp: new Date().toISOString()
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
this.logger.info('Release validation completed', result);
|
|
644
|
+
|
|
645
|
+
if (criticalFailures > 0) {
|
|
646
|
+
throw new ValidationFailedError('release_stability', [
|
|
647
|
+
`${criticalFailures} critical check(s) failed`,
|
|
648
|
+
...checks.filter(c => c.critical && !c.passed).map(c => c.message)
|
|
649
|
+
]);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
return result;
|
|
653
|
+
} finally {
|
|
654
|
+
// Restore original branch
|
|
655
|
+
if (currentBranch !== releaseBranch) {
|
|
656
|
+
await this.git.checkout(currentBranch);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Bump version in package.json
|
|
663
|
+
*/
|
|
664
|
+
async _bumpVersion(newVersion) {
|
|
665
|
+
const fs = require('fs');
|
|
666
|
+
const path = require('path');
|
|
667
|
+
|
|
668
|
+
const packagePath = path.join(process.cwd(), 'package.json');
|
|
669
|
+
if (fs.existsSync(packagePath)) {
|
|
670
|
+
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf-8'));
|
|
671
|
+
packageJson.version = newVersion;
|
|
672
|
+
fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2) + '\n');
|
|
673
|
+
|
|
674
|
+
// Commit version bump
|
|
675
|
+
await this.git.add('package.json');
|
|
676
|
+
await this.git.commitAtomic(`chore: bump version to ${newVersion} [RELEASE]`, ['package.json']);
|
|
677
|
+
|
|
678
|
+
this.logger.info('Version bumped', { version: newVersion });
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Merge release to main with version tag
|
|
684
|
+
* PHASE-GIT-12: Merge release to main with version tag
|
|
685
|
+
* PHASE-GIT-13: Merge release back to develop with version bump
|
|
686
|
+
*/
|
|
687
|
+
async mergeReleaseToMain(releaseBranch) {
|
|
688
|
+
const semver = require('semver');
|
|
689
|
+
|
|
690
|
+
// Extract version from branch name
|
|
691
|
+
const versionMatch = releaseBranch.match(/^release\/v(\d+\.\d+\.\d+)$/);
|
|
692
|
+
if (!versionMatch) {
|
|
693
|
+
throw new ValidationFailedError('release_branch_format', [
|
|
694
|
+
`Invalid release branch format: ${releaseBranch}. Expected: release/vX.Y.Z`
|
|
695
|
+
]);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const version = versionMatch[1];
|
|
699
|
+
const tagName = `v${version}`;
|
|
700
|
+
|
|
701
|
+
this.logger.info('Merging release to main', {
|
|
702
|
+
releaseBranch,
|
|
703
|
+
version,
|
|
704
|
+
tagName
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
// Validate release branch
|
|
708
|
+
await this.validateReleaseBranch(releaseBranch);
|
|
709
|
+
|
|
710
|
+
// Merge to main
|
|
711
|
+
await this.git.checkout('main');
|
|
712
|
+
await this.git.mergeWithStrategy(releaseBranch, 'main', 'merge');
|
|
713
|
+
|
|
714
|
+
// Create tag
|
|
715
|
+
await this.git.tagRelease(tagName, `Release ${tagName}`);
|
|
716
|
+
|
|
717
|
+
this.logger.info('Release merged to main', {
|
|
718
|
+
branch: 'main',
|
|
719
|
+
tag: tagName
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
// Merge back to develop with version bump
|
|
723
|
+
await this.git.checkout('develop');
|
|
724
|
+
await this.git.mergeWithStrategy(releaseBranch, 'develop', 'merge');
|
|
725
|
+
|
|
726
|
+
// Bump version in package.json
|
|
727
|
+
await this._bumpVersion(version);
|
|
728
|
+
|
|
729
|
+
this.logger.info('Release merged to develop', {
|
|
730
|
+
branch: 'develop',
|
|
731
|
+
version
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
return {
|
|
735
|
+
success: true,
|
|
736
|
+
releaseBranch,
|
|
737
|
+
mainTag: tagName,
|
|
738
|
+
version
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Create pull request for enterprise mode
|
|
744
|
+
*/
|
|
745
|
+
async _createPullRequest(source, target, options = {}) {
|
|
746
|
+
const { Octokit } = require('@octokit/rest');
|
|
747
|
+
|
|
748
|
+
// Check if GitHub token is configured
|
|
749
|
+
const githubToken = process.env.GITHUB_TOKEN;
|
|
750
|
+
if (!githubToken) {
|
|
751
|
+
throw new ValidationFailedError('github_auth', [
|
|
752
|
+
'GITHUB_TOKEN environment variable not set. Required for enterprise PR workflow.'
|
|
753
|
+
]);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const octokit = new Octokit({ auth: githubToken });
|
|
757
|
+
|
|
758
|
+
// Get repository info
|
|
759
|
+
const repoPath = process.cwd();
|
|
760
|
+
const { execFile } = require('child_process');
|
|
761
|
+
const { promisify } = require('util');
|
|
762
|
+
const execFileAsync = promisify(execFile);
|
|
763
|
+
|
|
764
|
+
try {
|
|
765
|
+
const { stdout: remoteUrl } = await execFileAsync('git', ['remote', 'get-url', 'origin'], { cwd: repoPath });
|
|
766
|
+
const repoMatch = remoteUrl.match(/github\.com[:/]([^/]+)\/([^.]+)\.git/);
|
|
767
|
+
|
|
768
|
+
if (!repoMatch) {
|
|
769
|
+
throw new Error('Could not parse repository URL');
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const [, owner, repo] = repoMatch;
|
|
773
|
+
|
|
774
|
+
// Create PR
|
|
775
|
+
const prTitle = options.title || `Merge '${source}' into '${target}'`;
|
|
776
|
+
const prBody = options.body || `Automated PR for merging ${source} into ${target}`;
|
|
777
|
+
|
|
778
|
+
const { data: pr } = await octokit.pulls.create({
|
|
779
|
+
owner,
|
|
780
|
+
repo,
|
|
781
|
+
title: prTitle,
|
|
782
|
+
body: prBody,
|
|
783
|
+
head: source,
|
|
784
|
+
base: target
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
this.logger.info('Pull request created', {
|
|
788
|
+
number: pr.number,
|
|
789
|
+
url: pr.html_url,
|
|
790
|
+
source,
|
|
791
|
+
target
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
return {
|
|
795
|
+
success: true,
|
|
796
|
+
mode: 'enterprise',
|
|
797
|
+
pullRequest: pr.number,
|
|
798
|
+
url: pr.html_url,
|
|
799
|
+
requiredReviewers: options.requiredReviewers,
|
|
800
|
+
createdAt: new Date().toISOString()
|
|
801
|
+
};
|
|
802
|
+
} catch (err) {
|
|
803
|
+
this.logger.error('Failed to create pull request', { error: err.message });
|
|
804
|
+
throw new GitWorkflowError(`Failed to create PR: ${err.message}`, {
|
|
805
|
+
code: 'PR_CREATION_FAILED',
|
|
806
|
+
details: { source, target }
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Merge branch with enterprise/open source mode support
|
|
813
|
+
* PHASE-GIT-14: Support enterprise workflow (protected branches, PR required, code review)
|
|
814
|
+
* PHASE-GIT-15: Support open source workflow (direct merge after automated validation)
|
|
815
|
+
*/
|
|
816
|
+
async mergeBranch(source, target, options = {}) {
|
|
817
|
+
const enterpriseMode = this.config.git?.enterprise_mode?.require_pull_request || false;
|
|
818
|
+
const requiredReviewers = this.config.git?.enterprise_mode?.required_reviewers || 1;
|
|
819
|
+
|
|
820
|
+
// Validate strategy
|
|
821
|
+
const strategy = options.strategy || this.config.git?.merge_strategies?.[this.detectBranchType(source)] || 'merge';
|
|
822
|
+
this._validateStrategy(strategy);
|
|
823
|
+
|
|
824
|
+
this.logger.info('Merge branch requested', {
|
|
825
|
+
source,
|
|
826
|
+
target,
|
|
827
|
+
enterpriseMode,
|
|
828
|
+
requiredReviewers,
|
|
829
|
+
strategy
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
if (enterpriseMode) {
|
|
833
|
+
// Enterprise mode: Create PR and require approval
|
|
834
|
+
return await this._createPullRequest(source, target, {
|
|
835
|
+
...options,
|
|
836
|
+
requiredReviewers
|
|
837
|
+
});
|
|
838
|
+
} else {
|
|
839
|
+
// Open source mode: Direct merge after validation
|
|
840
|
+
await this.validateBeforeMerge(source, options.validationLevel || 'standard');
|
|
841
|
+
|
|
842
|
+
await this.git.mergeWithStrategy(source, target, strategy);
|
|
843
|
+
|
|
844
|
+
this.logger.info('Direct merge completed (open source mode)', {
|
|
845
|
+
source,
|
|
846
|
+
target
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
return {
|
|
850
|
+
success: true,
|
|
851
|
+
mode: 'open_source',
|
|
852
|
+
source,
|
|
853
|
+
target,
|
|
854
|
+
mergedAt: new Date().toISOString()
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* Create and merge Hotfix
|
|
861
|
+
* PHASE-GIT-17: Hotfix workflow (create from main, merge to main + develop)
|
|
862
|
+
*/
|
|
863
|
+
async createHotfix(description) {
|
|
864
|
+
// Create slug from description
|
|
865
|
+
const slug = description
|
|
866
|
+
.toLowerCase()
|
|
867
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
868
|
+
.replace(/^-|-$/g, '');
|
|
869
|
+
|
|
870
|
+
const branchName = `hotfix/${slug}`;
|
|
871
|
+
|
|
872
|
+
// Check if branch exists
|
|
873
|
+
if (await this.git.branchExists(branchName)) {
|
|
874
|
+
throw new BranchExistsError(branchName);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Create hotfix branch from main
|
|
878
|
+
await this.git.createBranch(branchName, 'main');
|
|
879
|
+
|
|
880
|
+
this.logger.info('Hotfix branch created', {
|
|
881
|
+
branch: branchName,
|
|
882
|
+
description
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
return branchName;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* Merge hotfix to main and develop
|
|
890
|
+
*/
|
|
891
|
+
async mergeHotfix(hotfixBranch, version = null) {
|
|
892
|
+
const branchType = this.detectBranchType(hotfixBranch);
|
|
893
|
+
if (branchType !== 'hotfix') {
|
|
894
|
+
throw new ValidationFailedError('branch_type', [
|
|
895
|
+
`Expected hotfix branch, got: ${hotfixBranch}`
|
|
896
|
+
]);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
this.logger.info('Merging hotfix', { branch: hotfixBranch });
|
|
900
|
+
|
|
901
|
+
// Validate hotfix
|
|
902
|
+
await this.validateBeforeMerge(hotfixBranch, 'standard');
|
|
903
|
+
|
|
904
|
+
// Merge to main
|
|
905
|
+
await this.git.checkout('main');
|
|
906
|
+
await this.git.mergeWithStrategy(hotfixBranch, 'main', 'squash');
|
|
907
|
+
|
|
908
|
+
// Create tag if version provided
|
|
909
|
+
if (version) {
|
|
910
|
+
const tagName = `v${version}`;
|
|
911
|
+
await this.git.tagRelease(tagName, `Hotfix ${version}`);
|
|
912
|
+
this.logger.info('Hotfix tagged', { tag: tagName });
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// Merge to develop
|
|
916
|
+
await this.git.checkout('develop');
|
|
917
|
+
await this.git.mergeWithStrategy(hotfixBranch, 'develop', 'squash');
|
|
918
|
+
|
|
919
|
+
this.logger.info('Hotfix merged to main and develop', {
|
|
920
|
+
branch: hotfixBranch
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
return {
|
|
924
|
+
success: true,
|
|
925
|
+
hotfixBranch,
|
|
926
|
+
mergedTo: ['main', 'develop'],
|
|
927
|
+
version
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Rollback phase
|
|
933
|
+
* PHASE-GIT-18: Rollback capability with auto-revert if phase introduces regressions
|
|
934
|
+
*/
|
|
935
|
+
async rollbackPhase(phaseNumber) {
|
|
936
|
+
const phasePattern = `phase/${phaseNumber}-`;
|
|
937
|
+
const branches = await this.git.listBranches(phasePattern + '*');
|
|
938
|
+
|
|
939
|
+
if (branches.length === 0) {
|
|
940
|
+
throw new BranchNotFoundError(`phase/${phaseNumber}-*`);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const phaseBranch = branches[0];
|
|
944
|
+
this.logger.info('Rolling back phase', { phaseNumber, phaseBranch });
|
|
945
|
+
|
|
946
|
+
// Create rollback branch for safety
|
|
947
|
+
const rollbackBranch = `rollback/phase-${phaseNumber}-${Date.now()}`;
|
|
948
|
+
await this.git.createBranch(rollbackBranch, 'develop');
|
|
949
|
+
|
|
950
|
+
// Check if phase was merged to develop
|
|
951
|
+
const currentBranch = await this.git.getCurrentBranch();
|
|
952
|
+
await this.git.checkout('develop');
|
|
953
|
+
|
|
954
|
+
// Find merge commit
|
|
955
|
+
const { execFile } = require('child_process');
|
|
956
|
+
const { promisify } = require('util');
|
|
957
|
+
const execFileAsync = promisify(execFile);
|
|
958
|
+
|
|
959
|
+
try {
|
|
960
|
+
const { stdout } = await execFileAsync('git', [
|
|
961
|
+
'log', '--oneline', '--grep', phaseBranch, '-n', '1'
|
|
962
|
+
], { cwd: process.cwd() });
|
|
963
|
+
|
|
964
|
+
if (stdout) {
|
|
965
|
+
const mergeCommit = stdout.split(' ')[0];
|
|
966
|
+
|
|
967
|
+
// Revert the merge commit
|
|
968
|
+
await this.git.revertCommit(mergeCommit);
|
|
969
|
+
|
|
970
|
+
this.logger.info('Phase rollback completed', {
|
|
971
|
+
phaseNumber,
|
|
972
|
+
revertedCommit: mergeCommit,
|
|
973
|
+
rollbackBranch
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
return {
|
|
977
|
+
success: true,
|
|
978
|
+
phaseNumber,
|
|
979
|
+
revertedCommit: mergeCommit,
|
|
980
|
+
rollbackBranch
|
|
981
|
+
};
|
|
982
|
+
} else {
|
|
983
|
+
// Phase not merged yet, just delete the branch
|
|
984
|
+
await this.git.deleteBranch(phaseBranch, true);
|
|
985
|
+
|
|
986
|
+
this.logger.info('Phase branch deleted (not merged)', {
|
|
987
|
+
phaseNumber,
|
|
988
|
+
phaseBranch
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
return {
|
|
992
|
+
success: true,
|
|
993
|
+
phaseNumber,
|
|
994
|
+
deleted: true,
|
|
995
|
+
rollbackBranch
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
} finally {
|
|
999
|
+
await this.git.checkout(currentBranch);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* Check branch protection rules
|
|
1005
|
+
* PHASE-GIT-19: Branch protection rules enforcement (require PR, reviews, status checks)
|
|
1006
|
+
*/
|
|
1007
|
+
async checkBranchProtection(branch) {
|
|
1008
|
+
const { Octokit } = require('@octokit/rest');
|
|
1009
|
+
|
|
1010
|
+
const githubToken = process.env.GITHUB_TOKEN;
|
|
1011
|
+
if (!githubToken) {
|
|
1012
|
+
this.logger.warn('GITHUB_TOKEN not set, skipping branch protection check');
|
|
1013
|
+
return { protected: false, reason: 'no_token' };
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
const octokit = new Octokit({ auth: githubToken });
|
|
1017
|
+
|
|
1018
|
+
// Get repository info
|
|
1019
|
+
const { execFile } = require('child_process');
|
|
1020
|
+
const { promisify } = require('util');
|
|
1021
|
+
const execFileAsync = promisify(execFile);
|
|
1022
|
+
|
|
1023
|
+
try {
|
|
1024
|
+
const { stdout: remoteUrl } = await execFileAsync('git', ['remote', 'get-url', 'origin'], { cwd: process.cwd() });
|
|
1025
|
+
const repoMatch = remoteUrl.match(/github\.com[:/]([^/]+)\/([^.]+)\.git/);
|
|
1026
|
+
|
|
1027
|
+
if (!repoMatch) {
|
|
1028
|
+
throw new Error('Could not parse repository URL');
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const [, owner, repo] = repoMatch;
|
|
1032
|
+
|
|
1033
|
+
// Get branch protection rules
|
|
1034
|
+
try {
|
|
1035
|
+
const { data: protection } = await octokit.repos.getBranchProtection({
|
|
1036
|
+
owner,
|
|
1037
|
+
repo,
|
|
1038
|
+
branch
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
const result = {
|
|
1042
|
+
protected: true,
|
|
1043
|
+
requiredStatusChecks: protection.required_status_checks?.strict || false,
|
|
1044
|
+
requiredPullRequestReviews: protection.required_pull_request_reviews || null,
|
|
1045
|
+
requiredLinearHistory: protection.required_linear_history?.enabled || false,
|
|
1046
|
+
allowForcePushes: protection.allow_force_pushes?.enabled || false,
|
|
1047
|
+
allowDeletions: protection.allow_deletions?.enabled || false
|
|
1048
|
+
};
|
|
1049
|
+
|
|
1050
|
+
this.logger.info('Branch protection status', { branch, ...result });
|
|
1051
|
+
|
|
1052
|
+
// Validate enterprise mode requirements
|
|
1053
|
+
if (this.config.git?.enterprise_mode?.require_pull_request) {
|
|
1054
|
+
if (!result.requiredPullRequestReviews) {
|
|
1055
|
+
throw new ValidationFailedError('branch_protection', [
|
|
1056
|
+
`Branch '${branch}' does not require pull request reviews (enterprise mode requires it)`
|
|
1057
|
+
]);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
return result;
|
|
1062
|
+
} catch (err) {
|
|
1063
|
+
if (err.status === 404) {
|
|
1064
|
+
// Branch not protected
|
|
1065
|
+
return { protected: false, reason: 'not_protected' };
|
|
1066
|
+
}
|
|
1067
|
+
throw err;
|
|
1068
|
+
}
|
|
1069
|
+
} catch (err) {
|
|
1070
|
+
this.logger.error('Failed to check branch protection', { error: err.message });
|
|
1071
|
+
throw new GitWorkflowError(`Failed to check branch protection: ${err.message}`, {
|
|
1072
|
+
code: 'PROTECTION_CHECK_FAILED',
|
|
1073
|
+
details: { branch }
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* Enhance changelog with task IDs
|
|
1080
|
+
*/
|
|
1081
|
+
_enhanceChangelogWithTaskIds(changelog) {
|
|
1082
|
+
// Add header
|
|
1083
|
+
const header = `# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n`;
|
|
1084
|
+
|
|
1085
|
+
// Parse task IDs from commits and add to changelog
|
|
1086
|
+
const taskPattern = /\[TASK-(\d+)\]/g;
|
|
1087
|
+
const enhanced = changelog.replace(taskPattern, (match, taskId) => {
|
|
1088
|
+
return `[Task #${taskId}](https://github.com/howlil/ez-agents/issues/${taskId})`;
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
return header + enhanced;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* Generate changelog from commits
|
|
1096
|
+
* PHASE-GIT-20: Automated changelog generation from commits on merge to main
|
|
1097
|
+
*/
|
|
1098
|
+
async generateChangelog(fromTag, toTag = 'HEAD') {
|
|
1099
|
+
const conventionalChangelog = require('conventional-changelog');
|
|
1100
|
+
const fs = require('fs');
|
|
1101
|
+
const path = require('path');
|
|
1102
|
+
|
|
1103
|
+
this.logger.info('Generating changelog', { fromTag, toTag });
|
|
1104
|
+
|
|
1105
|
+
return new Promise((resolve, reject) => {
|
|
1106
|
+
const changelogStream = conventionalChangelog({
|
|
1107
|
+
preset: 'angular',
|
|
1108
|
+
releaseCount: 1
|
|
1109
|
+
}, {
|
|
1110
|
+
from: fromTag,
|
|
1111
|
+
to: toTag
|
|
1112
|
+
}, {
|
|
1113
|
+
commits: true,
|
|
1114
|
+
commitsPath: process.cwd()
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
let changelog = '';
|
|
1118
|
+
|
|
1119
|
+
changelogStream.on('data', (chunk) => {
|
|
1120
|
+
changelog += chunk.toString();
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
changelogStream.on('end', () => {
|
|
1124
|
+
// Parse and enhance with task IDs
|
|
1125
|
+
const enhancedChangelog = this._enhanceChangelogWithTaskIds(changelog);
|
|
1126
|
+
|
|
1127
|
+
// Append to CHANGELOG.md
|
|
1128
|
+
const changelogPath = path.join(process.cwd(), 'CHANGELOG.md');
|
|
1129
|
+
|
|
1130
|
+
if (fs.existsSync(changelogPath)) {
|
|
1131
|
+
const existingContent = fs.readFileSync(changelogPath, 'utf-8');
|
|
1132
|
+
fs.writeFileSync(changelogPath, enhancedChangelog + '\n' + existingContent);
|
|
1133
|
+
} else {
|
|
1134
|
+
fs.writeFileSync(changelogPath, enhancedChangelog);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
this.logger.info('Changelog generated', { path: changelogPath });
|
|
1138
|
+
|
|
1139
|
+
resolve({
|
|
1140
|
+
success: true,
|
|
1141
|
+
fromTag,
|
|
1142
|
+
toTag,
|
|
1143
|
+
path: changelogPath
|
|
1144
|
+
});
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
changelogStream.on('error', (err) => {
|
|
1148
|
+
this.logger.error('Changelog generation failed', { error: err.message });
|
|
1149
|
+
reject(new GitWorkflowError(`Changelog generation failed: ${err.message}`, {
|
|
1150
|
+
code: 'CHANGELOG_GENERATION_FAILED'
|
|
1151
|
+
}));
|
|
1152
|
+
});
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
module.exports = GitWorkflowEngine;
|