tlc-claude-code 2.4.10 → 2.6.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/.claude/commands/tlc/autofix.md +34 -1
- package/.claude/commands/tlc/build.md +203 -27
- package/.claude/commands/tlc/ci.md +178 -414
- package/.claude/commands/tlc/coverage.md +34 -0
- package/.claude/commands/tlc/deploy.md +19 -6
- package/.claude/commands/tlc/discuss.md +34 -0
- package/.claude/commands/tlc/docs.md +35 -1
- package/.claude/commands/tlc/e2e.md +300 -0
- package/.claude/commands/tlc/edge-cases.md +35 -1
- package/.claude/commands/tlc/init.md +38 -8
- package/.claude/commands/tlc/issues.md +46 -0
- package/.claude/commands/tlc/new-project.md +46 -4
- package/.claude/commands/tlc/plan.md +76 -0
- package/.claude/commands/tlc/quick.md +33 -0
- package/.claude/commands/tlc/release.md +85 -135
- package/.claude/commands/tlc/restore.md +14 -0
- package/.claude/commands/tlc/review.md +80 -1
- package/.claude/commands/tlc/tlc.md +134 -0
- package/.claude/commands/tlc/verify.md +64 -65
- package/.claude/commands/tlc/watchci.md +10 -0
- package/.claude/hooks/tlc-block-tools.sh +13 -0
- package/.claude/hooks/tlc-session-init.sh +9 -0
- package/CODING-STANDARDS.md +35 -10
- package/package.json +1 -1
- package/server/lib/block-tools-hook.js +23 -0
- package/server/lib/e2e/acceptance-parser.js +132 -0
- package/server/lib/e2e/acceptance-parser.test.js +110 -0
- package/server/lib/e2e/framework-detector.js +47 -0
- package/server/lib/e2e/framework-detector.test.js +94 -0
- package/server/lib/e2e/log-assertions.js +107 -0
- package/server/lib/e2e/log-assertions.test.js +68 -0
- package/server/lib/e2e/test-generator.js +159 -0
- package/server/lib/e2e/test-generator.test.js +121 -0
- package/server/lib/e2e/verify-runner.js +191 -0
- package/server/lib/e2e/verify-runner.test.js +167 -0
- package/server/lib/github/config.js +458 -0
- package/server/lib/github/config.test.js +385 -0
- package/server/lib/github/gh-client.js +303 -0
- package/server/lib/github/gh-client.test.js +499 -0
- package/server/lib/github/gh-projects.js +594 -0
- package/server/lib/github/gh-projects.test.js +583 -0
- package/server/lib/github/index.js +19 -0
- package/server/lib/github/plan-sync.js +456 -0
- package/server/lib/github/plan-sync.test.js +805 -0
- package/server/lib/hooks/block-tools-hook.test.js +54 -0
- package/server/lib/orchestration/cli-dispatch.js +16 -1
- package/server/lib/orchestration/cli-dispatch.test.js +94 -8
- package/server/lib/orchestration/completion-checker.js +101 -0
- package/server/lib/orchestration/completion-checker.test.js +177 -0
- package/server/lib/orchestration/result-verifier.js +143 -0
- package/server/lib/orchestration/result-verifier.test.js +291 -0
- package/server/lib/orchestration/session-dispatcher.js +99 -0
- package/server/lib/orchestration/session-dispatcher.test.js +215 -0
- package/server/lib/orchestration/session-status.js +147 -0
- package/server/lib/orchestration/session-status.test.js +130 -0
- package/server/lib/release/agent-runner-updates.js +24 -0
- package/server/lib/release/agent-runner-updates.test.js +22 -0
- package/server/lib/release/changelog-generator.js +142 -0
- package/server/lib/release/changelog-generator.test.js +113 -0
- package/server/lib/release/ci-watcher.js +83 -0
- package/server/lib/release/ci-watcher.test.js +81 -0
- package/server/lib/release/health-checker.js +111 -0
- package/server/lib/release/health-checker.test.js +121 -0
- package/server/lib/release/release-pipeline.js +187 -0
- package/server/lib/release/release-pipeline.test.js +262 -0
- package/server/lib/release/version-bumper.js +183 -0
- package/server/lib/release/version-bumper.test.js +142 -0
- package/server/lib/routing-preamble.integration.test.js +12 -0
- package/server/lib/routing-preamble.js +13 -2
- package/server/lib/routing-preamble.test.js +49 -0
- package/server/lib/scaffolding/ci-detector.js +139 -0
- package/server/lib/scaffolding/ci-detector.test.js +198 -0
- package/server/lib/scaffolding/ci-scaffolder.js +347 -0
- package/server/lib/scaffolding/ci-scaffolder.test.js +157 -0
- package/server/lib/scaffolding/deploy-detector.js +135 -0
- package/server/lib/scaffolding/deploy-detector.test.js +106 -0
- package/server/lib/scaffolding/health-scaffold.js +374 -0
- package/server/lib/scaffolding/health-scaffold.test.js +99 -0
- package/server/lib/scaffolding/logger-scaffold.js +196 -0
- package/server/lib/scaffolding/logger-scaffold.test.js +146 -0
- package/server/lib/scaffolding/migration-detector.js +78 -0
- package/server/lib/scaffolding/migration-detector.test.js +127 -0
- package/server/lib/scaffolding/snapshot-manager.js +142 -0
- package/server/lib/scaffolding/snapshot-manager.test.js +225 -0
- package/server/lib/task-router-config.js +50 -20
- package/server/lib/task-router-config.test.js +29 -15
|
@@ -37,8 +37,13 @@ function buildProgram(commandName) {
|
|
|
37
37
|
" };",
|
|
38
38
|
"}",
|
|
39
39
|
"function resolveRouting(options) {",
|
|
40
|
+
" const MODES = {",
|
|
41
|
+
" 'build': 'interactive', 'quick': 'interactive', 'autofix': 'interactive',",
|
|
42
|
+
" 'edge-cases': 'exec', 'review': 'exec', 'coverage': 'exec',",
|
|
43
|
+
" };",
|
|
40
44
|
" let models = ['claude'];",
|
|
41
45
|
" let strategy = 'single';",
|
|
46
|
+
" let mode = MODES[options.command] || 'inline';",
|
|
42
47
|
" let source = 'shipped-defaults';",
|
|
43
48
|
" let providers;",
|
|
44
49
|
" const warnings = [];",
|
|
@@ -62,6 +67,9 @@ function buildProgram(commandName) {
|
|
|
62
67
|
" if (personalRouting.strategy) {",
|
|
63
68
|
" strategy = personalRouting.strategy;",
|
|
64
69
|
" }",
|
|
70
|
+
" if (personalRouting.mode) {",
|
|
71
|
+
" mode = personalRouting.mode;",
|
|
72
|
+
" }",
|
|
65
73
|
" source = 'personal-config';",
|
|
66
74
|
" }",
|
|
67
75
|
" }",
|
|
@@ -82,7 +90,10 @@ function buildProgram(commandName) {
|
|
|
82
90
|
" if (overrideEntry.strategy) {",
|
|
83
91
|
" strategy = overrideEntry.strategy;",
|
|
84
92
|
" }",
|
|
85
|
-
|
|
93
|
+
" if (overrideEntry.mode) {",
|
|
94
|
+
" mode = overrideEntry.mode;",
|
|
95
|
+
" }",
|
|
96
|
+
" source = 'project-override';",
|
|
86
97
|
" }",
|
|
87
98
|
" }",
|
|
88
99
|
" if (options.flagModel) {",
|
|
@@ -90,7 +101,7 @@ function buildProgram(commandName) {
|
|
|
90
101
|
" strategy = 'single';",
|
|
91
102
|
" source = 'flag-override';",
|
|
92
103
|
" }",
|
|
93
|
-
" const result = { models, strategy, source, warnings };",
|
|
104
|
+
" const result = { models, strategy, mode, source, warnings };",
|
|
94
105
|
" if (providers) {",
|
|
95
106
|
" result.providers = providers;",
|
|
96
107
|
" }",
|
|
@@ -49,6 +49,7 @@ describe('routing-preamble', () => {
|
|
|
49
49
|
expect(result).toEqual({
|
|
50
50
|
models: ['claude'],
|
|
51
51
|
strategy: 'single',
|
|
52
|
+
mode: 'interactive',
|
|
52
53
|
source: 'shipped-defaults',
|
|
53
54
|
warnings: [],
|
|
54
55
|
});
|
|
@@ -67,6 +68,7 @@ describe('routing-preamble', () => {
|
|
|
67
68
|
expect(result).toEqual({
|
|
68
69
|
models: ['codex', 'claude'],
|
|
69
70
|
strategy: 'parallel',
|
|
71
|
+
mode: 'exec',
|
|
70
72
|
source: 'personal-config',
|
|
71
73
|
warnings: [],
|
|
72
74
|
});
|
|
@@ -90,6 +92,7 @@ describe('routing-preamble', () => {
|
|
|
90
92
|
expect(result).toEqual({
|
|
91
93
|
models: ['gemini'],
|
|
92
94
|
strategy: 'single',
|
|
95
|
+
mode: 'interactive',
|
|
93
96
|
source: 'project-override',
|
|
94
97
|
warnings: [],
|
|
95
98
|
});
|
|
@@ -108,6 +111,7 @@ describe('routing-preamble', () => {
|
|
|
108
111
|
expect(result).toEqual({
|
|
109
112
|
models: ['codex'],
|
|
110
113
|
strategy: 'single',
|
|
114
|
+
mode: 'interactive',
|
|
111
115
|
source: 'personal-config',
|
|
112
116
|
warnings: [expect.stringMatching(/models/i)],
|
|
113
117
|
});
|
|
@@ -126,6 +130,7 @@ describe('routing-preamble', () => {
|
|
|
126
130
|
expect(result).toEqual({
|
|
127
131
|
models: ['codex', 'claude'],
|
|
128
132
|
strategy: 'parallel',
|
|
133
|
+
mode: 'interactive',
|
|
129
134
|
source: 'personal-config',
|
|
130
135
|
warnings: [],
|
|
131
136
|
});
|
|
@@ -150,6 +155,7 @@ describe('routing-preamble', () => {
|
|
|
150
155
|
expect(result).toEqual({
|
|
151
156
|
models: ['codex'],
|
|
152
157
|
strategy: 'single',
|
|
158
|
+
mode: 'interactive',
|
|
153
159
|
source: 'personal-config',
|
|
154
160
|
providers,
|
|
155
161
|
warnings: [],
|
|
@@ -175,6 +181,7 @@ describe('routing-preamble', () => {
|
|
|
175
181
|
expect(result).toEqual({
|
|
176
182
|
models: ['local-model'],
|
|
177
183
|
strategy: 'single',
|
|
184
|
+
mode: 'interactive',
|
|
178
185
|
source: 'flag-override',
|
|
179
186
|
warnings: [],
|
|
180
187
|
});
|
|
@@ -198,6 +205,7 @@ describe('routing-preamble', () => {
|
|
|
198
205
|
expect(JSON.parse(output)).toEqual({
|
|
199
206
|
models: ['claude'],
|
|
200
207
|
strategy: 'single',
|
|
208
|
+
mode: 'interactive',
|
|
201
209
|
source: 'shipped-defaults',
|
|
202
210
|
warnings: [expect.stringContaining(path.join(homeDir, '.tlc', 'config.json'))],
|
|
203
211
|
});
|
|
@@ -220,6 +228,7 @@ describe('routing-preamble', () => {
|
|
|
220
228
|
expect(JSON.parse(output)).toEqual({
|
|
221
229
|
models: ['claude'],
|
|
222
230
|
strategy: 'single',
|
|
231
|
+
mode: 'interactive',
|
|
223
232
|
source: 'shipped-defaults',
|
|
224
233
|
warnings: [expect.stringContaining(path.join(cwd, '.tlc.json'))],
|
|
225
234
|
});
|
|
@@ -248,6 +257,7 @@ describe('routing-preamble', () => {
|
|
|
248
257
|
expect(JSON.parse(output)).toEqual({
|
|
249
258
|
models: ['codex'],
|
|
250
259
|
strategy: 'single',
|
|
260
|
+
mode: 'inline',
|
|
251
261
|
source: 'project-override',
|
|
252
262
|
warnings: [expect.stringMatching(/models/i)],
|
|
253
263
|
});
|
|
@@ -259,8 +269,47 @@ describe('routing-preamble', () => {
|
|
|
259
269
|
expect(result).toEqual({
|
|
260
270
|
models: ['claude'],
|
|
261
271
|
strategy: 'single',
|
|
272
|
+
mode: 'inline',
|
|
262
273
|
source: 'shipped-defaults',
|
|
263
274
|
warnings: [],
|
|
264
275
|
});
|
|
265
276
|
});
|
|
277
|
+
|
|
278
|
+
it('personal config can override mode', () => {
|
|
279
|
+
const result = runGeneratedScript({
|
|
280
|
+
commandName: 'build',
|
|
281
|
+
homeConfig: {
|
|
282
|
+
task_routing: {
|
|
283
|
+
build: { models: ['codex'], strategy: 'single', mode: 'exec' },
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
expect(result).toEqual({
|
|
289
|
+
models: ['codex'],
|
|
290
|
+
strategy: 'single',
|
|
291
|
+
mode: 'exec',
|
|
292
|
+
source: 'personal-config',
|
|
293
|
+
warnings: [],
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('project override can override mode', () => {
|
|
298
|
+
const result = runGeneratedScript({
|
|
299
|
+
commandName: 'review',
|
|
300
|
+
projectConfig: {
|
|
301
|
+
task_routing_override: {
|
|
302
|
+
review: { models: ['claude'], strategy: 'single', mode: 'interactive' },
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
expect(result).toEqual({
|
|
308
|
+
models: ['claude'],
|
|
309
|
+
strategy: 'single',
|
|
310
|
+
mode: 'interactive',
|
|
311
|
+
source: 'project-override',
|
|
312
|
+
warnings: [],
|
|
313
|
+
});
|
|
314
|
+
});
|
|
266
315
|
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
function isFile(fsImpl, targetPath) {
|
|
4
|
+
try {
|
|
5
|
+
return fsImpl.statSync(targetPath).isFile();
|
|
6
|
+
} catch {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function isDirectory(fsImpl, targetPath) {
|
|
12
|
+
try {
|
|
13
|
+
return fsImpl.statSync(targetPath).isDirectory();
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function listFiles(fsImpl, targetDir) {
|
|
20
|
+
try {
|
|
21
|
+
return fsImpl.readdirSync(targetDir, { withFileTypes: true });
|
|
22
|
+
} catch {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readFile(fsImpl, filePath) {
|
|
28
|
+
try {
|
|
29
|
+
return fsImpl.readFileSync(filePath, 'utf8');
|
|
30
|
+
} catch {
|
|
31
|
+
return '';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function makeWorkflow(projectDir, workflowPath, contents) {
|
|
36
|
+
const normalized = String(contents || '').toLowerCase();
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
path: path.relative(projectDir, workflowPath).replace(/\\/g, '/'),
|
|
40
|
+
hasTest: /\b(test|tests|jest|vitest|mocha|pytest|rspec|go test|cargo test|npm test|pnpm test|yarn test)\b/.test(normalized),
|
|
41
|
+
hasDeploy: /\b(deploy|deployment|release|publish|ship|helm upgrade|kubectl apply|terraform apply)\b/.test(normalized),
|
|
42
|
+
hasCoverage: /\b(coverage|codecov|coveralls|lcov|nyc|cobertura)\b/.test(normalized),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function collectGithubWorkflows(projectDir, fsImpl) {
|
|
47
|
+
const workflowsDir = path.join(projectDir, '.github', 'workflows');
|
|
48
|
+
const entries = listFiles(fsImpl, workflowsDir);
|
|
49
|
+
|
|
50
|
+
return entries
|
|
51
|
+
.filter((entry) => entry.isFile() && /\.ya?ml$/i.test(entry.name))
|
|
52
|
+
.map((entry) => {
|
|
53
|
+
const workflowPath = path.join(workflowsDir, entry.name);
|
|
54
|
+
return makeWorkflow(projectDir, workflowPath, readFile(fsImpl, workflowPath));
|
|
55
|
+
})
|
|
56
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function collectCircleciWorkflows(projectDir, fsImpl) {
|
|
60
|
+
const circleDir = path.join(projectDir, '.circleci');
|
|
61
|
+
if (!isDirectory(fsImpl, circleDir)) {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const entries = listFiles(fsImpl, circleDir).filter((entry) => entry.isFile());
|
|
66
|
+
if (entries.length === 0) {
|
|
67
|
+
return [
|
|
68
|
+
{
|
|
69
|
+
path: '.circleci/',
|
|
70
|
+
hasTest: false,
|
|
71
|
+
hasDeploy: false,
|
|
72
|
+
hasCoverage: false,
|
|
73
|
+
},
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return entries
|
|
78
|
+
.map((entry) => {
|
|
79
|
+
const workflowPath = path.join(circleDir, entry.name);
|
|
80
|
+
return makeWorkflow(projectDir, workflowPath, readFile(fsImpl, workflowPath));
|
|
81
|
+
})
|
|
82
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function detectCI({ projectDir, fs: fsImpl }) {
|
|
86
|
+
const githubWorkflows = collectGithubWorkflows(projectDir, fsImpl);
|
|
87
|
+
const jenkinsPath = path.join(projectDir, 'Jenkinsfile');
|
|
88
|
+
const gitlabPath = path.join(projectDir, '.gitlab-ci.yml');
|
|
89
|
+
const circleciWorkflows = collectCircleciWorkflows(projectDir, fsImpl);
|
|
90
|
+
|
|
91
|
+
let platform = 'none';
|
|
92
|
+
let workflows = [];
|
|
93
|
+
|
|
94
|
+
if (githubWorkflows.length > 0) {
|
|
95
|
+
platform = 'github-actions';
|
|
96
|
+
workflows = githubWorkflows;
|
|
97
|
+
} else if (isFile(fsImpl, jenkinsPath)) {
|
|
98
|
+
platform = 'jenkins';
|
|
99
|
+
workflows = [makeWorkflow(projectDir, jenkinsPath, readFile(fsImpl, jenkinsPath))];
|
|
100
|
+
} else if (isFile(fsImpl, gitlabPath)) {
|
|
101
|
+
platform = 'gitlab';
|
|
102
|
+
workflows = [makeWorkflow(projectDir, gitlabPath, readFile(fsImpl, gitlabPath))];
|
|
103
|
+
} else if (circleciWorkflows.length > 0) {
|
|
104
|
+
platform = 'circleci';
|
|
105
|
+
workflows = circleciWorkflows;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (platform === 'none') {
|
|
109
|
+
return {
|
|
110
|
+
platform,
|
|
111
|
+
workflows: [],
|
|
112
|
+
gaps: ['no-ci'],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const gaps = [];
|
|
117
|
+
|
|
118
|
+
if (!workflows.some((workflow) => workflow.hasTest)) {
|
|
119
|
+
gaps.push('missing-test-step');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!workflows.some((workflow) => workflow.hasDeploy)) {
|
|
123
|
+
gaps.push('missing-deploy-step');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!workflows.some((workflow) => workflow.hasCoverage)) {
|
|
127
|
+
gaps.push('missing-coverage-upload');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
platform,
|
|
132
|
+
workflows,
|
|
133
|
+
gaps,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = {
|
|
138
|
+
detectCI,
|
|
139
|
+
};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const { detectCI } = require('./ci-detector');
|
|
8
|
+
|
|
9
|
+
function makeTempProject() {
|
|
10
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'ci-detector-test-'));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function writeFile(projectDir, relativePath, contents = '') {
|
|
14
|
+
const filePath = path.join(projectDir, relativePath);
|
|
15
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
16
|
+
fs.writeFileSync(filePath, contents);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function makeDir(projectDir, relativePath) {
|
|
20
|
+
fs.mkdirSync(path.join(projectDir, relativePath), { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const tempDirs = [];
|
|
24
|
+
|
|
25
|
+
function createProject() {
|
|
26
|
+
const projectDir = makeTempProject();
|
|
27
|
+
tempDirs.push(projectDir);
|
|
28
|
+
return projectDir;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
while (tempDirs.length > 0) {
|
|
33
|
+
fs.rmSync(tempDirs.pop(), { recursive: true, force: true });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('detectCI', () => {
|
|
38
|
+
it('returns no-ci when no CI config exists', () => {
|
|
39
|
+
const projectDir = createProject();
|
|
40
|
+
|
|
41
|
+
expect(detectCI({ projectDir, fs })).toEqual({
|
|
42
|
+
platform: 'none',
|
|
43
|
+
workflows: [],
|
|
44
|
+
gaps: ['no-ci'],
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('detects GitHub Actions workflows and aggregates missing gaps across files', () => {
|
|
49
|
+
const projectDir = createProject();
|
|
50
|
+
writeFile(
|
|
51
|
+
projectDir,
|
|
52
|
+
'.github/workflows/test.yml',
|
|
53
|
+
[
|
|
54
|
+
'name: Test',
|
|
55
|
+
'jobs:',
|
|
56
|
+
' test:',
|
|
57
|
+
' steps:',
|
|
58
|
+
' - run: npm test',
|
|
59
|
+
' - run: npx vitest --coverage',
|
|
60
|
+
].join('\n')
|
|
61
|
+
);
|
|
62
|
+
writeFile(
|
|
63
|
+
projectDir,
|
|
64
|
+
'.github/workflows/deploy.yml',
|
|
65
|
+
[
|
|
66
|
+
'name: Deploy',
|
|
67
|
+
'jobs:',
|
|
68
|
+
' deploy:',
|
|
69
|
+
' steps:',
|
|
70
|
+
' - run: npm run deploy',
|
|
71
|
+
' - uses: codecov/codecov-action@v4',
|
|
72
|
+
].join('\n')
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
expect(detectCI({ projectDir, fs })).toEqual({
|
|
76
|
+
platform: 'github-actions',
|
|
77
|
+
workflows: [
|
|
78
|
+
{
|
|
79
|
+
path: '.github/workflows/deploy.yml',
|
|
80
|
+
hasTest: false,
|
|
81
|
+
hasDeploy: true,
|
|
82
|
+
hasCoverage: true,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
path: '.github/workflows/test.yml',
|
|
86
|
+
hasTest: true,
|
|
87
|
+
hasDeploy: false,
|
|
88
|
+
hasCoverage: true,
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
gaps: [],
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('detects Jenkinsfile and reports missing coverage upload', () => {
|
|
96
|
+
const projectDir = createProject();
|
|
97
|
+
writeFile(
|
|
98
|
+
projectDir,
|
|
99
|
+
'Jenkinsfile',
|
|
100
|
+
[
|
|
101
|
+
'pipeline {',
|
|
102
|
+
' stages {',
|
|
103
|
+
' stage("Test") { steps { sh "npm test" } }',
|
|
104
|
+
' stage("Deploy") { steps { sh "npm run deploy" } }',
|
|
105
|
+
' }',
|
|
106
|
+
'}',
|
|
107
|
+
].join('\n')
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
expect(detectCI({ projectDir, fs })).toEqual({
|
|
111
|
+
platform: 'jenkins',
|
|
112
|
+
workflows: [
|
|
113
|
+
{
|
|
114
|
+
path: 'Jenkinsfile',
|
|
115
|
+
hasTest: true,
|
|
116
|
+
hasDeploy: true,
|
|
117
|
+
hasCoverage: false,
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
gaps: ['missing-coverage-upload'],
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('detects GitLab CI and reports missing deploy and coverage steps', () => {
|
|
125
|
+
const projectDir = createProject();
|
|
126
|
+
writeFile(
|
|
127
|
+
projectDir,
|
|
128
|
+
'.gitlab-ci.yml',
|
|
129
|
+
[
|
|
130
|
+
'stages:',
|
|
131
|
+
' - test',
|
|
132
|
+
'test:',
|
|
133
|
+
' script:',
|
|
134
|
+
' - pytest',
|
|
135
|
+
].join('\n')
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
expect(detectCI({ projectDir, fs })).toEqual({
|
|
139
|
+
platform: 'gitlab',
|
|
140
|
+
workflows: [
|
|
141
|
+
{
|
|
142
|
+
path: '.gitlab-ci.yml',
|
|
143
|
+
hasTest: true,
|
|
144
|
+
hasDeploy: false,
|
|
145
|
+
hasCoverage: false,
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
gaps: ['missing-deploy-step', 'missing-coverage-upload'],
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('detects CircleCI from the directory and workflow file', () => {
|
|
153
|
+
const projectDir = createProject();
|
|
154
|
+
writeFile(
|
|
155
|
+
projectDir,
|
|
156
|
+
'.circleci/config.yml',
|
|
157
|
+
[
|
|
158
|
+
'version: 2.1',
|
|
159
|
+
'jobs:',
|
|
160
|
+
' build:',
|
|
161
|
+
' steps:',
|
|
162
|
+
' - run: yarn test',
|
|
163
|
+
' - run: yarn coverage',
|
|
164
|
+
].join('\n')
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
expect(detectCI({ projectDir, fs })).toEqual({
|
|
168
|
+
platform: 'circleci',
|
|
169
|
+
workflows: [
|
|
170
|
+
{
|
|
171
|
+
path: '.circleci/config.yml',
|
|
172
|
+
hasTest: true,
|
|
173
|
+
hasDeploy: false,
|
|
174
|
+
hasCoverage: true,
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
gaps: ['missing-deploy-step'],
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('treats an empty .circleci directory as CI with all gaps missing', () => {
|
|
182
|
+
const projectDir = createProject();
|
|
183
|
+
makeDir(projectDir, '.circleci');
|
|
184
|
+
|
|
185
|
+
expect(detectCI({ projectDir, fs })).toEqual({
|
|
186
|
+
platform: 'circleci',
|
|
187
|
+
workflows: [
|
|
188
|
+
{
|
|
189
|
+
path: '.circleci/',
|
|
190
|
+
hasTest: false,
|
|
191
|
+
hasDeploy: false,
|
|
192
|
+
hasCoverage: false,
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
gaps: ['missing-test-step', 'missing-deploy-step', 'missing-coverage-upload'],
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
});
|