pipecraft 0.0.0-releaseit
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/.claude/settings.local.json +35 -0
- package/.github/actions/calculate-version/action.yml +106 -0
- package/.github/actions/create-pr/action.yml +122 -0
- package/.github/actions/create-release/action.yml +74 -0
- package/.github/actions/create-tag/action.yml +94 -0
- package/.github/actions/detect-changes/action.yml +62 -0
- package/.github/actions/manage-branch/action.yml +113 -0
- package/.github/actions/promote-branch/action.yml +352 -0
- package/.github/example/workflows/job.analyze.code.yml +26 -0
- package/.github/example/workflows/job.analyze.docker.yml +32 -0
- package/.github/example/workflows/job.app.api.deploy.yml +127 -0
- package/.github/example/workflows/job.app.api.test.yml +102 -0
- package/.github/example/workflows/job.app.docs.deploy.yml +40 -0
- package/.github/example/workflows/job.app.docs.test.yml +50 -0
- package/.github/example/workflows/job.app.web.deploy.yml +96 -0
- package/.github/example/workflows/job.app.web.test.yml +49 -0
- package/.github/example/workflows/job.changes.yml +82 -0
- package/.github/example/workflows/job.create-pr.yml +96 -0
- package/.github/example/workflows/job.env-check.yml +43 -0
- package/.github/example/workflows/job.fast-forward.yml +103 -0
- package/.github/example/workflows/job.lint.yml +64 -0
- package/.github/example/workflows/job.pr-name.yml +70 -0
- package/.github/example/workflows/job.pr-squash-name.yml +27 -0
- package/.github/example/workflows/job.tag.yml +38 -0
- package/.github/example/workflows/job.version.yml +99 -0
- package/.github/example/workflows/lib.plugin-base.publish.yml +59 -0
- package/.github/example/workflows/pipe.yml +178 -0
- package/.github/example/workflows/scripts/fastforward.sh +51 -0
- package/.github/example/workflows/scripts/pipe-cleaner.sh +70 -0
- package/.github/scripts/fastforward.sh +51 -0
- package/.github/scripts/pipe-cleaner.sh +70 -0
- package/.github/workflows/pipeline.yml +220 -0
- package/.github/workflows/pr-title-check.yml +70 -0
- package/.github/workflows/publish.yml +39 -0
- package/.pipecraftrc.json +78 -0
- package/.release-it.cjs +71 -0
- package/PIPELINE_TESTING_PLAN.md +499 -0
- package/README.md +1101 -0
- package/TRUNK_FLOW_PLAN.md +401 -0
- package/assets/logo_banner.png +0 -0
- package/assets/logo_banner.webp +0 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +364 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/generators/init.tpl.d.ts +3 -0
- package/dist/generators/init.tpl.d.ts.map +1 -0
- package/dist/generators/init.tpl.js +117 -0
- package/dist/generators/init.tpl.js.map +1 -0
- package/dist/generators/workflows.tpl.d.ts +7 -0
- package/dist/generators/workflows.tpl.d.ts.map +1 -0
- package/dist/generators/workflows.tpl.js +73 -0
- package/dist/generators/workflows.tpl.js.map +1 -0
- package/dist/templates/actions/calculate-version.yml.tpl.d.ts +3 -0
- package/dist/templates/actions/calculate-version.yml.tpl.d.ts.map +1 -0
- package/dist/templates/actions/calculate-version.yml.tpl.js +122 -0
- package/dist/templates/actions/calculate-version.yml.tpl.js.map +1 -0
- package/dist/templates/actions/create-pr.yml.tpl.d.ts +3 -0
- package/dist/templates/actions/create-pr.yml.tpl.d.ts.map +1 -0
- package/dist/templates/actions/create-pr.yml.tpl.js +145 -0
- package/dist/templates/actions/create-pr.yml.tpl.js.map +1 -0
- package/dist/templates/actions/create-tag.yml.tpl.d.ts +3 -0
- package/dist/templates/actions/create-tag.yml.tpl.d.ts.map +1 -0
- package/dist/templates/actions/create-tag.yml.tpl.js +111 -0
- package/dist/templates/actions/create-tag.yml.tpl.js.map +1 -0
- package/dist/templates/actions/detect-changes.yml.tpl.d.ts +3 -0
- package/dist/templates/actions/detect-changes.yml.tpl.d.ts.map +1 -0
- package/dist/templates/actions/detect-changes.yml.tpl.js +79 -0
- package/dist/templates/actions/detect-changes.yml.tpl.js.map +1 -0
- package/dist/templates/actions/manage-branch.yml.tpl.d.ts +3 -0
- package/dist/templates/actions/manage-branch.yml.tpl.d.ts.map +1 -0
- package/dist/templates/actions/manage-branch.yml.tpl.js +130 -0
- package/dist/templates/actions/manage-branch.yml.tpl.js.map +1 -0
- package/dist/templates/actions/promote-branch.yml.tpl.d.ts +3 -0
- package/dist/templates/actions/promote-branch.yml.tpl.d.ts.map +1 -0
- package/dist/templates/actions/promote-branch.yml.tpl.js +362 -0
- package/dist/templates/actions/promote-branch.yml.tpl.js.map +1 -0
- package/dist/templates/workflows/pipeline-path-based.yml.tpl.d.ts +16 -0
- package/dist/templates/workflows/pipeline-path-based.yml.tpl.d.ts.map +1 -0
- package/dist/templates/workflows/pipeline-path-based.yml.tpl.js +652 -0
- package/dist/templates/workflows/pipeline-path-based.yml.tpl.js.map +1 -0
- package/dist/types/index.d.ts +63 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/ast-path-operations.d.ts +287 -0
- package/dist/utils/ast-path-operations.d.ts.map +1 -0
- package/dist/utils/ast-path-operations.js +419 -0
- package/dist/utils/ast-path-operations.js.map +1 -0
- package/dist/utils/config.d.ts +3 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +49 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/github-setup.d.ts +74 -0
- package/dist/utils/github-setup.d.ts.map +1 -0
- package/dist/utils/github-setup.js +418 -0
- package/dist/utils/github-setup.js.map +1 -0
- package/dist/utils/idempotency.d.ts +47 -0
- package/dist/utils/idempotency.d.ts.map +1 -0
- package/dist/utils/idempotency.js +176 -0
- package/dist/utils/idempotency.js.map +1 -0
- package/dist/utils/preflight.d.ts +55 -0
- package/dist/utils/preflight.d.ts.map +1 -0
- package/dist/utils/preflight.js +261 -0
- package/dist/utils/preflight.js.map +1 -0
- package/dist/utils/versioning.d.ts +41 -0
- package/dist/utils/versioning.d.ts.map +1 -0
- package/dist/utils/versioning.js +219 -0
- package/dist/utils/versioning.js.map +1 -0
- package/docs/USER_JOURNEY_ERRORS.md +352 -0
- package/eslint.config.js +77 -0
- package/examples/basic-config.json +41 -0
- package/examples/monorepo-config.json +49 -0
- package/examples/usage.md +289 -0
- package/package.json +58 -0
- package/scripts/debug-workflows.sh +339 -0
- package/scripts/parse-pipeline.js +29 -0
- package/scripts/test-job-order.cjs +140 -0
- package/scripts/validate-pipeline.cjs +379 -0
- package/scripts/verify-job-order.sh +30 -0
- package/src/cli/index.ts +414 -0
- package/src/generators/init.tpl.ts +126 -0
- package/src/generators/workflows.tpl.ts +80 -0
- package/src/templates/actions/calculate-version.yml.tpl.ts +124 -0
- package/src/templates/actions/create-pr.yml.tpl.ts +147 -0
- package/src/templates/actions/create-tag.yml.tpl.ts +112 -0
- package/src/templates/actions/detect-changes.yml.tpl.ts +87 -0
- package/src/templates/actions/manage-branch.yml.tpl.ts +132 -0
- package/src/templates/actions/promote-branch.yml.tpl.ts +364 -0
- package/src/templates/workflows/pipeline-path-based.yml.tpl.ts +706 -0
- package/src/types/index.ts +64 -0
- package/src/utils/README-ast-path-operations.md +390 -0
- package/src/utils/ast-path-operations.ts +581 -0
- package/src/utils/config.ts +64 -0
- package/src/utils/github-setup.ts +558 -0
- package/src/utils/idempotency.ts +215 -0
- package/src/utils/preflight.ts +306 -0
- package/src/utils/versioning.ts +244 -0
- package/tests/README.md +229 -0
- package/tests/TEST_STRUCTURE.md +256 -0
- package/tests/act/run-act-tests.sh +345 -0
- package/tests/debugging/debug-utils.ts +538 -0
- package/tests/debugging/debug-workflow.test.ts +339 -0
- package/tests/debugging/debug-workflows.sh +339 -0
- package/tests/debugging/iterative-debug.ts +652 -0
- package/tests/debugging/run-debug-tests.sh +431 -0
- package/tests/fixtures/basic-config.json +51 -0
- package/tests/fixtures/invalid-config.json +9 -0
- package/tests/fixtures/pipeline-generated.yml +235 -0
- package/tests/fixtures/pipeline-preserve-comments.yml +245 -0
- package/tests/fixtures/pipeline-user-modified.yml +245 -0
- package/tests/fixtures/test-config.json +58 -0
- package/tests/github-live/README.md +250 -0
- package/tests/github-local/JOB_WORKFLOW_TESTS.md +296 -0
- package/tests/github-local/README.md +240 -0
- package/tests/github-local/run-all-tests.sh +422 -0
- package/tests/github-local/test-job-workflows.sh +631 -0
- package/tests/github-local/test-pipeline-workflow.sh +440 -0
- package/tests/integration/generators.test.ts +578 -0
- package/tests/integration/path-based-template.test.ts +510 -0
- package/tests/integration/simple-path-based.test.ts +415 -0
- package/tests/setup.ts +56 -0
- package/tests/unit/ast-path-operations-extended.test.ts +302 -0
- package/tests/unit/cli.test.ts +541 -0
- package/tests/unit/config-extended.test.ts +412 -0
- package/tests/unit/config.test.ts +152 -0
- package/tests/unit/github-setup.test.ts +189 -0
- package/tests/unit/idempotency-isolated.test.ts +297 -0
- package/tests/unit/job-order.test.ts +157 -0
- package/tests/unit/pipeline-path-based.test.ts +511 -0
- package/tests/unit/validate-pipeline.test.ts +384 -0
- package/tests/unit/versioning-extended.test.ts +279 -0
- package/tests/unit/versioning.test.ts +241 -0
- package/tsconfig.debug.json +17 -0
- package/tsconfig.json +28 -0
- package/vitest.config.ts +51 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Actions Workflow Debugging Test Suite
|
|
3
|
+
*
|
|
4
|
+
* This test suite validates the debugging utilities and ensures
|
|
5
|
+
* they can properly analyze workflow failures.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
9
|
+
import {
|
|
10
|
+
GitHubWorkflowDebugger,
|
|
11
|
+
WorkflowAnalyzer,
|
|
12
|
+
LogAnalyzer,
|
|
13
|
+
DebugReportGenerator,
|
|
14
|
+
WorkflowDebugOrchestrator,
|
|
15
|
+
type WorkflowRun,
|
|
16
|
+
type Job,
|
|
17
|
+
type DebugAnalysis,
|
|
18
|
+
type LogAnalysis
|
|
19
|
+
} from './debug-utils';
|
|
20
|
+
|
|
21
|
+
// Mock data for testing
|
|
22
|
+
const mockWorkflowRun: WorkflowRun = {
|
|
23
|
+
id: 12345,
|
|
24
|
+
status: 'completed',
|
|
25
|
+
conclusion: 'failure',
|
|
26
|
+
created_at: '2024-01-15T10:00:00Z',
|
|
27
|
+
html_url: 'https://github.com/owner/repo/actions/runs/12345',
|
|
28
|
+
workflow_id: 67890,
|
|
29
|
+
head_branch: 'main',
|
|
30
|
+
head_sha: 'abc123def456'
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const mockJobs: Job[] = [
|
|
34
|
+
{
|
|
35
|
+
id: 1,
|
|
36
|
+
name: 'test-api',
|
|
37
|
+
status: 'completed',
|
|
38
|
+
conclusion: 'failure',
|
|
39
|
+
html_url: 'https://github.com/owner/repo/actions/runs/12345/jobs/1',
|
|
40
|
+
started_at: '2024-01-15T10:00:00Z',
|
|
41
|
+
completed_at: '2024-01-15T10:05:00Z',
|
|
42
|
+
steps: [
|
|
43
|
+
{
|
|
44
|
+
name: 'Setup Node.js',
|
|
45
|
+
status: 'completed',
|
|
46
|
+
conclusion: 'success',
|
|
47
|
+
number: 1,
|
|
48
|
+
started_at: '2024-01-15T10:00:00Z',
|
|
49
|
+
completed_at: '2024-01-15T10:01:00Z'
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'Install dependencies',
|
|
53
|
+
status: 'completed',
|
|
54
|
+
conclusion: 'success',
|
|
55
|
+
number: 2,
|
|
56
|
+
started_at: '2024-01-15T10:01:00Z',
|
|
57
|
+
completed_at: '2024-01-15T10:02:00Z'
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'Run tests',
|
|
61
|
+
status: 'completed',
|
|
62
|
+
conclusion: 'failure',
|
|
63
|
+
number: 3,
|
|
64
|
+
started_at: '2024-01-15T10:02:00Z',
|
|
65
|
+
completed_at: '2024-01-15T10:05:00Z'
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: 2,
|
|
71
|
+
name: 'build-web',
|
|
72
|
+
status: 'completed',
|
|
73
|
+
conclusion: 'failure',
|
|
74
|
+
html_url: 'https://github.com/owner/repo/actions/runs/12345/jobs/2',
|
|
75
|
+
started_at: '2024-01-15T10:00:00Z',
|
|
76
|
+
completed_at: '2024-01-15T10:03:00Z',
|
|
77
|
+
steps: [
|
|
78
|
+
{
|
|
79
|
+
name: 'Build application',
|
|
80
|
+
status: 'completed',
|
|
81
|
+
conclusion: 'failure',
|
|
82
|
+
number: 1,
|
|
83
|
+
started_at: '2024-01-15T10:00:00Z',
|
|
84
|
+
completed_at: '2024-01-15T10:03:00Z'
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: 3,
|
|
90
|
+
name: 'lint-code',
|
|
91
|
+
status: 'completed',
|
|
92
|
+
conclusion: 'success',
|
|
93
|
+
html_url: 'https://github.com/owner/repo/actions/runs/12345/jobs/3',
|
|
94
|
+
started_at: '2024-01-15T10:00:00Z',
|
|
95
|
+
completed_at: '2024-01-15T10:01:00Z',
|
|
96
|
+
steps: [
|
|
97
|
+
{
|
|
98
|
+
name: 'Run ESLint',
|
|
99
|
+
status: 'completed',
|
|
100
|
+
conclusion: 'success',
|
|
101
|
+
number: 1,
|
|
102
|
+
started_at: '2024-01-15T10:00:00Z',
|
|
103
|
+
completed_at: '2024-01-15T10:01:00Z'
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
const mockLogs = `
|
|
110
|
+
2024-01-15T10:02:00.000Z [INFO] Starting test suite
|
|
111
|
+
2024-01-15T10:02:30.000Z [ERROR] Test failed: API endpoint not responding
|
|
112
|
+
2024-01-15T10:03:00.000Z [WARN] Connection timeout after 30 seconds
|
|
113
|
+
2024-01-15T10:03:30.000Z [ERROR] Fatal error: Database connection refused
|
|
114
|
+
2024-01-15T10:04:00.000Z [ERROR] Test suite failed with 5 errors
|
|
115
|
+
2024-01-15T10:05:00.000Z [INFO] Test execution completed
|
|
116
|
+
`;
|
|
117
|
+
|
|
118
|
+
describe('WorkflowAnalyzer', () => {
|
|
119
|
+
it('should analyze workflow run and identify failed jobs', () => {
|
|
120
|
+
const analysis = WorkflowAnalyzer.analyzeWorkflowRun(mockWorkflowRun, mockJobs);
|
|
121
|
+
|
|
122
|
+
expect(analysis.runId).toBe(12345);
|
|
123
|
+
expect(analysis.status).toBe('completed');
|
|
124
|
+
expect(analysis.conclusion).toBe('failure');
|
|
125
|
+
expect(analysis.failedJobs).toHaveLength(2);
|
|
126
|
+
expect(analysis.totalJobs).toBe(3);
|
|
127
|
+
expect(analysis.successRate).toBeCloseTo(33.33, 1);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should extract common failure patterns', () => {
|
|
131
|
+
const analysis = WorkflowAnalyzer.analyzeWorkflowRun(mockWorkflowRun, mockJobs);
|
|
132
|
+
|
|
133
|
+
expect(analysis.commonFailurePatterns).toContain('Test Failures');
|
|
134
|
+
expect(analysis.commonFailurePatterns).toContain('Build Failures');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should generate relevant recommendations', () => {
|
|
138
|
+
const analysis = WorkflowAnalyzer.analyzeWorkflowRun(mockWorkflowRun, mockJobs);
|
|
139
|
+
|
|
140
|
+
expect(analysis.recommendations).toContain('Review test cases and ensure they are up to date');
|
|
141
|
+
expect(analysis.recommendations).toContain('Verify build dependencies and versions');
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('LogAnalyzer', () => {
|
|
146
|
+
it('should analyze logs and count errors and warnings', () => {
|
|
147
|
+
const analysis = LogAnalyzer.analyzeLogs(mockLogs, 'test-api');
|
|
148
|
+
|
|
149
|
+
expect(analysis.jobName).toBe('test-api');
|
|
150
|
+
expect(analysis.errorCount).toBeGreaterThan(0);
|
|
151
|
+
expect(analysis.warningCount).toBeGreaterThan(0);
|
|
152
|
+
expect(analysis.criticalErrors.length).toBeGreaterThan(0);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should extract common error patterns', () => {
|
|
156
|
+
const analysis = LogAnalyzer.analyzeLogs(mockLogs, 'test-api');
|
|
157
|
+
|
|
158
|
+
expect(analysis.commonErrors.length).toBeGreaterThan(0);
|
|
159
|
+
expect(analysis.executionTime).toBeGreaterThan(0);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should identify critical errors', () => {
|
|
163
|
+
const analysis = LogAnalyzer.analyzeLogs(mockLogs, 'test-api');
|
|
164
|
+
|
|
165
|
+
expect(analysis.criticalErrors.length).toBeGreaterThan(0);
|
|
166
|
+
expect(analysis.criticalErrors.some(error => error.includes('Database connection refused'))).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('DebugReportGenerator', () => {
|
|
171
|
+
let mockAnalysis: DebugAnalysis;
|
|
172
|
+
let mockLogAnalyses: LogAnalysis[];
|
|
173
|
+
|
|
174
|
+
beforeEach(() => {
|
|
175
|
+
mockAnalysis = {
|
|
176
|
+
runId: 12345,
|
|
177
|
+
status: 'completed',
|
|
178
|
+
conclusion: 'failure',
|
|
179
|
+
failedJobs: mockJobs.filter(job => job.conclusion === 'failure'),
|
|
180
|
+
totalJobs: 3,
|
|
181
|
+
successRate: 33.33,
|
|
182
|
+
commonFailurePatterns: ['Test Failures', 'Build Failures'],
|
|
183
|
+
recommendations: [
|
|
184
|
+
'Review test cases and ensure they are up to date',
|
|
185
|
+
'Verify build dependencies and versions'
|
|
186
|
+
]
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
mockLogAnalyses = [
|
|
190
|
+
{
|
|
191
|
+
jobId: 1,
|
|
192
|
+
jobName: 'test-api',
|
|
193
|
+
errorCount: 3,
|
|
194
|
+
warningCount: 1,
|
|
195
|
+
criticalErrors: ['Fatal error: Database connection refused'],
|
|
196
|
+
commonErrors: ['API endpoint not responding', 'Database connection refused'],
|
|
197
|
+
executionTime: 180
|
|
198
|
+
}
|
|
199
|
+
];
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should generate a comprehensive debug report', async () => {
|
|
203
|
+
const reportPath = await DebugReportGenerator.generateReport(
|
|
204
|
+
mockAnalysis,
|
|
205
|
+
mockLogAnalyses,
|
|
206
|
+
'./test-output'
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
expect(reportPath).toContain('debug-report.md');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should include all analysis data in the report', async () => {
|
|
213
|
+
const reportPath = await DebugReportGenerator.generateReport(
|
|
214
|
+
mockAnalysis,
|
|
215
|
+
mockLogAnalyses,
|
|
216
|
+
'./test-output'
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
// The report should be generated successfully
|
|
220
|
+
expect(reportPath).toBeDefined();
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe('GitHubWorkflowDebugger', () => {
|
|
225
|
+
let workflowDebugger: GitHubWorkflowDebugger;
|
|
226
|
+
|
|
227
|
+
beforeEach(() => {
|
|
228
|
+
workflowDebugger = new GitHubWorkflowDebugger('fake-token', 'owner', 'repo');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should initialize with correct parameters', () => {
|
|
232
|
+
expect(workflowDebugger).toBeDefined();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should handle API request errors gracefully', async () => {
|
|
236
|
+
// Mock fetch to throw an error
|
|
237
|
+
global.fetch = vi.fn().mockRejectedValue(new Error('API request failed'));
|
|
238
|
+
|
|
239
|
+
await expect(workflowDebugger.getWorkflowRuns('test-workflow')).rejects.toThrow('API request failed');
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe('WorkflowDebugOrchestrator', () => {
|
|
244
|
+
let orchestrator: WorkflowDebugOrchestrator;
|
|
245
|
+
|
|
246
|
+
beforeEach(() => {
|
|
247
|
+
orchestrator = new WorkflowDebugOrchestrator('fake-token', 'owner', 'repo', './test-output');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should initialize with correct parameters', () => {
|
|
251
|
+
expect(orchestrator).toBeDefined();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('should handle missing workflow runs', async () => {
|
|
255
|
+
// Mock the workflowDebugger to return empty runs
|
|
256
|
+
const mockWorkflowDebugger = {
|
|
257
|
+
getWorkflowRuns: vi.fn().mockResolvedValue([])
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// Replace the workflowDebugger instance
|
|
261
|
+
(orchestrator as any).debugger = mockWorkflowDebugger;
|
|
262
|
+
|
|
263
|
+
await expect(orchestrator.debugWorkflow('test-workflow')).rejects.toThrow('No workflow runs found');
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe('Integration Tests', () => {
|
|
268
|
+
it('should handle end-to-end debugging workflow', async () => {
|
|
269
|
+
// This test would require actual GitHub API access
|
|
270
|
+
// For now, we'll test the structure and error handling
|
|
271
|
+
|
|
272
|
+
const orchestrator = new WorkflowDebugOrchestrator('fake-token', 'owner', 'repo');
|
|
273
|
+
|
|
274
|
+
// Mock successful API responses
|
|
275
|
+
const mockRuns = [mockWorkflowRun];
|
|
276
|
+
const mockJobsData = mockJobs;
|
|
277
|
+
|
|
278
|
+
// This would be the actual integration test
|
|
279
|
+
// await orchestrator.debugWorkflow('test-workflow');
|
|
280
|
+
|
|
281
|
+
expect(orchestrator).toBeDefined();
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe('Error Handling', () => {
|
|
286
|
+
it('should handle network errors gracefully', async () => {
|
|
287
|
+
const workflowDebugger = new GitHubWorkflowDebugger('fake-token', 'owner', 'repo');
|
|
288
|
+
|
|
289
|
+
// Mock network error
|
|
290
|
+
global.fetch = vi.fn().mockRejectedValue(new Error('Network error'));
|
|
291
|
+
|
|
292
|
+
await expect(workflowDebugger.getWorkflowRuns('test-workflow')).rejects.toThrow('Network error');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should handle invalid JSON responses', async () => {
|
|
296
|
+
const workflowDebugger = new GitHubWorkflowDebugger('fake-token', 'owner', 'repo');
|
|
297
|
+
|
|
298
|
+
// Mock invalid JSON response
|
|
299
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
300
|
+
ok: true,
|
|
301
|
+
json: () => Promise.reject(new Error('Invalid JSON'))
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
await expect(workflowDebugger.getWorkflowRuns('test-workflow')).rejects.toThrow('Invalid JSON');
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
describe('Performance Tests', () => {
|
|
309
|
+
it('should handle large log files efficiently', () => {
|
|
310
|
+
const largeLogs = 'INFO: Starting\n'.repeat(10000) + 'ERROR: Failed\n'.repeat(100);
|
|
311
|
+
|
|
312
|
+
const startTime = Date.now();
|
|
313
|
+
const analysis = LogAnalyzer.analyzeLogs(largeLogs, 'test-job');
|
|
314
|
+
const endTime = Date.now();
|
|
315
|
+
|
|
316
|
+
expect(analysis.errorCount).toBe(100);
|
|
317
|
+
expect(endTime - startTime).toBeLessThan(1000); // Should complete in under 1 second
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('should handle many jobs efficiently', () => {
|
|
321
|
+
const manyJobs: Job[] = Array.from({ length: 100 }, (_, i) => ({
|
|
322
|
+
id: i,
|
|
323
|
+
name: `job-${i}`,
|
|
324
|
+
status: 'completed',
|
|
325
|
+
conclusion: i % 10 === 0 ? 'failure' : 'success',
|
|
326
|
+
html_url: `https://example.com/job/${i}`,
|
|
327
|
+
started_at: '2024-01-15T10:00:00Z',
|
|
328
|
+
completed_at: '2024-01-15T10:05:00Z',
|
|
329
|
+
steps: []
|
|
330
|
+
}));
|
|
331
|
+
|
|
332
|
+
const startTime = Date.now();
|
|
333
|
+
const analysis = WorkflowAnalyzer.analyzeWorkflowRun(mockWorkflowRun, manyJobs);
|
|
334
|
+
const endTime = Date.now();
|
|
335
|
+
|
|
336
|
+
expect(analysis.totalJobs).toBe(100);
|
|
337
|
+
expect(endTime - startTime).toBeLessThan(500); // Should complete in under 500ms
|
|
338
|
+
});
|
|
339
|
+
});
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# GitHub Actions Workflow Debugging Script
|
|
4
|
+
# This script fetches workflow status, job details, and logs for debugging
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
# Configuration
|
|
9
|
+
REPO_OWNER="${GITHUB_REPOSITORY_OWNER:-$(git config --get remote.origin.url | sed 's/.*github.com[:/]\([^/]*\)\/\([^/]*\)\.git.*/\1/')}"
|
|
10
|
+
REPO_NAME="${GITHUB_REPOSITORY##*/:-$(git config --get remote.origin.url | sed 's/.*github.com[:/]\([^/]*\)\/\([^/]*\)\.git.*/\2/')}"
|
|
11
|
+
GITHUB_TOKEN="${GITHUB_TOKEN:-}"
|
|
12
|
+
API_BASE="https://api.github.com"
|
|
13
|
+
|
|
14
|
+
# Colors for output
|
|
15
|
+
RED='\033[0;31m'
|
|
16
|
+
GREEN='\033[0;32m'
|
|
17
|
+
YELLOW='\033[1;33m'
|
|
18
|
+
BLUE='\033[0;34m'
|
|
19
|
+
PURPLE='\033[0;35m'
|
|
20
|
+
CYAN='\033[0;36m'
|
|
21
|
+
NC='\033[0m' # No Color
|
|
22
|
+
|
|
23
|
+
# Default values
|
|
24
|
+
WORKFLOW_NAME=""
|
|
25
|
+
BRANCH=""
|
|
26
|
+
LIMIT=10
|
|
27
|
+
OUTPUT_DIR="./debug-output"
|
|
28
|
+
VERBOSE=false
|
|
29
|
+
FETCH_LOGS=true
|
|
30
|
+
|
|
31
|
+
# Function to print colored output
|
|
32
|
+
print_status() {
|
|
33
|
+
local status=$1
|
|
34
|
+
local message=$2
|
|
35
|
+
|
|
36
|
+
case $status in
|
|
37
|
+
"SUCCESS") echo -e "${GREEN}✓${NC} $message" ;;
|
|
38
|
+
"FAILURE") echo -e "${RED}✗${NC} $message" ;;
|
|
39
|
+
"CANCELLED") echo -e "${YELLOW}⚠${NC} $message" ;;
|
|
40
|
+
"IN_PROGRESS") echo -e "${BLUE}⟳${NC} $message" ;;
|
|
41
|
+
"QUEUED") echo -e "${PURPLE}⏳${NC} $message" ;;
|
|
42
|
+
"SKIPPED") echo -e "${CYAN}⊘${NC} $message" ;;
|
|
43
|
+
*) echo -e "${YELLOW}?${NC} $message" ;;
|
|
44
|
+
esac
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Function to make GitHub API calls
|
|
48
|
+
github_api() {
|
|
49
|
+
local endpoint=$1
|
|
50
|
+
local method=${2:-GET}
|
|
51
|
+
|
|
52
|
+
if [[ -z "$GITHUB_TOKEN" ]]; then
|
|
53
|
+
echo -e "${RED}Error: GITHUB_TOKEN environment variable is required${NC}" >&2
|
|
54
|
+
exit 1
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
curl -s -H "Authorization: token $GITHUB_TOKEN" \
|
|
58
|
+
-H "Accept: application/vnd.github.v3+json" \
|
|
59
|
+
-X "$method" \
|
|
60
|
+
"$API_BASE/repos/$REPO_OWNER/$REPO_NAME$endpoint"
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Function to get workflow runs
|
|
64
|
+
get_workflow_runs() {
|
|
65
|
+
local workflow_name=$1
|
|
66
|
+
local branch=$2
|
|
67
|
+
local limit=$3
|
|
68
|
+
|
|
69
|
+
local endpoint="/actions/workflows/$workflow_name/runs"
|
|
70
|
+
local params="?per_page=$limit"
|
|
71
|
+
|
|
72
|
+
if [[ -n "$branch" ]]; then
|
|
73
|
+
params="${params}&branch=$branch"
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
github_api "$endpoint$params"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Function to get workflow run details
|
|
80
|
+
get_workflow_run() {
|
|
81
|
+
local run_id=$1
|
|
82
|
+
github_api "/actions/runs/$run_id"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# Function to get jobs for a workflow run
|
|
86
|
+
get_workflow_jobs() {
|
|
87
|
+
local run_id=$1
|
|
88
|
+
github_api "/actions/runs/$run_id/jobs"
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Function to get job logs
|
|
92
|
+
get_job_logs() {
|
|
93
|
+
local run_id=$1
|
|
94
|
+
local job_id=$2
|
|
95
|
+
local output_file=$3
|
|
96
|
+
|
|
97
|
+
local endpoint="/actions/runs/$run_id/jobs/$job_id/logs"
|
|
98
|
+
|
|
99
|
+
if [[ -z "$GITHUB_TOKEN" ]]; then
|
|
100
|
+
echo -e "${RED}Error: GITHUB_TOKEN required for log download${NC}" >&2
|
|
101
|
+
return 1
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
curl -s -H "Authorization: token $GITHUB_TOKEN" \
|
|
105
|
+
-H "Accept: application/vnd.github.v3+json" \
|
|
106
|
+
"$API_BASE/repos/$REPO_OWNER/$REPO_NAME$endpoint" > "$output_file"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# Function to analyze workflow failures
|
|
110
|
+
analyze_failures() {
|
|
111
|
+
local run_id=$1
|
|
112
|
+
local output_dir=$2
|
|
113
|
+
|
|
114
|
+
echo -e "${BLUE}Analyzing workflow failures for run $run_id...${NC}"
|
|
115
|
+
|
|
116
|
+
# Get workflow run details
|
|
117
|
+
local run_details=$(get_workflow_run "$run_id")
|
|
118
|
+
local run_status=$(echo "$run_details" | jq -r '.conclusion // .status')
|
|
119
|
+
local run_url=$(echo "$run_details" | jq -r '.html_url')
|
|
120
|
+
|
|
121
|
+
echo -e "${CYAN}Workflow Status:${NC} $run_status"
|
|
122
|
+
echo -e "${CYAN}Workflow URL:${NC} $run_url"
|
|
123
|
+
|
|
124
|
+
# Get jobs
|
|
125
|
+
local jobs=$(get_workflow_jobs "$run_id")
|
|
126
|
+
local failed_jobs=$(echo "$jobs" | jq -r '.jobs[] | select(.conclusion == "failure" or .conclusion == "cancelled") | .name')
|
|
127
|
+
|
|
128
|
+
if [[ -z "$failed_jobs" ]]; then
|
|
129
|
+
echo -e "${GREEN}No failed jobs found${NC}"
|
|
130
|
+
return 0
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
echo -e "${RED}Failed Jobs:${NC}"
|
|
134
|
+
echo "$failed_jobs" | while read -r job_name; do
|
|
135
|
+
echo -e " ${RED}•${NC} $job_name"
|
|
136
|
+
done
|
|
137
|
+
|
|
138
|
+
# Get detailed job information
|
|
139
|
+
echo "$jobs" | jq -r '.jobs[] | select(.conclusion == "failure" or .conclusion == "cancelled") | .id' | while read -r job_id; do
|
|
140
|
+
local job_info=$(echo "$jobs" | jq -r ".jobs[] | select(.id == $job_id)")
|
|
141
|
+
local job_name=$(echo "$job_info" | jq -r '.name')
|
|
142
|
+
local job_conclusion=$(echo "$job_info" | jq -r '.conclusion')
|
|
143
|
+
local job_url=$(echo "$job_info" | jq -r '.html_url')
|
|
144
|
+
|
|
145
|
+
echo -e "\n${YELLOW}Job: $job_name${NC}"
|
|
146
|
+
echo -e "${YELLOW}Status:${NC} $job_conclusion"
|
|
147
|
+
echo -e "${YELLOW}URL:${NC} $job_url"
|
|
148
|
+
|
|
149
|
+
# Get job steps
|
|
150
|
+
local steps=$(echo "$job_info" | jq -r '.steps[] | select(.conclusion == "failure") | "\(.name): \(.conclusion)"')
|
|
151
|
+
if [[ -n "$steps" ]]; then
|
|
152
|
+
echo -e "${YELLOW}Failed Steps:${NC}"
|
|
153
|
+
echo "$steps" | while read -r step; do
|
|
154
|
+
echo -e " ${RED}•${NC} $step"
|
|
155
|
+
done
|
|
156
|
+
fi
|
|
157
|
+
|
|
158
|
+
# Download logs if requested
|
|
159
|
+
if [[ "$FETCH_LOGS" == "true" ]]; then
|
|
160
|
+
local log_file="$output_dir/job-${job_id}-logs.txt"
|
|
161
|
+
echo -e "${BLUE}Downloading logs for job $job_name...${NC}"
|
|
162
|
+
if get_job_logs "$run_id" "$job_id" "$log_file"; then
|
|
163
|
+
echo -e "${GREEN}Logs saved to: $log_file${NC}"
|
|
164
|
+
else
|
|
165
|
+
echo -e "${RED}Failed to download logs for job $job_name${NC}"
|
|
166
|
+
fi
|
|
167
|
+
fi
|
|
168
|
+
done
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# Function to create summary report
|
|
172
|
+
create_summary_report() {
|
|
173
|
+
local output_dir=$1
|
|
174
|
+
local report_file="$output_dir/debug-summary.md"
|
|
175
|
+
|
|
176
|
+
echo -e "${BLUE}Creating summary report...${NC}"
|
|
177
|
+
|
|
178
|
+
cat > "$report_file" << EOF
|
|
179
|
+
# GitHub Actions Debug Summary
|
|
180
|
+
|
|
181
|
+
Generated: $(date)
|
|
182
|
+
Repository: $REPO_OWNER/$REPO_NAME
|
|
183
|
+
Workflow: $WORKFLOW_NAME
|
|
184
|
+
Branch: ${BRANCH:-"all"}
|
|
185
|
+
|
|
186
|
+
## Recent Workflow Runs
|
|
187
|
+
|
|
188
|
+
EOF
|
|
189
|
+
|
|
190
|
+
# Get recent runs
|
|
191
|
+
local runs=$(get_workflow_runs "$WORKFLOW_NAME" "$BRANCH" "$LIMIT")
|
|
192
|
+
|
|
193
|
+
echo "$runs" | jq -r '.workflow_runs[] | "\(.id)|\(.status)|\(.conclusion)|\(.created_at)|\(.html_url)"' | while IFS='|' read -r run_id status conclusion created_at url; do
|
|
194
|
+
echo "### Run $run_id" >> "$report_file"
|
|
195
|
+
echo "- **Status**: $status" >> "$report_file"
|
|
196
|
+
echo "- **Conclusion**: $conclusion" >> "$report_file"
|
|
197
|
+
echo "- **Created**: $created_at" >> "$report_file"
|
|
198
|
+
echo "- **URL**: $url" >> "$report_file"
|
|
199
|
+
echo "" >> "$report_file"
|
|
200
|
+
done
|
|
201
|
+
|
|
202
|
+
echo -e "${GREEN}Summary report created: $report_file${NC}"
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
# Function to show usage
|
|
206
|
+
show_usage() {
|
|
207
|
+
cat << EOF
|
|
208
|
+
Usage: $0 [OPTIONS]
|
|
209
|
+
|
|
210
|
+
GitHub Actions Workflow Debugging Script
|
|
211
|
+
|
|
212
|
+
OPTIONS:
|
|
213
|
+
-w, --workflow NAME Workflow name to debug (required)
|
|
214
|
+
-b, --branch BRANCH Branch to filter runs (optional)
|
|
215
|
+
-l, --limit NUMBER Number of recent runs to fetch (default: 10)
|
|
216
|
+
-o, --output DIR Output directory for logs and reports (default: ./debug-output)
|
|
217
|
+
-t, --token TOKEN GitHub token (or set GITHUB_TOKEN env var)
|
|
218
|
+
-v, --verbose Verbose output
|
|
219
|
+
--no-logs Don't download job logs
|
|
220
|
+
-h, --help Show this help message
|
|
221
|
+
|
|
222
|
+
EXAMPLES:
|
|
223
|
+
$0 -w "My Custom Pipeline" -b main -l 5
|
|
224
|
+
$0 -w "CI" --no-logs -o ./debug-logs
|
|
225
|
+
GITHUB_TOKEN=your_token $0 -w "Deploy" -v
|
|
226
|
+
|
|
227
|
+
ENVIRONMENT VARIABLES:
|
|
228
|
+
GITHUB_TOKEN GitHub personal access token
|
|
229
|
+
GITHUB_REPOSITORY_OWNER Repository owner (auto-detected from git)
|
|
230
|
+
GITHUB_REPOSITORY Repository name (auto-detected from git)
|
|
231
|
+
|
|
232
|
+
EOF
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
# Parse command line arguments
|
|
236
|
+
while [[ $# -gt 0 ]]; do
|
|
237
|
+
case $1 in
|
|
238
|
+
-w|--workflow)
|
|
239
|
+
WORKFLOW_NAME="$2"
|
|
240
|
+
shift 2
|
|
241
|
+
;;
|
|
242
|
+
-b|--branch)
|
|
243
|
+
BRANCH="$2"
|
|
244
|
+
shift 2
|
|
245
|
+
;;
|
|
246
|
+
-l|--limit)
|
|
247
|
+
LIMIT="$2"
|
|
248
|
+
shift 2
|
|
249
|
+
;;
|
|
250
|
+
-o|--output)
|
|
251
|
+
OUTPUT_DIR="$2"
|
|
252
|
+
shift 2
|
|
253
|
+
;;
|
|
254
|
+
-t|--token)
|
|
255
|
+
GITHUB_TOKEN="$2"
|
|
256
|
+
shift 2
|
|
257
|
+
;;
|
|
258
|
+
-v|--verbose)
|
|
259
|
+
VERBOSE=true
|
|
260
|
+
shift
|
|
261
|
+
;;
|
|
262
|
+
--no-logs)
|
|
263
|
+
FETCH_LOGS=false
|
|
264
|
+
shift
|
|
265
|
+
;;
|
|
266
|
+
-h|--help)
|
|
267
|
+
show_usage
|
|
268
|
+
exit 0
|
|
269
|
+
;;
|
|
270
|
+
*)
|
|
271
|
+
echo -e "${RED}Unknown option: $1${NC}" >&2
|
|
272
|
+
show_usage
|
|
273
|
+
exit 1
|
|
274
|
+
;;
|
|
275
|
+
esac
|
|
276
|
+
done
|
|
277
|
+
|
|
278
|
+
# Validate required parameters
|
|
279
|
+
if [[ -z "$WORKFLOW_NAME" ]]; then
|
|
280
|
+
echo -e "${RED}Error: Workflow name is required${NC}" >&2
|
|
281
|
+
show_usage
|
|
282
|
+
exit 1
|
|
283
|
+
fi
|
|
284
|
+
|
|
285
|
+
if [[ -z "$GITHUB_TOKEN" ]]; then
|
|
286
|
+
echo -e "${RED}Error: GitHub token is required${NC}" >&2
|
|
287
|
+
echo -e "${YELLOW}Set GITHUB_TOKEN environment variable or use -t option${NC}"
|
|
288
|
+
exit 1
|
|
289
|
+
fi
|
|
290
|
+
|
|
291
|
+
# Check if jq is installed
|
|
292
|
+
if ! command -v jq &> /dev/null; then
|
|
293
|
+
echo -e "${RED}Error: jq is required but not installed${NC}" >&2
|
|
294
|
+
echo -e "${YELLOW}Install jq: brew install jq (macOS) or apt-get install jq (Ubuntu)${NC}"
|
|
295
|
+
exit 1
|
|
296
|
+
fi
|
|
297
|
+
|
|
298
|
+
# Create output directory
|
|
299
|
+
mkdir -p "$OUTPUT_DIR"
|
|
300
|
+
|
|
301
|
+
echo -e "${BLUE}GitHub Actions Workflow Debugger${NC}"
|
|
302
|
+
echo -e "${CYAN}Repository:${NC} $REPO_OWNER/$REPO_NAME"
|
|
303
|
+
echo -e "${CYAN}Workflow:${NC} $WORKFLOW_NAME"
|
|
304
|
+
echo -e "${CYAN}Branch:${NC} ${BRANCH:-"all"}"
|
|
305
|
+
echo -e "${CYAN}Output:${NC} $OUTPUT_DIR"
|
|
306
|
+
echo ""
|
|
307
|
+
|
|
308
|
+
# Get recent workflow runs
|
|
309
|
+
echo -e "${BLUE}Fetching recent workflow runs...${NC}"
|
|
310
|
+
runs=$(get_workflow_runs "$WORKFLOW_NAME" "$BRANCH" "$LIMIT")
|
|
311
|
+
|
|
312
|
+
if [[ "$runs" == "null" || $(echo "$runs" | jq '.workflow_runs | length') -eq 0 ]]; then
|
|
313
|
+
echo -e "${YELLOW}No workflow runs found${NC}"
|
|
314
|
+
exit 0
|
|
315
|
+
fi
|
|
316
|
+
|
|
317
|
+
# Display recent runs
|
|
318
|
+
echo -e "${CYAN}Recent Workflow Runs:${NC}"
|
|
319
|
+
echo "$runs" | jq -r '.workflow_runs[] | "\(.id)|\(.status)|\(.conclusion)|\(.created_at)|\(.html_url)"' | while IFS='|' read -r run_id status conclusion created_at url; do
|
|
320
|
+
print_status "$conclusion" "Run $run_id ($status) - $created_at"
|
|
321
|
+
if [[ "$VERBOSE" == "true" ]]; then
|
|
322
|
+
echo " URL: $url"
|
|
323
|
+
fi
|
|
324
|
+
done
|
|
325
|
+
|
|
326
|
+
# Analyze failures
|
|
327
|
+
echo ""
|
|
328
|
+
echo -e "${BLUE}Analyzing failures...${NC}"
|
|
329
|
+
echo "$runs" | jq -r '.workflow_runs[] | select(.conclusion == "failure" or .conclusion == "cancelled") | .id' | while read -r run_id; do
|
|
330
|
+
analyze_failures "$run_id" "$OUTPUT_DIR"
|
|
331
|
+
done
|
|
332
|
+
|
|
333
|
+
# Create summary report
|
|
334
|
+
create_summary_report "$OUTPUT_DIR"
|
|
335
|
+
|
|
336
|
+
echo ""
|
|
337
|
+
echo -e "${GREEN}Debug analysis complete!${NC}"
|
|
338
|
+
echo -e "${CYAN}Output directory:${NC} $OUTPUT_DIR"
|
|
339
|
+
echo -e "${CYAN}Summary report:${NC} $OUTPUT_DIR/debug-summary.md"
|