neuro-cli 4.1.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 -0
- package/README.md +510 -0
- package/dist/advisor/advisor.d.ts +50 -0
- package/dist/advisor/advisor.js +178 -0
- package/dist/agents/base.d.ts +62 -0
- package/dist/agents/base.js +215 -0
- package/dist/agents/orchestrator.d.ts +46 -0
- package/dist/agents/orchestrator.js +192 -0
- package/dist/agents/team.d.ts +51 -0
- package/dist/agents/team.js +210 -0
- package/dist/api/models.d.ts +23 -0
- package/dist/api/models.js +514 -0
- package/dist/api/ollama.d.ts +153 -0
- package/dist/api/ollama.js +751 -0
- package/dist/api/openrouter.d.ts +55 -0
- package/dist/api/openrouter.js +223 -0
- package/dist/commands/commands.d.ts +43 -0
- package/dist/commands/commands.js +308 -0
- package/dist/config/config.d.ts +8 -0
- package/dist/config/config.js +311 -0
- package/dist/context/compaction.d.ts +54 -0
- package/dist/context/compaction.js +251 -0
- package/dist/context/custom-agents.d.ts +107 -0
- package/dist/context/custom-agents.js +397 -0
- package/dist/context/custom-tools.d.ts +120 -0
- package/dist/context/custom-tools.js +564 -0
- package/dist/context/git-checkpoint.d.ts +52 -0
- package/dist/context/git-checkpoint.js +240 -0
- package/dist/context/neuro-md.d.ts +48 -0
- package/dist/context/neuro-md.js +202 -0
- package/dist/context/neuroignore.d.ts +102 -0
- package/dist/context/neuroignore.js +441 -0
- package/dist/context/repo-map.d.ts +38 -0
- package/dist/context/repo-map.js +220 -0
- package/dist/context/skill-standard.d.ts +262 -0
- package/dist/context/skill-standard.js +1156 -0
- package/dist/context/skill-system.d.ts +75 -0
- package/dist/context/skill-system.js +578 -0
- package/dist/context/tree-sitter.d.ts +99 -0
- package/dist/context/tree-sitter.js +1956 -0
- package/dist/core/acp.d.ts +325 -0
- package/dist/core/acp.js +1498 -0
- package/dist/core/api-server.d.ts +143 -0
- package/dist/core/api-server.js +550 -0
- package/dist/core/approval.d.ts +81 -0
- package/dist/core/approval.js +432 -0
- package/dist/core/auto-compact.d.ts +127 -0
- package/dist/core/auto-compact.js +436 -0
- package/dist/core/auto-mode.d.ts +232 -0
- package/dist/core/auto-mode.js +831 -0
- package/dist/core/background-session.d.ts +166 -0
- package/dist/core/background-session.js +696 -0
- package/dist/core/cicd.d.ts +257 -0
- package/dist/core/cicd.js +1443 -0
- package/dist/core/cloud-sync.d.ts +156 -0
- package/dist/core/cloud-sync.js +582 -0
- package/dist/core/code-review.d.ts +132 -0
- package/dist/core/code-review.js +1191 -0
- package/dist/core/completion.d.ts +49 -0
- package/dist/core/completion.js +384 -0
- package/dist/core/context.d.ts +38 -0
- package/dist/core/context.js +144 -0
- package/dist/core/diff-preview.d.ts +35 -0
- package/dist/core/diff-preview.js +173 -0
- package/dist/core/doom-loop.d.ts +51 -0
- package/dist/core/doom-loop.js +179 -0
- package/dist/core/engine.d.ts +183 -0
- package/dist/core/engine.js +942 -0
- package/dist/core/extended-thinking.d.ts +103 -0
- package/dist/core/extended-thinking.js +269 -0
- package/dist/core/fallback.d.ts +54 -0
- package/dist/core/fallback.js +104 -0
- package/dist/core/git-worktree.d.ts +139 -0
- package/dist/core/git-worktree.js +614 -0
- package/dist/core/headless.d.ts +30 -0
- package/dist/core/headless.js +101 -0
- package/dist/core/i18n.d.ts +83 -0
- package/dist/core/i18n.js +739 -0
- package/dist/core/linting.d.ts +129 -0
- package/dist/core/linting.js +699 -0
- package/dist/core/model-router.d.ts +109 -0
- package/dist/core/model-router.js +581 -0
- package/dist/core/multi-model.d.ts +243 -0
- package/dist/core/multi-model.js +1099 -0
- package/dist/core/multi-session.d.ts +144 -0
- package/dist/core/multi-session.js +442 -0
- package/dist/core/multimodal.d.ts +125 -0
- package/dist/core/multimodal.js +286 -0
- package/dist/core/observability.d.ts +93 -0
- package/dist/core/observability.js +737 -0
- package/dist/core/os-sandbox.d.ts +122 -0
- package/dist/core/os-sandbox.js +1193 -0
- package/dist/core/outcome-grading.d.ts +228 -0
- package/dist/core/outcome-grading.js +1123 -0
- package/dist/core/output-styles.d.ts +57 -0
- package/dist/core/output-styles.js +382 -0
- package/dist/core/parallel-agents.d.ts +183 -0
- package/dist/core/parallel-agents.js +563 -0
- package/dist/core/plugin-bundle.d.ts +236 -0
- package/dist/core/plugin-bundle.js +887 -0
- package/dist/core/plugin-sdk.d.ts +139 -0
- package/dist/core/plugin-sdk.js +273 -0
- package/dist/core/prompt-cache.d.ts +163 -0
- package/dist/core/prompt-cache.js +599 -0
- package/dist/core/sandbox.d.ts +127 -0
- package/dist/core/sandbox.js +369 -0
- package/dist/core/scheduled-tasks.d.ts +151 -0
- package/dist/core/scheduled-tasks.js +502 -0
- package/dist/core/security-scanner.d.ts +160 -0
- package/dist/core/security-scanner.js +1494 -0
- package/dist/core/session.d.ts +83 -0
- package/dist/core/session.js +269 -0
- package/dist/core/shell-completion.d.ts +51 -0
- package/dist/core/shell-completion.js +674 -0
- package/dist/core/smart-monitor.d.ts +146 -0
- package/dist/core/smart-monitor.js +1199 -0
- package/dist/core/spec-driven.d.ts +233 -0
- package/dist/core/spec-driven.js +1485 -0
- package/dist/core/spending-warnings.d.ts +123 -0
- package/dist/core/spending-warnings.js +456 -0
- package/dist/core/sub-agent.d.ts +298 -0
- package/dist/core/sub-agent.js +1023 -0
- package/dist/core/telemetry.d.ts +157 -0
- package/dist/core/telemetry.js +412 -0
- package/dist/core/terminal-ux.d.ts +134 -0
- package/dist/core/terminal-ux.js +649 -0
- package/dist/core/testing.d.ts +146 -0
- package/dist/core/testing.js +1200 -0
- package/dist/core/types.d.ts +439 -0
- package/dist/core/types.js +6 -0
- package/dist/core/undo-redo.d.ts +112 -0
- package/dist/core/undo-redo.js +290 -0
- package/dist/core/updater.d.ts +159 -0
- package/dist/core/updater.js +608 -0
- package/dist/core/vim-mode.d.ts +151 -0
- package/dist/core/vim-mode.js +771 -0
- package/dist/core/voice.d.ts +137 -0
- package/dist/core/voice.js +538 -0
- package/dist/core/web-dashboard.d.ts +109 -0
- package/dist/core/web-dashboard.js +484 -0
- package/dist/hooks/hooks.d.ts +74 -0
- package/dist/hooks/hooks.js +160 -0
- package/dist/hooks/llm-evaluator.d.ts +165 -0
- package/dist/hooks/llm-evaluator.js +560 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1186 -0
- package/dist/lsp/lsp-manager.d.ts +63 -0
- package/dist/lsp/lsp-manager.js +351 -0
- package/dist/mcp/client.d.ts +133 -0
- package/dist/mcp/client.js +684 -0
- package/dist/mcp/mcp-apps.d.ts +70 -0
- package/dist/mcp/mcp-apps.js +1007 -0
- package/dist/tools/bash.d.ts +5 -0
- package/dist/tools/bash.js +195 -0
- package/dist/tools/browser.d.ts +92 -0
- package/dist/tools/browser.js +1570 -0
- package/dist/tools/extended.d.ts +6 -0
- package/dist/tools/extended.js +191 -0
- package/dist/tools/file.d.ts +10 -0
- package/dist/tools/file.js +382 -0
- package/dist/tools/github.d.ts +389 -0
- package/dist/tools/github.js +759 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.js +40 -0
- package/dist/tools/memory.d.ts +6 -0
- package/dist/tools/memory.js +197 -0
- package/dist/tools/registry.d.ts +29 -0
- package/dist/tools/registry.js +64 -0
- package/dist/tools/web.d.ts +6 -0
- package/dist/tools/web.js +150 -0
- package/dist/ui/renderer.d.ts +97 -0
- package/dist/ui/renderer.js +279 -0
- package/dist/ui/theme.d.ts +27 -0
- package/dist/ui/theme.js +106 -0
- package/package.json +83 -0
|
@@ -0,0 +1,1443 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// NeuroCLI - CI/CD Pipeline Integration
|
|
3
|
+
// Detect, run, and manage CI/CD pipelines across platforms
|
|
4
|
+
// ============================================================
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, } from 'fs';
|
|
7
|
+
import { join, basename } from 'path';
|
|
8
|
+
// ---- Config file paths ----
|
|
9
|
+
const GITHUB_WORKFLOWS_DIR = '.github/workflows';
|
|
10
|
+
const GITLAB_CI_FILE = '.gitlab-ci.yml';
|
|
11
|
+
const JENKINS_FILE = 'Jenkinsfile';
|
|
12
|
+
const CIRCLECI_DIR = '.circleci';
|
|
13
|
+
const CIRCLECI_CONFIG_FILE = '.circleci/config.yml';
|
|
14
|
+
function execCommand(command, options) {
|
|
15
|
+
const timeout = options?.timeout ?? 60000;
|
|
16
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
17
|
+
try {
|
|
18
|
+
const stdout = execSync(command, {
|
|
19
|
+
encoding: 'utf-8',
|
|
20
|
+
cwd,
|
|
21
|
+
timeout,
|
|
22
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
23
|
+
env: { ...process.env, GH_PROMPT_DISABLED: '1', NO_COLOR: '1' },
|
|
24
|
+
});
|
|
25
|
+
return { stdout: stdout.trim(), stderr: '', exitCode: 0 };
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
return {
|
|
29
|
+
stdout: error.stdout?.toString().trim() ?? '',
|
|
30
|
+
stderr: error.stderr?.toString().trim() ?? '',
|
|
31
|
+
exitCode: error.status ?? 1,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function parseJSON(text) {
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(text);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function isGhAvailable() {
|
|
44
|
+
return execCommand('gh --version').exitCode === 0;
|
|
45
|
+
}
|
|
46
|
+
// ---- CICDIntegration Class ----
|
|
47
|
+
export class CICDIntegration {
|
|
48
|
+
config = null;
|
|
49
|
+
projectRoot;
|
|
50
|
+
ghAvailable = null;
|
|
51
|
+
constructor(projectRoot) {
|
|
52
|
+
this.projectRoot = projectRoot ?? process.cwd();
|
|
53
|
+
}
|
|
54
|
+
ensureGh() {
|
|
55
|
+
if (this.ghAvailable === null) {
|
|
56
|
+
this.ghAvailable = isGhAvailable();
|
|
57
|
+
}
|
|
58
|
+
return this.ghAvailable;
|
|
59
|
+
}
|
|
60
|
+
gh(args, timeout) {
|
|
61
|
+
return execCommand(`gh ${args}`, { cwd: this.projectRoot, timeout });
|
|
62
|
+
}
|
|
63
|
+
git(args) {
|
|
64
|
+
return execCommand(`git ${args}`, { cwd: this.projectRoot });
|
|
65
|
+
}
|
|
66
|
+
resolvePath(relativePath) {
|
|
67
|
+
return join(this.projectRoot, relativePath);
|
|
68
|
+
}
|
|
69
|
+
getRepoSlug() {
|
|
70
|
+
const result = this.git('remote get-url origin');
|
|
71
|
+
if (result.exitCode !== 0 || !result.stdout)
|
|
72
|
+
return null;
|
|
73
|
+
const match = result.stdout.match(/github\.com[/:]([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
74
|
+
return match ? match[1] : null;
|
|
75
|
+
}
|
|
76
|
+
getDefaultBranch() {
|
|
77
|
+
const result = this.git('rev-parse --abbrev-ref HEAD');
|
|
78
|
+
return result.exitCode === 0 ? result.stdout : 'main';
|
|
79
|
+
}
|
|
80
|
+
// ---- Detection ----
|
|
81
|
+
/**
|
|
82
|
+
* Detect which CI/CD system(s) are configured in the project
|
|
83
|
+
*/
|
|
84
|
+
detectPipeline(projectRoot) {
|
|
85
|
+
const root = projectRoot ?? this.projectRoot;
|
|
86
|
+
const detected = [];
|
|
87
|
+
let primarySystem = 'unknown';
|
|
88
|
+
let primaryConfigFile = '';
|
|
89
|
+
// Check GitHub Actions
|
|
90
|
+
const workflowsDir = join(root, GITHUB_WORKFLOWS_DIR);
|
|
91
|
+
if (existsSync(workflowsDir)) {
|
|
92
|
+
try {
|
|
93
|
+
const files = readdirSync(workflowsDir).filter(f => f.endsWith('.yml') || f.endsWith('.yaml'));
|
|
94
|
+
if (files.length > 0) {
|
|
95
|
+
detected.push('github-actions');
|
|
96
|
+
if (primarySystem === 'unknown') {
|
|
97
|
+
primarySystem = 'github-actions';
|
|
98
|
+
primaryConfigFile = join(GITHUB_WORKFLOWS_DIR, files[0]);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Ignore permission errors
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Check GitLab CI
|
|
107
|
+
if (existsSync(join(root, GITLAB_CI_FILE))) {
|
|
108
|
+
detected.push('gitlab-ci');
|
|
109
|
+
if (primarySystem === 'unknown') {
|
|
110
|
+
primarySystem = 'gitlab-ci';
|
|
111
|
+
primaryConfigFile = GITLAB_CI_FILE;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Check Jenkins
|
|
115
|
+
if (existsSync(join(root, JENKINS_FILE))) {
|
|
116
|
+
detected.push('jenkins');
|
|
117
|
+
if (primarySystem === 'unknown') {
|
|
118
|
+
primarySystem = 'jenkins';
|
|
119
|
+
primaryConfigFile = JENKINS_FILE;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Check CircleCI
|
|
123
|
+
if (existsSync(join(root, CIRCLECI_CONFIG_FILE))) {
|
|
124
|
+
detected.push('circleci');
|
|
125
|
+
if (primarySystem === 'unknown') {
|
|
126
|
+
primarySystem = 'circleci';
|
|
127
|
+
primaryConfigFile = CIRCLECI_CONFIG_FILE;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Cache config
|
|
131
|
+
if (primarySystem !== 'unknown') {
|
|
132
|
+
this.config = {
|
|
133
|
+
system: primarySystem,
|
|
134
|
+
projectRoot: root,
|
|
135
|
+
configFile: primaryConfigFile,
|
|
136
|
+
projectSlug: this.getRepoSlug() ?? undefined,
|
|
137
|
+
defaultBranch: this.getDefaultBranch(),
|
|
138
|
+
remoteUrl: this.git('remote get-url origin').exitCode === 0
|
|
139
|
+
? this.git('remote get-url origin').stdout
|
|
140
|
+
: undefined,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
system: primarySystem,
|
|
145
|
+
configFile: primaryConfigFile,
|
|
146
|
+
configExists: primarySystem !== 'unknown',
|
|
147
|
+
multipleSystems: detected,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Get or initialize the CI/CD config
|
|
152
|
+
*/
|
|
153
|
+
getConfig() {
|
|
154
|
+
if (!this.config) {
|
|
155
|
+
const detection = this.detectPipeline();
|
|
156
|
+
if (!this.config) {
|
|
157
|
+
this.config = {
|
|
158
|
+
system: detection.system,
|
|
159
|
+
projectRoot: this.projectRoot,
|
|
160
|
+
configFile: detection.configFile,
|
|
161
|
+
projectSlug: this.getRepoSlug() ?? undefined,
|
|
162
|
+
defaultBranch: this.getDefaultBranch(),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return this.config;
|
|
167
|
+
}
|
|
168
|
+
// ---- Pipeline Execution ----
|
|
169
|
+
/**
|
|
170
|
+
* Trigger a pipeline run
|
|
171
|
+
*/
|
|
172
|
+
runPipeline(pipeline, options) {
|
|
173
|
+
const config = this.getConfig();
|
|
174
|
+
const opts = options ?? {};
|
|
175
|
+
switch (config.system) {
|
|
176
|
+
case 'github-actions':
|
|
177
|
+
return this.runGitHubActions(pipeline, opts);
|
|
178
|
+
case 'gitlab-ci':
|
|
179
|
+
return this.runGitLabCI(pipeline, opts);
|
|
180
|
+
case 'jenkins':
|
|
181
|
+
return this.runJenkins(pipeline, opts);
|
|
182
|
+
case 'circleci':
|
|
183
|
+
return this.runCircleCI(pipeline, opts);
|
|
184
|
+
default:
|
|
185
|
+
return { success: false, error: 'No CI/CD system detected. Run detectPipeline() first.' };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Get pipeline status
|
|
190
|
+
*/
|
|
191
|
+
getPipelineStatus(runId) {
|
|
192
|
+
const config = this.getConfig();
|
|
193
|
+
switch (config.system) {
|
|
194
|
+
case 'github-actions':
|
|
195
|
+
return this.getGitHubActionsStatus(runId);
|
|
196
|
+
case 'gitlab-ci':
|
|
197
|
+
return this.getGitLabCIStatus(runId);
|
|
198
|
+
case 'jenkins':
|
|
199
|
+
return this.getJenkinsStatus(runId);
|
|
200
|
+
case 'circleci':
|
|
201
|
+
return this.getCircleCIStatus(runId);
|
|
202
|
+
default:
|
|
203
|
+
return { success: false, error: 'No CI/CD system detected.' };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Get pipeline logs
|
|
208
|
+
*/
|
|
209
|
+
getPipelineLogs(runId) {
|
|
210
|
+
const config = this.getConfig();
|
|
211
|
+
switch (config.system) {
|
|
212
|
+
case 'github-actions':
|
|
213
|
+
return this.getGitHubActionsLogs(runId);
|
|
214
|
+
case 'gitlab-ci':
|
|
215
|
+
return this.getGitLabCILogs(runId);
|
|
216
|
+
case 'jenkins':
|
|
217
|
+
return this.getJenkinsLogs(runId);
|
|
218
|
+
case 'circleci':
|
|
219
|
+
return this.getCircleCILogs(runId);
|
|
220
|
+
default:
|
|
221
|
+
return { success: false, error: 'No CI/CD system detected.' };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* List available pipelines
|
|
226
|
+
*/
|
|
227
|
+
listPipelines() {
|
|
228
|
+
const config = this.getConfig();
|
|
229
|
+
switch (config.system) {
|
|
230
|
+
case 'github-actions':
|
|
231
|
+
return this.listGitHubActionsPipelines();
|
|
232
|
+
case 'gitlab-ci':
|
|
233
|
+
return this.listGitLabCIPipelines();
|
|
234
|
+
case 'jenkins':
|
|
235
|
+
return this.listJenkinsPipelines();
|
|
236
|
+
case 'circleci':
|
|
237
|
+
return this.listCircleCIPipelines();
|
|
238
|
+
default:
|
|
239
|
+
return { success: false, error: 'No CI/CD system detected.' };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Generate a CI/CD config file
|
|
244
|
+
*/
|
|
245
|
+
createPipelineConfig(type, options) {
|
|
246
|
+
switch (type) {
|
|
247
|
+
case 'github-actions':
|
|
248
|
+
return this.createGitHubActionsConfig(options);
|
|
249
|
+
case 'gitlab-ci':
|
|
250
|
+
return this.createGitLabCIConfig(options);
|
|
251
|
+
case 'jenkins':
|
|
252
|
+
return this.createJenkinsConfig(options);
|
|
253
|
+
case 'circleci':
|
|
254
|
+
return this.createCircleCIConfig(options);
|
|
255
|
+
default:
|
|
256
|
+
return { success: false, error: `Unsupported CI/CD system: ${type}` };
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Validate current CI/CD config
|
|
261
|
+
*/
|
|
262
|
+
validateConfig() {
|
|
263
|
+
const config = this.getConfig();
|
|
264
|
+
switch (config.system) {
|
|
265
|
+
case 'github-actions':
|
|
266
|
+
return this.validateGitHubActionsConfig();
|
|
267
|
+
case 'gitlab-ci':
|
|
268
|
+
return this.validateGitLabCIConfig();
|
|
269
|
+
case 'jenkins':
|
|
270
|
+
return this.validateJenkinsConfig();
|
|
271
|
+
case 'circleci':
|
|
272
|
+
return this.validateCircleCIConfig();
|
|
273
|
+
default:
|
|
274
|
+
return { success: false, error: 'No CI/CD system detected.' };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Get recent pipeline runs
|
|
279
|
+
*/
|
|
280
|
+
getPipelineHistory(limit) {
|
|
281
|
+
const config = this.getConfig();
|
|
282
|
+
const maxResults = limit ?? 10;
|
|
283
|
+
switch (config.system) {
|
|
284
|
+
case 'github-actions':
|
|
285
|
+
return this.getGitHubActionsHistory(maxResults);
|
|
286
|
+
case 'gitlab-ci':
|
|
287
|
+
return this.getGitLabCIHistory(maxResults);
|
|
288
|
+
case 'jenkins':
|
|
289
|
+
return this.getJenkinsHistory(maxResults);
|
|
290
|
+
case 'circleci':
|
|
291
|
+
return this.getCircleCIHistory(maxResults);
|
|
292
|
+
default:
|
|
293
|
+
return { success: false, error: 'No CI/CD system detected.' };
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Cancel a running pipeline
|
|
298
|
+
*/
|
|
299
|
+
cancelPipeline(runId) {
|
|
300
|
+
const config = this.getConfig();
|
|
301
|
+
switch (config.system) {
|
|
302
|
+
case 'github-actions':
|
|
303
|
+
return this.cancelGitHubActionsRun(runId);
|
|
304
|
+
case 'gitlab-ci':
|
|
305
|
+
return this.cancelGitLabCIRun(runId);
|
|
306
|
+
case 'jenkins':
|
|
307
|
+
return this.cancelJenkinsRun(runId);
|
|
308
|
+
case 'circleci':
|
|
309
|
+
return this.cancelCircleCIRun(runId);
|
|
310
|
+
default:
|
|
311
|
+
return { success: false, error: 'No CI/CD system detected.' };
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Re-run a pipeline
|
|
316
|
+
*/
|
|
317
|
+
rerunPipeline(runId) {
|
|
318
|
+
const config = this.getConfig();
|
|
319
|
+
switch (config.system) {
|
|
320
|
+
case 'github-actions':
|
|
321
|
+
return this.rerunGitHubActions(runId);
|
|
322
|
+
case 'gitlab-ci':
|
|
323
|
+
return this.rerunGitLabCI(runId);
|
|
324
|
+
case 'jenkins':
|
|
325
|
+
return this.rerunJenkins(runId);
|
|
326
|
+
case 'circleci':
|
|
327
|
+
return this.rerunCircleCI(runId);
|
|
328
|
+
default:
|
|
329
|
+
return { success: false, error: 'No CI/CD system detected.' };
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Watch a pipeline in real-time
|
|
334
|
+
*/
|
|
335
|
+
watchPipeline(runId, callback, options) {
|
|
336
|
+
const config = this.getConfig();
|
|
337
|
+
const interval = options?.interval ?? 10000;
|
|
338
|
+
const timeoutMs = options?.timeout ?? 600000; // 10 minutes default
|
|
339
|
+
let timer = null;
|
|
340
|
+
let timeoutTimer = null;
|
|
341
|
+
let lastStatus = 'unknown';
|
|
342
|
+
const startTime = Date.now();
|
|
343
|
+
const stop = () => {
|
|
344
|
+
if (timer) {
|
|
345
|
+
clearInterval(timer);
|
|
346
|
+
timer = null;
|
|
347
|
+
}
|
|
348
|
+
if (timeoutTimer) {
|
|
349
|
+
clearTimeout(timeoutTimer);
|
|
350
|
+
timer = null;
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
const poll = () => {
|
|
354
|
+
const statusResult = this.getPipelineStatus(runId);
|
|
355
|
+
if (!statusResult.success || !statusResult.run) {
|
|
356
|
+
callback({
|
|
357
|
+
runId,
|
|
358
|
+
status: 'unknown',
|
|
359
|
+
timestamp: Date.now(),
|
|
360
|
+
message: statusResult.error ?? 'Failed to get status',
|
|
361
|
+
});
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const run = statusResult.run;
|
|
365
|
+
const currentStatus = run.status;
|
|
366
|
+
if (currentStatus !== lastStatus) {
|
|
367
|
+
lastStatus = currentStatus;
|
|
368
|
+
callback({
|
|
369
|
+
runId,
|
|
370
|
+
status: currentStatus,
|
|
371
|
+
timestamp: Date.now(),
|
|
372
|
+
message: `Status changed to: ${currentStatus}`,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
// Check if pipeline is complete
|
|
376
|
+
if (['success', 'failure', 'cancelled', 'skipped'].includes(currentStatus)) {
|
|
377
|
+
callback({
|
|
378
|
+
runId,
|
|
379
|
+
status: currentStatus,
|
|
380
|
+
timestamp: Date.now(),
|
|
381
|
+
message: `Pipeline completed with status: ${currentStatus}`,
|
|
382
|
+
});
|
|
383
|
+
stop();
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
// Start polling
|
|
387
|
+
poll(); // Initial check
|
|
388
|
+
timer = setInterval(poll, interval);
|
|
389
|
+
// Set timeout
|
|
390
|
+
timeoutTimer = setTimeout(() => {
|
|
391
|
+
callback({
|
|
392
|
+
runId,
|
|
393
|
+
status: lastStatus,
|
|
394
|
+
timestamp: Date.now(),
|
|
395
|
+
message: 'Watch timed out',
|
|
396
|
+
});
|
|
397
|
+
stop();
|
|
398
|
+
}, timeoutMs);
|
|
399
|
+
return { success: true, stop };
|
|
400
|
+
}
|
|
401
|
+
// ================================================================
|
|
402
|
+
// GitHub Actions Implementation
|
|
403
|
+
// ================================================================
|
|
404
|
+
runGitHubActions(workflow, options) {
|
|
405
|
+
if (!this.ensureGh()) {
|
|
406
|
+
return { success: false, error: 'GitHub CLI (`gh`) is not installed or not authenticated' };
|
|
407
|
+
}
|
|
408
|
+
const slug = this.getRepoSlug();
|
|
409
|
+
const repoFlag = slug ? `--repo ${slug}` : '';
|
|
410
|
+
const ref = options?.ref ?? this.getDefaultBranch();
|
|
411
|
+
if (workflow) {
|
|
412
|
+
const refFlag = ref ? `--ref "${ref}"` : '';
|
|
413
|
+
let paramFlags = '';
|
|
414
|
+
if (options?.parameters) {
|
|
415
|
+
for (const [key, value] of Object.entries(options.parameters)) {
|
|
416
|
+
paramFlags += ` -f ${key}="${value}"`;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const result = this.gh(`workflow run "${workflow}" ${refFlag}${paramFlags} ${repoFlag}`.trim());
|
|
420
|
+
if (result.exitCode !== 0) {
|
|
421
|
+
return { success: false, error: result.stderr || 'Failed to trigger workflow' };
|
|
422
|
+
}
|
|
423
|
+
// gh workflow run doesn't return a run ID directly; fetch the latest
|
|
424
|
+
return this.getLatestGitHubActionsRun(workflow);
|
|
425
|
+
}
|
|
426
|
+
// No specific workflow - try to trigger all on-push workflows by pushing a dummy commit or listing
|
|
427
|
+
return { success: false, error: 'Specify a workflow name or ID to trigger GitHub Actions' };
|
|
428
|
+
}
|
|
429
|
+
getLatestGitHubActionsRun(workflowName) {
|
|
430
|
+
const slug = this.getRepoSlug();
|
|
431
|
+
const repoFlag = slug ? `--repo ${slug}` : '';
|
|
432
|
+
const workflowFilter = workflowName ? `--workflow="${workflowName}"` : '';
|
|
433
|
+
const result = this.gh(`run list ${workflowFilter} --limit 1 --json databaseId,name,status,conclusion,headBranch,headSha,htmlUrl,createdAt,updatedAt,event ${repoFlag}`.trim());
|
|
434
|
+
if (result.exitCode !== 0) {
|
|
435
|
+
return { success: false, error: result.stderr || 'Failed to fetch latest run' };
|
|
436
|
+
}
|
|
437
|
+
const runs = parseJSON(result.stdout);
|
|
438
|
+
if (!runs || runs.length === 0) {
|
|
439
|
+
return { success: false, error: 'No workflow runs found' };
|
|
440
|
+
}
|
|
441
|
+
return {
|
|
442
|
+
success: true,
|
|
443
|
+
run: this.mapGitHubActionsRun(runs[0]),
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
getGitHubActionsStatus(runId) {
|
|
447
|
+
if (!this.ensureGh()) {
|
|
448
|
+
return { success: false, error: 'GitHub CLI (`gh`) is not installed or not authenticated' };
|
|
449
|
+
}
|
|
450
|
+
const slug = this.getRepoSlug();
|
|
451
|
+
const repoFlag = slug ? `--repo ${slug}` : '';
|
|
452
|
+
if (runId) {
|
|
453
|
+
const result = this.gh(`run view ${runId} --json databaseId,name,status,conclusion,headBranch,headSha,htmlUrl,createdAt,updatedAt,event ${repoFlag}`.trim());
|
|
454
|
+
if (result.exitCode !== 0) {
|
|
455
|
+
return { success: false, error: result.stderr || 'Failed to get run status' };
|
|
456
|
+
}
|
|
457
|
+
const run = parseJSON(result.stdout);
|
|
458
|
+
if (!run)
|
|
459
|
+
return { success: false, error: 'Failed to parse run status' };
|
|
460
|
+
return { success: true, run: this.mapGitHubActionsRun(run) };
|
|
461
|
+
}
|
|
462
|
+
// List recent runs
|
|
463
|
+
const result = this.gh(`run list --limit 10 --json databaseId,name,status,conclusion,headBranch,headSha,htmlUrl,createdAt,updatedAt,event ${repoFlag}`.trim());
|
|
464
|
+
if (result.exitCode !== 0) {
|
|
465
|
+
return { success: false, error: result.stderr || 'Failed to list runs' };
|
|
466
|
+
}
|
|
467
|
+
const runs = parseJSON(result.stdout);
|
|
468
|
+
if (!runs)
|
|
469
|
+
return { success: false, error: 'Failed to parse runs' };
|
|
470
|
+
return { success: true, runs: runs.map(r => this.mapGitHubActionsRun(r)) };
|
|
471
|
+
}
|
|
472
|
+
getGitHubActionsLogs(runId) {
|
|
473
|
+
if (!this.ensureGh()) {
|
|
474
|
+
return { success: false, error: 'GitHub CLI (`gh`) is not installed or not authenticated' };
|
|
475
|
+
}
|
|
476
|
+
// Get jobs for this run
|
|
477
|
+
const slug = this.getRepoSlug();
|
|
478
|
+
const repoFlag = slug ? `--repo ${slug}` : '';
|
|
479
|
+
const runResult = this.gh(`run view ${runId} --json jobs ${repoFlag}`.trim());
|
|
480
|
+
if (runResult.exitCode !== 0) {
|
|
481
|
+
return { success: false, error: runResult.stderr || 'Failed to get run details' };
|
|
482
|
+
}
|
|
483
|
+
const runData = parseJSON(runResult.stdout);
|
|
484
|
+
if (!runData)
|
|
485
|
+
return { success: false, error: 'Failed to parse run data' };
|
|
486
|
+
const jobs = [];
|
|
487
|
+
for (const job of runData.jobs ?? []) {
|
|
488
|
+
// Get log for each job
|
|
489
|
+
const logResult = this.gh(`run view ${runId} --job=${job.databaseId ?? job.id} --log ${repoFlag}`.trim());
|
|
490
|
+
jobs.push({
|
|
491
|
+
name: job.name ?? '',
|
|
492
|
+
status: this.mapGitHubStatus(job.status, job.conclusion),
|
|
493
|
+
log: logResult.exitCode === 0 ? logResult.stdout : `Failed to retrieve logs: ${logResult.stderr}`,
|
|
494
|
+
startedAt: job.startedAt,
|
|
495
|
+
completedAt: job.completedAt,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
return {
|
|
499
|
+
success: true,
|
|
500
|
+
logs: {
|
|
501
|
+
runId,
|
|
502
|
+
system: 'github-actions',
|
|
503
|
+
jobs,
|
|
504
|
+
},
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
listGitHubActionsPipelines() {
|
|
508
|
+
const workflowsDir = this.resolvePath(GITHUB_WORKFLOWS_DIR);
|
|
509
|
+
const pipelines = [];
|
|
510
|
+
if (!existsSync(workflowsDir)) {
|
|
511
|
+
return { success: false, error: 'No .github/workflows directory found' };
|
|
512
|
+
}
|
|
513
|
+
try {
|
|
514
|
+
const files = readdirSync(workflowsDir).filter(f => f.endsWith('.yml') || f.endsWith('.yaml'));
|
|
515
|
+
for (const file of files) {
|
|
516
|
+
const filePath = join(workflowsDir, file);
|
|
517
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
518
|
+
const triggers = this.extractYAMLTriggers(content);
|
|
519
|
+
const name = this.extractYAMLWorkflowName(content) ?? basename(file, '.yml').replace(/\.yaml$/, '');
|
|
520
|
+
pipelines.push({
|
|
521
|
+
id: file,
|
|
522
|
+
name,
|
|
523
|
+
system: 'github-actions',
|
|
524
|
+
configFile: join(GITHUB_WORKFLOWS_DIR, file),
|
|
525
|
+
triggers,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
catch (error) {
|
|
530
|
+
return { success: false, error: `Failed to read workflows: ${error instanceof Error ? error.message : String(error)}` };
|
|
531
|
+
}
|
|
532
|
+
return { success: true, pipelines };
|
|
533
|
+
}
|
|
534
|
+
getGitHubActionsHistory(limit) {
|
|
535
|
+
if (!this.ensureGh()) {
|
|
536
|
+
return { success: false, error: 'GitHub CLI (`gh`) is not installed or not authenticated' };
|
|
537
|
+
}
|
|
538
|
+
const slug = this.getRepoSlug();
|
|
539
|
+
const repoFlag = slug ? `--repo ${slug}` : '';
|
|
540
|
+
const result = this.gh(`run list --limit ${limit} --json databaseId,name,status,conclusion,headBranch,headSha,htmlUrl,createdAt,updatedAt,event ${repoFlag}`.trim());
|
|
541
|
+
if (result.exitCode !== 0) {
|
|
542
|
+
return { success: false, error: result.stderr || 'Failed to get run history' };
|
|
543
|
+
}
|
|
544
|
+
const runs = parseJSON(result.stdout);
|
|
545
|
+
if (!runs)
|
|
546
|
+
return { success: false, error: 'Failed to parse run history' };
|
|
547
|
+
return { success: true, runs: runs.map(r => this.mapGitHubActionsRun(r)) };
|
|
548
|
+
}
|
|
549
|
+
cancelGitHubActionsRun(runId) {
|
|
550
|
+
if (!this.ensureGh()) {
|
|
551
|
+
return { success: false, error: 'GitHub CLI (`gh`) is not installed or not authenticated' };
|
|
552
|
+
}
|
|
553
|
+
const slug = this.getRepoSlug();
|
|
554
|
+
const repoFlag = slug ? `--repo ${slug}` : '';
|
|
555
|
+
const result = this.gh(`run cancel ${runId} ${repoFlag}`.trim());
|
|
556
|
+
if (result.exitCode !== 0) {
|
|
557
|
+
return { success: false, error: result.stderr || 'Failed to cancel run' };
|
|
558
|
+
}
|
|
559
|
+
return { success: true };
|
|
560
|
+
}
|
|
561
|
+
rerunGitHubActions(runId) {
|
|
562
|
+
if (!this.ensureGh()) {
|
|
563
|
+
return { success: false, error: 'GitHub CLI (`gh`) is not installed or not authenticated' };
|
|
564
|
+
}
|
|
565
|
+
const slug = this.getRepoSlug();
|
|
566
|
+
const repoFlag = slug ? `--repo ${slug}` : '';
|
|
567
|
+
const result = this.gh(`run rerun ${runId} ${repoFlag}`.trim());
|
|
568
|
+
if (result.exitCode !== 0) {
|
|
569
|
+
return { success: false, error: result.stderr || 'Failed to rerun' };
|
|
570
|
+
}
|
|
571
|
+
// Get the new run (most recent)
|
|
572
|
+
const latestResult = this.gh(`run list --limit 1 --json databaseId ${repoFlag}`.trim());
|
|
573
|
+
if (latestResult.exitCode === 0) {
|
|
574
|
+
const runs = parseJSON(latestResult.stdout);
|
|
575
|
+
if (runs && runs.length > 0) {
|
|
576
|
+
return { success: true, newRunId: String(runs[0].databaseId) };
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return { success: true };
|
|
580
|
+
}
|
|
581
|
+
// ================================================================
|
|
582
|
+
// GitLab CI Implementation
|
|
583
|
+
// ================================================================
|
|
584
|
+
runGitLabCI(pipeline, options) {
|
|
585
|
+
// GitLab CI pipelines are triggered via API; require `glab` CLI
|
|
586
|
+
const ref = options?.ref ?? this.getDefaultBranch();
|
|
587
|
+
const result = execCommand(`glab ci run --ref "${ref}"${pipeline ? ` --pipeline "${pipeline}"` : ''}`, { cwd: this.projectRoot });
|
|
588
|
+
if (result.exitCode !== 0) {
|
|
589
|
+
return { success: false, error: result.stderr || 'Failed to trigger GitLab CI pipeline. Ensure `glab` CLI is installed.' };
|
|
590
|
+
}
|
|
591
|
+
return { success: true };
|
|
592
|
+
}
|
|
593
|
+
getGitLabCIStatus(runId) {
|
|
594
|
+
const result = execCommand(`glab ci status${runId ? ` --pipeline-id ${runId}` : ''}`, { cwd: this.projectRoot });
|
|
595
|
+
if (result.exitCode !== 0) {
|
|
596
|
+
return { success: false, error: result.stderr || 'Failed to get GitLab CI status' };
|
|
597
|
+
}
|
|
598
|
+
return {
|
|
599
|
+
success: true,
|
|
600
|
+
run: {
|
|
601
|
+
id: runId ?? 'latest',
|
|
602
|
+
system: 'gitlab-ci',
|
|
603
|
+
name: 'GitLab CI Pipeline',
|
|
604
|
+
status: 'unknown',
|
|
605
|
+
branch: this.getDefaultBranch(),
|
|
606
|
+
commit: '',
|
|
607
|
+
url: '',
|
|
608
|
+
createdAt: new Date().toISOString(),
|
|
609
|
+
updatedAt: new Date().toISOString(),
|
|
610
|
+
triggeredBy: '',
|
|
611
|
+
},
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
getGitLabCILogs(runId) {
|
|
615
|
+
const result = execCommand(`glab ci trace --pipeline-id ${runId}`, { cwd: this.projectRoot });
|
|
616
|
+
if (result.exitCode !== 0) {
|
|
617
|
+
return { success: false, error: result.stderr || 'Failed to get GitLab CI logs' };
|
|
618
|
+
}
|
|
619
|
+
return {
|
|
620
|
+
success: true,
|
|
621
|
+
logs: {
|
|
622
|
+
runId,
|
|
623
|
+
system: 'gitlab-ci',
|
|
624
|
+
jobs: [{
|
|
625
|
+
name: 'pipeline',
|
|
626
|
+
status: 'unknown',
|
|
627
|
+
log: result.stdout,
|
|
628
|
+
}],
|
|
629
|
+
},
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
listGitLabCIPipelines() {
|
|
633
|
+
const configFile = this.resolvePath(GITLAB_CI_FILE);
|
|
634
|
+
if (!existsSync(configFile)) {
|
|
635
|
+
return { success: false, error: 'No .gitlab-ci.yml found' };
|
|
636
|
+
}
|
|
637
|
+
const content = readFileSync(configFile, 'utf-8');
|
|
638
|
+
const stages = this.extractYAMLStages(content);
|
|
639
|
+
const pipelines = stages.map(stage => ({
|
|
640
|
+
id: stage,
|
|
641
|
+
name: stage,
|
|
642
|
+
system: 'gitlab-ci',
|
|
643
|
+
configFile: GITLAB_CI_FILE,
|
|
644
|
+
triggers: ['push', 'merge_request'],
|
|
645
|
+
}));
|
|
646
|
+
return { success: true, pipelines };
|
|
647
|
+
}
|
|
648
|
+
getGitLabCIHistory(limit) {
|
|
649
|
+
const result = execCommand(`glab ci list --per-page ${limit}`, { cwd: this.projectRoot });
|
|
650
|
+
if (result.exitCode !== 0) {
|
|
651
|
+
return { success: false, error: result.stderr || 'Failed to get GitLab CI history' };
|
|
652
|
+
}
|
|
653
|
+
return { success: true, runs: [] };
|
|
654
|
+
}
|
|
655
|
+
cancelGitLabCIRun(runId) {
|
|
656
|
+
const result = execCommand(`glab ci cancel --pipeline-id ${runId}`, { cwd: this.projectRoot });
|
|
657
|
+
if (result.exitCode !== 0) {
|
|
658
|
+
return { success: false, error: result.stderr || 'Failed to cancel GitLab CI run' };
|
|
659
|
+
}
|
|
660
|
+
return { success: true };
|
|
661
|
+
}
|
|
662
|
+
rerunGitLabCI(runId) {
|
|
663
|
+
const result = execCommand(`glab ci retry --pipeline-id ${runId}`, { cwd: this.projectRoot });
|
|
664
|
+
if (result.exitCode !== 0) {
|
|
665
|
+
return { success: false, error: result.stderr || 'Failed to rerun GitLab CI pipeline' };
|
|
666
|
+
}
|
|
667
|
+
return { success: true };
|
|
668
|
+
}
|
|
669
|
+
// ================================================================
|
|
670
|
+
// Jenkins Implementation
|
|
671
|
+
// ================================================================
|
|
672
|
+
runJenkins(pipeline, options) {
|
|
673
|
+
// Jenkins builds are triggered via API or `jenkins-cli`
|
|
674
|
+
const jobName = pipeline ?? 'main';
|
|
675
|
+
const parameters = options?.parameters ?? {};
|
|
676
|
+
let paramFlags = '';
|
|
677
|
+
for (const [key, value] of Object.entries(parameters)) {
|
|
678
|
+
paramFlags += ` -p ${key}="${value}"`;
|
|
679
|
+
}
|
|
680
|
+
// Try using jenkins-cli.jar if available
|
|
681
|
+
const result = execCommand(`java -jar jenkins-cli.jar build ${jobName}${paramFlags}`, { cwd: this.projectRoot });
|
|
682
|
+
if (result.exitCode !== 0) {
|
|
683
|
+
return {
|
|
684
|
+
success: false,
|
|
685
|
+
error: 'Failed to trigger Jenkins build. Ensure `jenkins-cli.jar` is available and Jenkins is accessible.',
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
return { success: true };
|
|
689
|
+
}
|
|
690
|
+
getJenkinsStatus(runId) {
|
|
691
|
+
// Jenkins status via CLI
|
|
692
|
+
return {
|
|
693
|
+
success: true,
|
|
694
|
+
run: {
|
|
695
|
+
id: runId ?? 'latest',
|
|
696
|
+
system: 'jenkins',
|
|
697
|
+
name: 'Jenkins Build',
|
|
698
|
+
status: 'unknown',
|
|
699
|
+
branch: this.getDefaultBranch(),
|
|
700
|
+
commit: '',
|
|
701
|
+
url: '',
|
|
702
|
+
createdAt: new Date().toISOString(),
|
|
703
|
+
updatedAt: new Date().toISOString(),
|
|
704
|
+
triggeredBy: '',
|
|
705
|
+
},
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
getJenkinsLogs(runId) {
|
|
709
|
+
const result = execCommand(`java -jar jenkins-cli.jar console ${runId}`, { cwd: this.projectRoot });
|
|
710
|
+
if (result.exitCode !== 0) {
|
|
711
|
+
return { success: false, error: result.stderr || 'Failed to get Jenkins console output' };
|
|
712
|
+
}
|
|
713
|
+
return {
|
|
714
|
+
success: true,
|
|
715
|
+
logs: {
|
|
716
|
+
runId,
|
|
717
|
+
system: 'jenkins',
|
|
718
|
+
jobs: [{
|
|
719
|
+
name: 'build',
|
|
720
|
+
status: 'unknown',
|
|
721
|
+
log: result.stdout,
|
|
722
|
+
}],
|
|
723
|
+
},
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
listJenkinsPipelines() {
|
|
727
|
+
const jenkinsfile = this.resolvePath(JENKINS_FILE);
|
|
728
|
+
if (!existsSync(jenkinsfile)) {
|
|
729
|
+
return { success: false, error: 'No Jenkinsfile found' };
|
|
730
|
+
}
|
|
731
|
+
const content = readFileSync(jenkinsfile, 'utf-8');
|
|
732
|
+
const stages = this.extractJenkinsStages(content);
|
|
733
|
+
return {
|
|
734
|
+
success: true,
|
|
735
|
+
pipelines: [{
|
|
736
|
+
id: 'Jenkinsfile',
|
|
737
|
+
name: 'Jenkins Pipeline',
|
|
738
|
+
system: 'jenkins',
|
|
739
|
+
configFile: JENKINS_FILE,
|
|
740
|
+
triggers: stages,
|
|
741
|
+
}],
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
getJenkinsHistory(limit) {
|
|
745
|
+
// Jenkins history via CLI - limited support
|
|
746
|
+
return { success: true, runs: [] };
|
|
747
|
+
}
|
|
748
|
+
cancelJenkinsRun(runId) {
|
|
749
|
+
const result = execCommand(`java -jar jenkins-cli.jar stop ${runId}`, { cwd: this.projectRoot });
|
|
750
|
+
if (result.exitCode !== 0) {
|
|
751
|
+
return { success: false, error: 'Failed to cancel Jenkins build' };
|
|
752
|
+
}
|
|
753
|
+
return { success: true };
|
|
754
|
+
}
|
|
755
|
+
rerunJenkins(runId) {
|
|
756
|
+
const result = execCommand(`java -jar jenkins-cli.jar build ${runId}`, { cwd: this.projectRoot });
|
|
757
|
+
if (result.exitCode !== 0) {
|
|
758
|
+
return { success: false, error: 'Failed to rerun Jenkins build' };
|
|
759
|
+
}
|
|
760
|
+
return { success: true };
|
|
761
|
+
}
|
|
762
|
+
// ================================================================
|
|
763
|
+
// CircleCI Implementation
|
|
764
|
+
// ================================================================
|
|
765
|
+
runCircleCI(pipeline, options) {
|
|
766
|
+
const slug = this.getRepoSlug() ?? '';
|
|
767
|
+
const ref = options?.ref ?? this.getDefaultBranch();
|
|
768
|
+
const slugFlag = slug ? `--repo ${slug}` : '';
|
|
769
|
+
const result = execCommand(`circleci trigger-pipeline --branch "${ref}" ${slugFlag}`, { cwd: this.projectRoot });
|
|
770
|
+
if (result.exitCode !== 0) {
|
|
771
|
+
return {
|
|
772
|
+
success: false,
|
|
773
|
+
error: 'Failed to trigger CircleCI pipeline. Ensure `circleci` CLI is installed and authenticated.',
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
return { success: true };
|
|
777
|
+
}
|
|
778
|
+
getCircleCIStatus(runId) {
|
|
779
|
+
const slug = this.getRepoSlug() ?? '';
|
|
780
|
+
const slugFlag = slug ? `--repo ${slug}` : '';
|
|
781
|
+
const result = execCommand(`circleci pipeline status ${slugFlag}${runId ? ` --pipeline-id ${runId}` : ''}`, { cwd: this.projectRoot });
|
|
782
|
+
if (result.exitCode !== 0) {
|
|
783
|
+
return { success: false, error: result.stderr || 'Failed to get CircleCI status' };
|
|
784
|
+
}
|
|
785
|
+
return {
|
|
786
|
+
success: true,
|
|
787
|
+
run: {
|
|
788
|
+
id: runId ?? 'latest',
|
|
789
|
+
system: 'circleci',
|
|
790
|
+
name: 'CircleCI Pipeline',
|
|
791
|
+
status: 'unknown',
|
|
792
|
+
branch: this.getDefaultBranch(),
|
|
793
|
+
commit: '',
|
|
794
|
+
url: '',
|
|
795
|
+
createdAt: new Date().toISOString(),
|
|
796
|
+
updatedAt: new Date().toISOString(),
|
|
797
|
+
triggeredBy: '',
|
|
798
|
+
},
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
getCircleCILogs(runId) {
|
|
802
|
+
const result = execCommand(`circleci pipeline show --pipeline-id ${runId}`, { cwd: this.projectRoot });
|
|
803
|
+
if (result.exitCode !== 0) {
|
|
804
|
+
return { success: false, error: result.stderr || 'Failed to get CircleCI logs' };
|
|
805
|
+
}
|
|
806
|
+
return {
|
|
807
|
+
success: true,
|
|
808
|
+
logs: {
|
|
809
|
+
runId,
|
|
810
|
+
system: 'circleci',
|
|
811
|
+
jobs: [{
|
|
812
|
+
name: 'pipeline',
|
|
813
|
+
status: 'unknown',
|
|
814
|
+
log: result.stdout,
|
|
815
|
+
}],
|
|
816
|
+
},
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
listCircleCIPipelines() {
|
|
820
|
+
const configPath = this.resolvePath(CIRCLECI_CONFIG_FILE);
|
|
821
|
+
if (!existsSync(configPath)) {
|
|
822
|
+
return { success: false, error: 'No .circleci/config.yml found' };
|
|
823
|
+
}
|
|
824
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
825
|
+
const jobs = this.extractCircleCIJobs(content);
|
|
826
|
+
return {
|
|
827
|
+
success: true,
|
|
828
|
+
pipelines: jobs.map(job => ({
|
|
829
|
+
id: job,
|
|
830
|
+
name: job,
|
|
831
|
+
system: 'circleci',
|
|
832
|
+
configFile: CIRCLECI_CONFIG_FILE,
|
|
833
|
+
triggers: ['push'],
|
|
834
|
+
})),
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
getCircleCIHistory(limit) {
|
|
838
|
+
const slug = this.getRepoSlug() ?? '';
|
|
839
|
+
const slugFlag = slug ? `--repo ${slug}` : '';
|
|
840
|
+
const result = execCommand(`circleci pipeline list ${slugFlag} --limit ${limit}`, { cwd: this.projectRoot });
|
|
841
|
+
if (result.exitCode !== 0) {
|
|
842
|
+
return { success: false, error: result.stderr || 'Failed to get CircleCI history' };
|
|
843
|
+
}
|
|
844
|
+
return { success: true, runs: [] };
|
|
845
|
+
}
|
|
846
|
+
cancelCircleCIRun(runId) {
|
|
847
|
+
const result = execCommand(`circleci pipeline cancel --pipeline-id ${runId}`, { cwd: this.projectRoot });
|
|
848
|
+
if (result.exitCode !== 0) {
|
|
849
|
+
return { success: false, error: result.stderr || 'Failed to cancel CircleCI pipeline' };
|
|
850
|
+
}
|
|
851
|
+
return { success: true };
|
|
852
|
+
}
|
|
853
|
+
rerunCircleCI(runId) {
|
|
854
|
+
const result = execCommand(`circleci pipeline rerun --pipeline-id ${runId}`, { cwd: this.projectRoot });
|
|
855
|
+
if (result.exitCode !== 0) {
|
|
856
|
+
return { success: false, error: result.stderr || 'Failed to rerun CircleCI pipeline' };
|
|
857
|
+
}
|
|
858
|
+
return { success: true };
|
|
859
|
+
}
|
|
860
|
+
// ================================================================
|
|
861
|
+
// Config Generation
|
|
862
|
+
// ================================================================
|
|
863
|
+
createGitHubActionsConfig(options) {
|
|
864
|
+
const lang = options.language.toLowerCase();
|
|
865
|
+
const nodeVersion = options.nodeVersion ?? '20';
|
|
866
|
+
const branches = options.branches ?? ['main'];
|
|
867
|
+
const buildCmd = options.buildCommand ?? '';
|
|
868
|
+
const testCmd = options.testCommand ?? '';
|
|
869
|
+
const lintCmd = options.lintCommand ?? '';
|
|
870
|
+
const deployCmd = options.deployCommand ?? '';
|
|
871
|
+
let steps = '';
|
|
872
|
+
// Setup steps based on language
|
|
873
|
+
if (lang === 'typescript' || lang === 'javascript' || lang === 'node') {
|
|
874
|
+
steps += `
|
|
875
|
+
- name: Setup Node.js
|
|
876
|
+
uses: actions/setup-node@v4
|
|
877
|
+
with:
|
|
878
|
+
node-version: '${nodeVersion}'
|
|
879
|
+
cache: 'npm'
|
|
880
|
+
- run: npm ci`;
|
|
881
|
+
}
|
|
882
|
+
else if (lang === 'python') {
|
|
883
|
+
const pyVersion = options.pythonVersion ?? '3.11';
|
|
884
|
+
steps += `
|
|
885
|
+
- name: Setup Python
|
|
886
|
+
uses: actions/setup-python@v5
|
|
887
|
+
with:
|
|
888
|
+
python-version: '${pyVersion}'
|
|
889
|
+
cache: 'pip'
|
|
890
|
+
- run: pip install -r requirements.txt`;
|
|
891
|
+
}
|
|
892
|
+
else if (lang === 'go') {
|
|
893
|
+
steps += `
|
|
894
|
+
- name: Setup Go
|
|
895
|
+
uses: actions/setup-go@v5
|
|
896
|
+
with:
|
|
897
|
+
go-version: '1.21'`;
|
|
898
|
+
}
|
|
899
|
+
else {
|
|
900
|
+
steps += `
|
|
901
|
+
- name: Install dependencies
|
|
902
|
+
run: |${buildCmd ? `\n ${buildCmd.split(' ')[0]} install` : ' echo "Add your install step here"'}`;
|
|
903
|
+
}
|
|
904
|
+
if (lintCmd) {
|
|
905
|
+
steps += `
|
|
906
|
+
- name: Lint
|
|
907
|
+
run: ${lintCmd}`;
|
|
908
|
+
}
|
|
909
|
+
if (buildCmd) {
|
|
910
|
+
steps += `
|
|
911
|
+
- name: Build
|
|
912
|
+
run: ${buildCmd}`;
|
|
913
|
+
}
|
|
914
|
+
if (testCmd) {
|
|
915
|
+
steps += `
|
|
916
|
+
- name: Test
|
|
917
|
+
run: ${testCmd}`;
|
|
918
|
+
}
|
|
919
|
+
if (deployCmd) {
|
|
920
|
+
steps += `
|
|
921
|
+
- name: Deploy
|
|
922
|
+
run: ${deployCmd}
|
|
923
|
+
if: github.ref == 'refs/heads/${branches[0]}'`;
|
|
924
|
+
}
|
|
925
|
+
// Add custom stages
|
|
926
|
+
if (options.stages) {
|
|
927
|
+
for (const stage of options.stages) {
|
|
928
|
+
steps += `
|
|
929
|
+
- name: ${stage.name}
|
|
930
|
+
run: ${stage.command}${stage.condition ? `\n if: ${stage.condition}` : ''}`;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
const yaml = `name: CI
|
|
934
|
+
|
|
935
|
+
on:
|
|
936
|
+
push:
|
|
937
|
+
branches: [${branches.map(b => `'${b}'`).join(', ')}]
|
|
938
|
+
pull_request:
|
|
939
|
+
branches: [${branches.map(b => `'${b}'`).join(', ')}]
|
|
940
|
+
|
|
941
|
+
jobs:
|
|
942
|
+
build:
|
|
943
|
+
runs-on: ubuntu-latest
|
|
944
|
+
${options.dockerImage ? ` container: ${options.dockerImage}` : ''}
|
|
945
|
+
steps:
|
|
946
|
+
- name: Checkout
|
|
947
|
+
uses: actions/checkout@v4${steps}
|
|
948
|
+
`;
|
|
949
|
+
// Write file
|
|
950
|
+
const workflowsDir = this.resolvePath(GITHUB_WORKFLOWS_DIR);
|
|
951
|
+
if (!existsSync(workflowsDir)) {
|
|
952
|
+
mkdirSync(workflowsDir, { recursive: true });
|
|
953
|
+
}
|
|
954
|
+
const filePath = join(workflowsDir, 'ci.yml');
|
|
955
|
+
writeFileSync(filePath, yaml, 'utf-8');
|
|
956
|
+
return { success: true, path: join(GITHUB_WORKFLOWS_DIR, 'ci.yml') };
|
|
957
|
+
}
|
|
958
|
+
createGitLabCIConfig(options) {
|
|
959
|
+
const lang = options.language.toLowerCase();
|
|
960
|
+
const nodeVersion = options.nodeVersion ?? '20';
|
|
961
|
+
const buildCmd = options.buildCommand ?? '';
|
|
962
|
+
const testCmd = options.testCommand ?? '';
|
|
963
|
+
const lintCmd = options.lintCommand ?? '';
|
|
964
|
+
const branches = options.branches ?? ['main'];
|
|
965
|
+
let imageLine = '';
|
|
966
|
+
if (options.dockerImage) {
|
|
967
|
+
imageLine = `image: ${options.dockerImage}\n\n`;
|
|
968
|
+
}
|
|
969
|
+
else if (lang === 'typescript' || lang === 'javascript' || lang === 'node') {
|
|
970
|
+
imageLine = `image: node:${nodeVersion}\n\n`;
|
|
971
|
+
}
|
|
972
|
+
else if (lang === 'python') {
|
|
973
|
+
imageLine = `image: python:${options.pythonVersion ?? '3.11'}\n\n`;
|
|
974
|
+
}
|
|
975
|
+
let stages = 'stages:\n - lint\n - build\n - test\n';
|
|
976
|
+
let jobDefs = '';
|
|
977
|
+
if (lintCmd) {
|
|
978
|
+
stages += ' - deploy\n';
|
|
979
|
+
jobDefs += `
|
|
980
|
+
lint:
|
|
981
|
+
stage: lint
|
|
982
|
+
script:
|
|
983
|
+
- ${lintCmd}
|
|
984
|
+
rules:
|
|
985
|
+
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
986
|
+
- if: $CI_COMMIT_BRANCH == "${branches[0]}"
|
|
987
|
+
|
|
988
|
+
`;
|
|
989
|
+
}
|
|
990
|
+
if (buildCmd) {
|
|
991
|
+
jobDefs += `build:
|
|
992
|
+
stage: build
|
|
993
|
+
script:
|
|
994
|
+
- ${buildCmd}
|
|
995
|
+
rules:
|
|
996
|
+
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
997
|
+
- if: $CI_COMMIT_BRANCH == "${branches[0]}"
|
|
998
|
+
|
|
999
|
+
`;
|
|
1000
|
+
}
|
|
1001
|
+
if (testCmd) {
|
|
1002
|
+
jobDefs += `test:
|
|
1003
|
+
stage: test
|
|
1004
|
+
script:
|
|
1005
|
+
- ${testCmd}
|
|
1006
|
+
rules:
|
|
1007
|
+
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
1008
|
+
- if: $CI_COMMIT_BRANCH == "${branches[0]}"
|
|
1009
|
+
|
|
1010
|
+
`;
|
|
1011
|
+
}
|
|
1012
|
+
if (options.deployCommand) {
|
|
1013
|
+
jobDefs += `deploy:
|
|
1014
|
+
stage: deploy
|
|
1015
|
+
script:
|
|
1016
|
+
- ${options.deployCommand}
|
|
1017
|
+
rules:
|
|
1018
|
+
- if: $CI_COMMIT_BRANCH == "${branches[0]}"
|
|
1019
|
+
environment: production
|
|
1020
|
+
|
|
1021
|
+
`;
|
|
1022
|
+
}
|
|
1023
|
+
const yaml = `${imageLine}${stages}\n${jobDefs}`;
|
|
1024
|
+
const filePath = this.resolvePath(GITLAB_CI_FILE);
|
|
1025
|
+
writeFileSync(filePath, yaml, 'utf-8');
|
|
1026
|
+
return { success: true, path: GITLAB_CI_FILE };
|
|
1027
|
+
}
|
|
1028
|
+
createJenkinsConfig(options) {
|
|
1029
|
+
const buildCmd = options.buildCommand ?? 'echo "Build step"';
|
|
1030
|
+
const testCmd = options.testCommand ?? 'echo "Test step"';
|
|
1031
|
+
const lintCmd = options.lintCommand ?? '';
|
|
1032
|
+
let stages = '';
|
|
1033
|
+
if (lintCmd) {
|
|
1034
|
+
stages += `
|
|
1035
|
+
stage('Lint') {
|
|
1036
|
+
steps {
|
|
1037
|
+
sh '${lintCmd}'
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
`;
|
|
1041
|
+
}
|
|
1042
|
+
stages += `
|
|
1043
|
+
stage('Build') {
|
|
1044
|
+
steps {
|
|
1045
|
+
sh '${buildCmd}'
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
stage('Test') {
|
|
1049
|
+
steps {
|
|
1050
|
+
sh '${testCmd}'
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
`;
|
|
1054
|
+
if (options.deployCommand) {
|
|
1055
|
+
stages += `
|
|
1056
|
+
stage('Deploy') {
|
|
1057
|
+
when {
|
|
1058
|
+
branch '${options.branches?.[0] ?? 'main'}'
|
|
1059
|
+
}
|
|
1060
|
+
steps {
|
|
1061
|
+
sh '${options.deployCommand}'
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
`;
|
|
1065
|
+
}
|
|
1066
|
+
if (options.stages) {
|
|
1067
|
+
for (const stage of options.stages) {
|
|
1068
|
+
stages += `
|
|
1069
|
+
stage('${stage.name}') {
|
|
1070
|
+
${stage.condition ? ` when {\n expression { ${stage.condition} }\n }\n` : ''} steps {
|
|
1071
|
+
sh '${stage.command}'
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
`;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
const jenkinsfile = `pipeline {
|
|
1078
|
+
agent any
|
|
1079
|
+
|
|
1080
|
+
stages {${stages}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
post {
|
|
1084
|
+
always {
|
|
1085
|
+
cleanWs()
|
|
1086
|
+
}
|
|
1087
|
+
success {
|
|
1088
|
+
echo 'Pipeline succeeded!'
|
|
1089
|
+
}
|
|
1090
|
+
failure {
|
|
1091
|
+
echo 'Pipeline failed!'
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
`;
|
|
1096
|
+
const filePath = this.resolvePath(JENKINS_FILE);
|
|
1097
|
+
writeFileSync(filePath, jenkinsfile, 'utf-8');
|
|
1098
|
+
return { success: true, path: JENKINS_FILE };
|
|
1099
|
+
}
|
|
1100
|
+
createCircleCIConfig(options) {
|
|
1101
|
+
const lang = options.language.toLowerCase();
|
|
1102
|
+
const nodeVersion = options.nodeVersion ?? '20';
|
|
1103
|
+
const buildCmd = options.buildCommand ?? '';
|
|
1104
|
+
const testCmd = options.testCommand ?? '';
|
|
1105
|
+
const lintCmd = options.lintCommand ?? '';
|
|
1106
|
+
let orbs = '';
|
|
1107
|
+
let setupSteps = '';
|
|
1108
|
+
let dockerImage = options.dockerImage ?? 'cimg/base:stable';
|
|
1109
|
+
if (lang === 'typescript' || lang === 'javascript' || lang === 'node') {
|
|
1110
|
+
dockerImage = `cimg/node:${nodeVersion}`;
|
|
1111
|
+
setupSteps = ` - run:\n name: Install dependencies\n command: npm ci\n`;
|
|
1112
|
+
}
|
|
1113
|
+
else if (lang === 'python') {
|
|
1114
|
+
dockerImage = `cimg/python:${options.pythonVersion ?? '3.11'}`;
|
|
1115
|
+
setupSteps = ` - run:\n name: Install dependencies\n command: pip install -r requirements.txt\n`;
|
|
1116
|
+
}
|
|
1117
|
+
let jobSteps = setupSteps;
|
|
1118
|
+
if (lintCmd) {
|
|
1119
|
+
jobSteps += ` - run:\n name: Lint\n command: ${lintCmd}\n`;
|
|
1120
|
+
}
|
|
1121
|
+
if (buildCmd) {
|
|
1122
|
+
jobSteps += ` - run:\n name: Build\n command: ${buildCmd}\n`;
|
|
1123
|
+
}
|
|
1124
|
+
if (testCmd) {
|
|
1125
|
+
jobSteps += ` - run:\n name: Test\n command: ${testCmd}\n`;
|
|
1126
|
+
}
|
|
1127
|
+
if (options.deployCommand) {
|
|
1128
|
+
jobSteps += ` - run:\n name: Deploy\n command: ${options.deployCommand}\n`;
|
|
1129
|
+
}
|
|
1130
|
+
if (options.stages) {
|
|
1131
|
+
for (const stage of options.stages) {
|
|
1132
|
+
jobSteps += ` - run:\n name: ${stage.name}\n command: ${stage.command}\n`;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
const yaml = `version: 2.1
|
|
1136
|
+
|
|
1137
|
+
${orbs}jobs:
|
|
1138
|
+
build-and-test:
|
|
1139
|
+
docker:
|
|
1140
|
+
- image: ${dockerImage}
|
|
1141
|
+
steps:
|
|
1142
|
+
- checkout
|
|
1143
|
+
${jobSteps}
|
|
1144
|
+
workflows:
|
|
1145
|
+
ci:
|
|
1146
|
+
jobs:
|
|
1147
|
+
- build-and-test
|
|
1148
|
+
`;
|
|
1149
|
+
const circleDir = this.resolvePath(CIRCLECI_DIR);
|
|
1150
|
+
if (!existsSync(circleDir)) {
|
|
1151
|
+
mkdirSync(circleDir, { recursive: true });
|
|
1152
|
+
}
|
|
1153
|
+
const filePath = join(circleDir, 'config.yml');
|
|
1154
|
+
writeFileSync(filePath, yaml, 'utf-8');
|
|
1155
|
+
return { success: true, path: join(CIRCLECI_DIR, 'config.yml') };
|
|
1156
|
+
}
|
|
1157
|
+
// ================================================================
|
|
1158
|
+
// Config Validation
|
|
1159
|
+
// ================================================================
|
|
1160
|
+
validateGitHubActionsConfig() {
|
|
1161
|
+
const workflowsDir = this.resolvePath(GITHUB_WORKFLOWS_DIR);
|
|
1162
|
+
const issues = [];
|
|
1163
|
+
if (!existsSync(workflowsDir)) {
|
|
1164
|
+
return {
|
|
1165
|
+
success: true,
|
|
1166
|
+
result: {
|
|
1167
|
+
valid: false,
|
|
1168
|
+
issues: [{ severity: 'error', message: 'No .github/workflows directory found', file: GITHUB_WORKFLOWS_DIR }],
|
|
1169
|
+
system: 'github-actions',
|
|
1170
|
+
},
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
try {
|
|
1174
|
+
const files = readdirSync(workflowsDir).filter(f => f.endsWith('.yml') || f.endsWith('.yaml'));
|
|
1175
|
+
if (files.length === 0) {
|
|
1176
|
+
issues.push({ severity: 'warning', message: 'No workflow files found in .github/workflows', file: GITHUB_WORKFLOWS_DIR });
|
|
1177
|
+
}
|
|
1178
|
+
for (const file of files) {
|
|
1179
|
+
const filePath = join(workflowsDir, file);
|
|
1180
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
1181
|
+
const lines = content.split('\n');
|
|
1182
|
+
// Basic YAML structure checks
|
|
1183
|
+
let hasOn = false;
|
|
1184
|
+
let hasJobs = false;
|
|
1185
|
+
let hasRunsOn = false;
|
|
1186
|
+
let hasCheckout = false;
|
|
1187
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1188
|
+
const line = lines[i];
|
|
1189
|
+
if (/^on\s*:/.test(line.trim()))
|
|
1190
|
+
hasOn = true;
|
|
1191
|
+
if (/^jobs\s*:/.test(line.trim()))
|
|
1192
|
+
hasJobs = true;
|
|
1193
|
+
if (/runs-on\s*:/.test(line))
|
|
1194
|
+
hasRunsOn = true;
|
|
1195
|
+
if (/actions\/checkout/.test(line))
|
|
1196
|
+
hasCheckout = true;
|
|
1197
|
+
}
|
|
1198
|
+
if (!hasOn) {
|
|
1199
|
+
issues.push({ severity: 'error', message: 'Missing "on" trigger definition', file, line: 1 });
|
|
1200
|
+
}
|
|
1201
|
+
if (!hasJobs) {
|
|
1202
|
+
issues.push({ severity: 'error', message: 'Missing "jobs" definition', file, line: 1 });
|
|
1203
|
+
}
|
|
1204
|
+
if (hasJobs && !hasRunsOn) {
|
|
1205
|
+
issues.push({ severity: 'warning', message: 'No "runs-on" found - jobs may not specify runner', file });
|
|
1206
|
+
}
|
|
1207
|
+
if (hasJobs && !hasCheckout) {
|
|
1208
|
+
issues.push({ severity: 'info', message: 'Consider adding "actions/checkout" step', file });
|
|
1209
|
+
}
|
|
1210
|
+
// Check for common YAML issues
|
|
1211
|
+
if (content.includes('\t')) {
|
|
1212
|
+
issues.push({ severity: 'warning', message: 'File contains tab characters (YAML requires spaces)', file });
|
|
1213
|
+
}
|
|
1214
|
+
// Check for trailing spaces on key lines
|
|
1215
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1216
|
+
if (lines[i].endsWith(' ') && lines[i].trim().endsWith(':')) {
|
|
1217
|
+
issues.push({ severity: 'info', message: 'Trailing whitespace after key', file, line: i + 1 });
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
catch (error) {
|
|
1223
|
+
return { success: false, error: `Validation failed: ${error instanceof Error ? error.message : String(error)}` };
|
|
1224
|
+
}
|
|
1225
|
+
const hasErrors = issues.some(i => i.severity === 'error');
|
|
1226
|
+
return {
|
|
1227
|
+
success: true,
|
|
1228
|
+
result: {
|
|
1229
|
+
valid: !hasErrors,
|
|
1230
|
+
issues,
|
|
1231
|
+
system: 'github-actions',
|
|
1232
|
+
},
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
validateGitLabCIConfig() {
|
|
1236
|
+
const configPath = this.resolvePath(GITLAB_CI_FILE);
|
|
1237
|
+
const issues = [];
|
|
1238
|
+
if (!existsSync(configPath)) {
|
|
1239
|
+
return {
|
|
1240
|
+
success: true,
|
|
1241
|
+
result: {
|
|
1242
|
+
valid: false,
|
|
1243
|
+
issues: [{ severity: 'error', message: 'No .gitlab-ci.yml found', file: GITLAB_CI_FILE }],
|
|
1244
|
+
system: 'gitlab-ci',
|
|
1245
|
+
},
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
1249
|
+
if (!content.includes('stages:') && !content.includes('stage:')) {
|
|
1250
|
+
issues.push({ severity: 'warning', message: 'No "stages" definition found', file: GITLAB_CI_FILE });
|
|
1251
|
+
}
|
|
1252
|
+
if (content.includes('\t')) {
|
|
1253
|
+
issues.push({ severity: 'warning', message: 'File contains tab characters', file: GITLAB_CI_FILE });
|
|
1254
|
+
}
|
|
1255
|
+
const hasErrors = issues.some(i => i.severity === 'error');
|
|
1256
|
+
return {
|
|
1257
|
+
success: true,
|
|
1258
|
+
result: { valid: !hasErrors, issues, system: 'gitlab-ci' },
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
validateJenkinsConfig() {
|
|
1262
|
+
const configPath = this.resolvePath(JENKINS_FILE);
|
|
1263
|
+
const issues = [];
|
|
1264
|
+
if (!existsSync(configPath)) {
|
|
1265
|
+
return {
|
|
1266
|
+
success: true,
|
|
1267
|
+
result: {
|
|
1268
|
+
valid: false,
|
|
1269
|
+
issues: [{ severity: 'error', message: 'No Jenkinsfile found', file: JENKINS_FILE }],
|
|
1270
|
+
system: 'jenkins',
|
|
1271
|
+
},
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
1275
|
+
if (!content.includes('pipeline') && !content.includes('node')) {
|
|
1276
|
+
issues.push({ severity: 'error', message: 'No "pipeline" or "node" block found', file: JENKINS_FILE });
|
|
1277
|
+
}
|
|
1278
|
+
if (!content.includes('stage')) {
|
|
1279
|
+
issues.push({ severity: 'warning', message: 'No "stage" definitions found', file: JENKINS_FILE });
|
|
1280
|
+
}
|
|
1281
|
+
if (!content.includes('agent')) {
|
|
1282
|
+
issues.push({ severity: 'info', message: 'No "agent" definition found - will use "any"', file: JENKINS_FILE });
|
|
1283
|
+
}
|
|
1284
|
+
const hasErrors = issues.some(i => i.severity === 'error');
|
|
1285
|
+
return {
|
|
1286
|
+
success: true,
|
|
1287
|
+
result: { valid: !hasErrors, issues, system: 'jenkins' },
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
validateCircleCIConfig() {
|
|
1291
|
+
const configPath = this.resolvePath(CIRCLECI_CONFIG_FILE);
|
|
1292
|
+
const issues = [];
|
|
1293
|
+
if (!existsSync(configPath)) {
|
|
1294
|
+
return {
|
|
1295
|
+
success: true,
|
|
1296
|
+
result: {
|
|
1297
|
+
valid: false,
|
|
1298
|
+
issues: [{ severity: 'error', message: 'No .circleci/config.yml found', file: CIRCLECI_CONFIG_FILE }],
|
|
1299
|
+
system: 'circleci',
|
|
1300
|
+
},
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
1304
|
+
if (!content.includes('version:')) {
|
|
1305
|
+
issues.push({ severity: 'error', message: 'Missing "version" key', file: CIRCLECI_CONFIG_FILE });
|
|
1306
|
+
}
|
|
1307
|
+
if (!content.includes('jobs:')) {
|
|
1308
|
+
issues.push({ severity: 'error', message: 'Missing "jobs" definition', file: CIRCLECI_CONFIG_FILE });
|
|
1309
|
+
}
|
|
1310
|
+
if (!content.includes('workflows:')) {
|
|
1311
|
+
issues.push({ severity: 'error', message: 'Missing "workflows" definition', file: CIRCLECI_CONFIG_FILE });
|
|
1312
|
+
}
|
|
1313
|
+
if (content.includes('\t')) {
|
|
1314
|
+
issues.push({ severity: 'warning', message: 'File contains tab characters', file: CIRCLECI_CONFIG_FILE });
|
|
1315
|
+
}
|
|
1316
|
+
const hasErrors = issues.some(i => i.severity === 'error');
|
|
1317
|
+
return {
|
|
1318
|
+
success: true,
|
|
1319
|
+
result: { valid: !hasErrors, issues, system: 'circleci' },
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
// ================================================================
|
|
1323
|
+
// YAML / Config Parsing Helpers
|
|
1324
|
+
// ================================================================
|
|
1325
|
+
mapGitHubActionsRun(r) {
|
|
1326
|
+
return {
|
|
1327
|
+
id: String(r.databaseId ?? r.id ?? ''),
|
|
1328
|
+
system: 'github-actions',
|
|
1329
|
+
name: r.name ?? '',
|
|
1330
|
+
status: this.mapGitHubStatus(r.status, r.conclusion),
|
|
1331
|
+
branch: r.headBranch ?? '',
|
|
1332
|
+
commit: r.headSha ?? '',
|
|
1333
|
+
url: r.htmlUrl ?? '',
|
|
1334
|
+
createdAt: r.createdAt ?? '',
|
|
1335
|
+
updatedAt: r.updatedAt ?? '',
|
|
1336
|
+
triggeredBy: r.event ?? '',
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
mapGitHubStatus(status, conclusion) {
|
|
1340
|
+
if (conclusion === 'success')
|
|
1341
|
+
return 'success';
|
|
1342
|
+
if (conclusion === 'failure')
|
|
1343
|
+
return 'failure';
|
|
1344
|
+
if (conclusion === 'cancelled')
|
|
1345
|
+
return 'cancelled';
|
|
1346
|
+
if (conclusion === 'skipped')
|
|
1347
|
+
return 'skipped';
|
|
1348
|
+
switch (status) {
|
|
1349
|
+
case 'queued': return 'queued';
|
|
1350
|
+
case 'in_progress':
|
|
1351
|
+
case 'running': return 'in_progress';
|
|
1352
|
+
case 'waiting': return 'waiting';
|
|
1353
|
+
case 'completed':
|
|
1354
|
+
return conclusion ? this.mapGitHubStatus('', conclusion) : 'unknown';
|
|
1355
|
+
default: return 'unknown';
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
extractYAMLTriggers(content) {
|
|
1359
|
+
const triggers = [];
|
|
1360
|
+
const onMatch = content.match(/^on\s*:\s*$/m);
|
|
1361
|
+
if (onMatch) {
|
|
1362
|
+
const afterOn = content.slice(onMatch.index + onMatch[0].length);
|
|
1363
|
+
const triggerBlock = afterOn.split(/^[\w_-]+\s*:/m)[0];
|
|
1364
|
+
const pushMatch = triggerBlock.match(/push\s*:/);
|
|
1365
|
+
const prMatch = triggerBlock.match(/pull_request\s*:/);
|
|
1366
|
+
const scheduleMatch = triggerBlock.match(/schedule\s*:/);
|
|
1367
|
+
const workflowDispatchMatch = triggerBlock.match(/workflow_dispatch\s*:/);
|
|
1368
|
+
if (pushMatch)
|
|
1369
|
+
triggers.push('push');
|
|
1370
|
+
if (prMatch)
|
|
1371
|
+
triggers.push('pull_request');
|
|
1372
|
+
if (scheduleMatch)
|
|
1373
|
+
triggers.push('schedule');
|
|
1374
|
+
if (workflowDispatchMatch)
|
|
1375
|
+
triggers.push('workflow_dispatch');
|
|
1376
|
+
}
|
|
1377
|
+
// Also check for single-line on: [push, pull_request]
|
|
1378
|
+
const inlineMatch = content.match(/^on\s*:\s*\[(.+?)\]/m);
|
|
1379
|
+
if (inlineMatch) {
|
|
1380
|
+
const items = inlineMatch[1].split(',').map(s => s.trim());
|
|
1381
|
+
triggers.push(...items);
|
|
1382
|
+
}
|
|
1383
|
+
// Check for simple on: push
|
|
1384
|
+
const simpleMatch = content.match(/^on\s*:\s*(\w+)\s*$/m);
|
|
1385
|
+
if (simpleMatch && triggers.length === 0) {
|
|
1386
|
+
triggers.push(simpleMatch[1]);
|
|
1387
|
+
}
|
|
1388
|
+
return triggers.length > 0 ? triggers : ['unknown'];
|
|
1389
|
+
}
|
|
1390
|
+
extractYAMLWorkflowName(content) {
|
|
1391
|
+
const match = content.match(/^name\s*:\s*['"]?(.+?)['"]?\s*$/m);
|
|
1392
|
+
return match ? match[1].trim() : null;
|
|
1393
|
+
}
|
|
1394
|
+
extractYAMLStages(content) {
|
|
1395
|
+
const stages = [];
|
|
1396
|
+
const match = content.match(/^stages\s*:\s*$/m);
|
|
1397
|
+
if (match) {
|
|
1398
|
+
const afterStages = content.slice(match.index + match[0].length);
|
|
1399
|
+
const lines = afterStages.split('\n');
|
|
1400
|
+
for (const line of lines) {
|
|
1401
|
+
const stageMatch = line.match(/^\s+-\s*['"]?(.+?)['"]?\s*$/);
|
|
1402
|
+
if (stageMatch) {
|
|
1403
|
+
stages.push(stageMatch[1].trim());
|
|
1404
|
+
}
|
|
1405
|
+
else if (line.trim() && !line.trim().startsWith('#') && !line.match(/^\s+-/)) {
|
|
1406
|
+
break;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
return stages.length > 0 ? stages : ['build', 'test', 'deploy'];
|
|
1411
|
+
}
|
|
1412
|
+
extractJenkinsStages(content) {
|
|
1413
|
+
const stages = [];
|
|
1414
|
+
const stageRegex = /stage\s*\(\s*['"](.+?)['"]\s*\)/g;
|
|
1415
|
+
let match;
|
|
1416
|
+
while ((match = stageRegex.exec(content)) !== null) {
|
|
1417
|
+
stages.push(match[1]);
|
|
1418
|
+
}
|
|
1419
|
+
return stages;
|
|
1420
|
+
}
|
|
1421
|
+
extractCircleCIJobs(content) {
|
|
1422
|
+
const jobs = [];
|
|
1423
|
+
const lines = content.split('\n');
|
|
1424
|
+
let inJobs = false;
|
|
1425
|
+
for (const line of lines) {
|
|
1426
|
+
if (/^jobs\s*:/.test(line.trim())) {
|
|
1427
|
+
inJobs = true;
|
|
1428
|
+
continue;
|
|
1429
|
+
}
|
|
1430
|
+
if (inJobs) {
|
|
1431
|
+
const jobMatch = line.match(/^\s{2}(\w[\w-]*)\s*:/);
|
|
1432
|
+
if (jobMatch) {
|
|
1433
|
+
jobs.push(jobMatch[1]);
|
|
1434
|
+
}
|
|
1435
|
+
else if (line.trim() && !line.trim().startsWith('#') && !line.match(/^\s{4,}/)) {
|
|
1436
|
+
inJobs = false;
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
return jobs.length > 0 ? jobs : ['build'];
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
//# sourceMappingURL=cicd.js.map
|