faf-taf-git 2.4.0 → 2.4.2
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/CHANGELOG.md +23 -1
- package/README.md +1 -1
- package/RECEIPT-FORMAT.md +1 -1
- package/dist/cli.d.ts +5 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +14 -1
- package/dist/cli.js.map +1 -1
- package/package.json +15 -2
- package/project.faf +32 -242
- package/.faf-dna +0 -95
- package/.fafignore +0 -41
- package/.github/dependabot.yml +0 -10
- package/.github/workflows/ci.yml +0 -53
- package/.github/workflows/claude-code-review.yml +0 -36
- package/.github/workflows/claude.yml +0 -39
- package/.github/workflows/homebrew.yml +0 -19
- package/.taf +0 -98
- package/CLAUDE.md +0 -38
- package/CODE_OF_CONDUCT.md +0 -21
- package/CONTRIBUTING.md +0 -289
- package/SECURITY.md +0 -19
- package/action.yml +0 -50
- package/agent.fafa +0 -53
- package/badge.svg +0 -21
- package/dist/bundle/index.js +0 -36554
- package/dist/index.js +0 -42827
- package/dist/index.js.map +0 -1
- package/dist/licenses.txt +0 -126
- package/dist/sourcemap-register.js +0 -1
- package/docs/RELEASE-NOTES.md +0 -200
- package/docs/USER-GUIDE.md +0 -301
- package/jest.config.js +0 -13
- package/src/badge.ts +0 -138
- package/src/cli.ts +0 -297
- package/src/index.ts +0 -201
- package/src/parsers/bun.ts +0 -103
- package/src/parsers/index.ts +0 -50
- package/src/parsers/jest.ts +0 -104
- package/src/parsers/junit.ts +0 -110
- package/src/parsers/vitest.ts +0 -65
- package/src/parsers/wjttc.ts +0 -160
- package/src/taf-core.ts +0 -139
- package/test-mcp.ts +0 -83
- package/test-standalone.ts +0 -87
- package/tests/action-bundle.test.ts +0 -34
- package/tests/action-parse-error.test.ts +0 -47
- package/tests/badge.test.ts +0 -260
- package/tests/bun-parser.test.ts +0 -173
- package/tests/jest-parser.test.ts +0 -122
- package/tests/junit-parser.test.ts +0 -109
- package/tests/vitest-parser.test.ts +0 -112
- package/tests/wjttc-parser.test.ts +0 -116
- package/tests/wjttc-taf.test.ts +0 -470
- package/tsconfig.json +0 -18
package/src/cli.ts
DELETED
|
@@ -1,297 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* faf-taf-git - The Git-Native Receipt Printer
|
|
4
|
-
*
|
|
5
|
-
* v2.x CLI: reads test output from file (pre-capture pattern)
|
|
6
|
-
* Platform-agnostic - works in ANY CI/CD environment or locally
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { execSync } from 'child_process';
|
|
10
|
-
import * as fs from 'fs';
|
|
11
|
-
import * as path from 'path';
|
|
12
|
-
import { parseTestOutput, UNPARSEABLE_OUTPUT } from './parsers';
|
|
13
|
-
import { updateTafFile } from './taf-core';
|
|
14
|
-
import { generateBadge } from './badge';
|
|
15
|
-
|
|
16
|
-
export interface CLIOptions {
|
|
17
|
-
file?: string;
|
|
18
|
-
autoCommit?: boolean;
|
|
19
|
-
commitMessage?: string;
|
|
20
|
-
cwd?: string;
|
|
21
|
-
verbose?: boolean;
|
|
22
|
-
logger?: (message: string) => void;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface CLIResult {
|
|
26
|
-
success: boolean;
|
|
27
|
-
testResults?: {
|
|
28
|
-
total: number;
|
|
29
|
-
passed: number;
|
|
30
|
-
failed: number;
|
|
31
|
-
result: string;
|
|
32
|
-
};
|
|
33
|
-
tafUpdated: boolean;
|
|
34
|
-
error?: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Main CLI function - reads test output from file
|
|
39
|
-
*/
|
|
40
|
-
export async function runTafGit(options: CLIOptions = {}): Promise<CLIResult> {
|
|
41
|
-
const {
|
|
42
|
-
file,
|
|
43
|
-
autoCommit = false,
|
|
44
|
-
commitMessage = 'chore: update .taf with test results',
|
|
45
|
-
cwd = process.cwd(),
|
|
46
|
-
verbose = false,
|
|
47
|
-
logger = console.log,
|
|
48
|
-
} = options;
|
|
49
|
-
|
|
50
|
-
try {
|
|
51
|
-
if (!file) {
|
|
52
|
-
return {
|
|
53
|
-
success: false,
|
|
54
|
-
tafUpdated: false,
|
|
55
|
-
error: 'No test output file specified. Use --file <path>.\nExample: npm test 2>&1 | tee test-output.txt && npx faf-taf-git --file test-output.txt',
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Resolve file path
|
|
60
|
-
const filePath = path.isAbsolute(file) ? file : path.join(cwd, file);
|
|
61
|
-
|
|
62
|
-
if (!fs.existsSync(filePath)) {
|
|
63
|
-
return {
|
|
64
|
-
success: false,
|
|
65
|
-
tafUpdated: false,
|
|
66
|
-
error: `Test output file not found: ${filePath}`,
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const output = fs.readFileSync(filePath, 'utf-8');
|
|
71
|
-
if (verbose) logger(`Read ${output.length} bytes from ${filePath}`);
|
|
72
|
-
|
|
73
|
-
// Parse test output
|
|
74
|
-
const testResults = parseTestOutput(output);
|
|
75
|
-
|
|
76
|
-
if (!testResults) {
|
|
77
|
-
if (verbose) {
|
|
78
|
-
logger(`Parser returned null`);
|
|
79
|
-
logger(`Output sample (first 500 chars): ${output.slice(0, 500)}`);
|
|
80
|
-
}
|
|
81
|
-
return {
|
|
82
|
-
success: false,
|
|
83
|
-
tafUpdated: false,
|
|
84
|
-
error: UNPARSEABLE_OUTPUT,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (verbose) {
|
|
89
|
-
logger(`Parsed results: ${testResults.passed}/${testResults.total} tests passing`);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Check if .taf file exists
|
|
93
|
-
const tafPath = path.join(cwd, '.taf');
|
|
94
|
-
if (!fs.existsSync(tafPath)) {
|
|
95
|
-
return {
|
|
96
|
-
success: false,
|
|
97
|
-
testResults: {
|
|
98
|
-
total: testResults.total,
|
|
99
|
-
passed: testResults.passed,
|
|
100
|
-
failed: testResults.failed,
|
|
101
|
-
result: testResults.result,
|
|
102
|
-
},
|
|
103
|
-
tafUpdated: false,
|
|
104
|
-
error: 'No .taf file found. Run `faf taf init` to create one.',
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Update .taf file
|
|
109
|
-
const updated = await updateTafFile(tafPath, testResults);
|
|
110
|
-
|
|
111
|
-
if (updated) {
|
|
112
|
-
if (verbose) logger('.taf file updated successfully');
|
|
113
|
-
|
|
114
|
-
// Commit changes if enabled
|
|
115
|
-
if (autoCommit) {
|
|
116
|
-
commitTafUpdate(cwd, commitMessage, verbose, logger);
|
|
117
|
-
if (verbose) logger('Changes committed to git');
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return {
|
|
121
|
-
success: true,
|
|
122
|
-
testResults: {
|
|
123
|
-
total: testResults.total,
|
|
124
|
-
passed: testResults.passed,
|
|
125
|
-
failed: testResults.failed,
|
|
126
|
-
result: testResults.result,
|
|
127
|
-
},
|
|
128
|
-
tafUpdated: true,
|
|
129
|
-
};
|
|
130
|
-
} else {
|
|
131
|
-
return {
|
|
132
|
-
success: false,
|
|
133
|
-
testResults: {
|
|
134
|
-
total: testResults.total,
|
|
135
|
-
passed: testResults.passed,
|
|
136
|
-
failed: testResults.failed,
|
|
137
|
-
result: testResults.result,
|
|
138
|
-
},
|
|
139
|
-
tafUpdated: false,
|
|
140
|
-
error: '.taf file was not updated',
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
} catch (error) {
|
|
144
|
-
return {
|
|
145
|
-
success: false,
|
|
146
|
-
tafUpdated: false,
|
|
147
|
-
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Commit .taf file changes to git
|
|
154
|
-
*/
|
|
155
|
-
function commitTafUpdate(cwd: string, message: string, verbose: boolean, logger: (msg: string) => void = console.log): void {
|
|
156
|
-
try {
|
|
157
|
-
const opts = { cwd, stdio: 'pipe' as const };
|
|
158
|
-
|
|
159
|
-
execSync('git add .taf', opts);
|
|
160
|
-
|
|
161
|
-
const diff = execSync('git diff --cached --name-only', opts).toString().trim();
|
|
162
|
-
if (diff.length === 0) {
|
|
163
|
-
if (verbose) logger('No changes to commit');
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
execSync(`git commit -m "${message}"`, opts);
|
|
168
|
-
|
|
169
|
-
try {
|
|
170
|
-
execSync('git push', opts);
|
|
171
|
-
} catch {
|
|
172
|
-
if (verbose) logger('Could not push changes (this is ok in some CI environments)');
|
|
173
|
-
}
|
|
174
|
-
} catch (error) {
|
|
175
|
-
if (verbose) logger(`Error committing changes: ${error}`);
|
|
176
|
-
throw error;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Handle the 'badge' subcommand
|
|
182
|
-
*/
|
|
183
|
-
function handleBadgeCommand(args: string[]): void {
|
|
184
|
-
if (args.includes('--help') || args.includes('-h')) {
|
|
185
|
-
console.log(`\nfaf-taf-git badge - Generate SVG badge from .taf history\n\nUSAGE:\n npx faf-taf-git badge [OPTIONS]\n\nOPTIONS:\n --output <path> Write SVG to file (default: stdout)\n --label <text> Badge label (default: TAF)\n --taf <path> Path to .taf file (default: .taf)\n --help, -h Show this help\n`);
|
|
186
|
-
process.exit(0);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Parse --output
|
|
190
|
-
let outputPath: string | undefined;
|
|
191
|
-
const outputIndex = args.indexOf('--output');
|
|
192
|
-
if (outputIndex !== -1 && args[outputIndex + 1]) {
|
|
193
|
-
outputPath = args[outputIndex + 1];
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Parse --label
|
|
197
|
-
let label: string | undefined;
|
|
198
|
-
const labelIndex = args.indexOf('--label');
|
|
199
|
-
if (labelIndex !== -1 && args[labelIndex + 1]) {
|
|
200
|
-
label = args[labelIndex + 1];
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Parse --taf
|
|
204
|
-
let tafPath: string | undefined;
|
|
205
|
-
const tafIndex = args.indexOf('--taf');
|
|
206
|
-
if (tafIndex !== -1 && args[tafIndex + 1]) {
|
|
207
|
-
tafPath = args[tafIndex + 1];
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const result = generateBadge({ tafPath, label });
|
|
211
|
-
|
|
212
|
-
if (!result.success || !result.svg) {
|
|
213
|
-
console.error(`Error: ${result.error || 'Failed to generate badge'}`);
|
|
214
|
-
process.exit(1);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (outputPath) {
|
|
218
|
-
fs.writeFileSync(outputPath, result.svg, 'utf-8');
|
|
219
|
-
console.log(`Badge written to ${outputPath}`);
|
|
220
|
-
} else {
|
|
221
|
-
process.stdout.write(result.svg);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* CLI entry point
|
|
227
|
-
*/
|
|
228
|
-
async function main() {
|
|
229
|
-
const args = process.argv.slice(2);
|
|
230
|
-
|
|
231
|
-
// Subcommand: badge
|
|
232
|
-
if (args[0] === 'badge') {
|
|
233
|
-
handleBadgeCommand(args.slice(1));
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// Show help
|
|
238
|
-
if (args.includes('--help') || args.includes('-h')) {
|
|
239
|
-
console.log(`\nfaf-taf-git - The Git-Native Receipt Printer\n\nUSAGE:\n npm test 2>&1 | tee test-output.txt\n npx faf-taf-git --file test-output.txt [OPTIONS]\n\n # WJTTC / pc-ai bar\n node tests/wjttc/run-wjttc.mjs 2>&1 | tee wjttc-out.txt\n npx faf-taf-git --file wjttc-out.txt\n\nCOMMANDS:\n badge Generate a shields.io-style SVG badge from .taf\n\nOPTIONS:\n --file <path> Path to test output file (required)\n --commit Auto-commit .taf changes to git\n --message <msg> Custom commit message\n --cwd <dir> Working directory (default: current)\n --verbose, -v Verbose output\n --help, -h Show this help\n\nPARSERS:\n JUnit XML (pytest, cargo, Go, Java, .NET ...), Bun, WJTTC (pc-ai bar), Jest, Vitest\n\nEXAMPLES:\n npm test 2>&1 | tee test-output.txt\n npx faf-taf-git --file test-output.txt\n\n npx faf-taf-git --file test-output.txt --commit\n\n npx faf-taf-git badge --output badge.svg\n\nPLATFORM SUPPORT:\n Works in ANY CI/CD that runs Node.js:\n GitHub Actions, GitLab CI, Jenkins, CircleCI,\n Bitbucket Pipelines, Azure Pipelines, local dev\n\nLEARN MORE:\n https://github.com/Wolfe-Jam/faf-taf-git\n`);
|
|
240
|
-
process.exit(0);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const options: CLIOptions = {
|
|
244
|
-
verbose: args.includes('--verbose') || args.includes('-v'),
|
|
245
|
-
};
|
|
246
|
-
|
|
247
|
-
// Parse --file
|
|
248
|
-
const fileIndex = args.indexOf('--file');
|
|
249
|
-
if (fileIndex !== -1 && args[fileIndex + 1]) {
|
|
250
|
-
options.file = args[fileIndex + 1];
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Parse auto-commit flag
|
|
254
|
-
if (args.includes('--commit')) {
|
|
255
|
-
options.autoCommit = true;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Parse commit message
|
|
259
|
-
const messageIndex = args.indexOf('--message');
|
|
260
|
-
if (messageIndex !== -1 && args[messageIndex + 1]) {
|
|
261
|
-
options.commitMessage = args[messageIndex + 1];
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Parse working directory
|
|
265
|
-
const cwdIndex = args.indexOf('--cwd');
|
|
266
|
-
if (cwdIndex !== -1 && args[cwdIndex + 1]) {
|
|
267
|
-
options.cwd = args[cwdIndex + 1];
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Run the tool
|
|
271
|
-
const result = await runTafGit(options);
|
|
272
|
-
|
|
273
|
-
if (result.success) {
|
|
274
|
-
if (result.testResults) {
|
|
275
|
-
console.log(`Tests: ${result.testResults.passed}/${result.testResults.total} passing`);
|
|
276
|
-
console.log(`Result: ${result.testResults.result}`);
|
|
277
|
-
}
|
|
278
|
-
if (result.tafUpdated) {
|
|
279
|
-
console.log('.taf file updated');
|
|
280
|
-
}
|
|
281
|
-
process.exit(0);
|
|
282
|
-
} else {
|
|
283
|
-
console.error(`Error: ${result.error}`);
|
|
284
|
-
if (result.testResults) {
|
|
285
|
-
console.log(`Tests: ${result.testResults.passed}/${result.testResults.total} passing`);
|
|
286
|
-
}
|
|
287
|
-
process.exit(1);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// Run CLI if called directly
|
|
292
|
-
if (require.main === module) {
|
|
293
|
-
main().catch((error) => {
|
|
294
|
-
console.error('Fatal error:', error);
|
|
295
|
-
process.exit(1);
|
|
296
|
-
});
|
|
297
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* faf-taf-git v2.0.0 - GitHub Actions wrapper
|
|
3
|
-
*
|
|
4
|
-
* Pre-Capture Pattern: Reads test output from file (written by previous step)
|
|
5
|
-
* Handles GitHub Actions-specific inputs/outputs
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import * as core from '@actions/core';
|
|
9
|
-
import * as exec from '@actions/exec';
|
|
10
|
-
import * as fs from 'fs';
|
|
11
|
-
import * as path from 'path';
|
|
12
|
-
import { parseTestOutput, UNPARSEABLE_OUTPUT } from './parsers';
|
|
13
|
-
import { updateTafFile } from './taf-core';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Switch to a dedicated target branch for TAF receipts.
|
|
17
|
-
* Creates the branch as an orphan on first run, seeded with existing .taf.
|
|
18
|
-
*/
|
|
19
|
-
async function switchToTargetBranch(cwd: string, targetBranch: string, tafSeed: string | null): Promise<void> {
|
|
20
|
-
const execOptions = { cwd };
|
|
21
|
-
|
|
22
|
-
// Configure git (needed before any git operations)
|
|
23
|
-
await exec.exec('git', ['config', '--global', 'user.name', 'faf-taf-git[bot]'], execOptions);
|
|
24
|
-
await exec.exec('git', ['config', '--global', 'user.email', 'faf-taf-git[bot]@users.noreply.github.com'], execOptions);
|
|
25
|
-
|
|
26
|
-
// Check if target branch exists on remote
|
|
27
|
-
const { exitCode } = await exec.getExecOutput(
|
|
28
|
-
'git', ['ls-remote', '--exit-code', 'origin', targetBranch],
|
|
29
|
-
{ ...execOptions, ignoreReturnCode: true }
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
if (exitCode === 0) {
|
|
33
|
-
// Branch exists — fetch and checkout
|
|
34
|
-
// Stash any working tree changes (e.g. dist/ from build step) to allow clean checkout
|
|
35
|
-
await exec.exec('git', ['stash', '--include-untracked'], execOptions);
|
|
36
|
-
await exec.exec('git', ['fetch', 'origin', targetBranch], execOptions);
|
|
37
|
-
await exec.exec('git', ['checkout', targetBranch], execOptions);
|
|
38
|
-
core.info(`Switched to existing branch: ${targetBranch}`);
|
|
39
|
-
} else {
|
|
40
|
-
// Branch doesn't exist — create orphan
|
|
41
|
-
await exec.exec('git', ['checkout', '--orphan', targetBranch], execOptions);
|
|
42
|
-
await exec.getExecOutput('git', ['rm', '-rf', '.'], { ...execOptions, ignoreReturnCode: true });
|
|
43
|
-
|
|
44
|
-
const tafPath = path.join(cwd, '.taf');
|
|
45
|
-
|
|
46
|
-
if (tafSeed) {
|
|
47
|
-
// Seed with existing .taf from source branch
|
|
48
|
-
fs.writeFileSync(tafPath, tafSeed, 'utf-8');
|
|
49
|
-
core.info(`Seeded ${targetBranch} with existing .taf data`);
|
|
50
|
-
} else {
|
|
51
|
-
// No seed — create minimal valid YAML so updateTafFile can populate test_history.
|
|
52
|
-
// Without this, `git add .taf` fails with exit 128 (pathspec did not match)
|
|
53
|
-
// when consumer repo has no .taf on source branch.
|
|
54
|
-
const projectName = path.basename(cwd);
|
|
55
|
-
const minimal =
|
|
56
|
-
`format_version: 1.0.0\n` +
|
|
57
|
-
`project: ${projectName}\n` +
|
|
58
|
-
`created: ${new Date().toISOString()}\n` +
|
|
59
|
-
`test_history: []\n`;
|
|
60
|
-
fs.writeFileSync(tafPath, minimal, 'utf-8');
|
|
61
|
-
core.info(`Created empty .taf for new ${targetBranch} branch (no seed)`);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
await exec.exec('git', ['add', '.taf'], execOptions);
|
|
65
|
-
await exec.exec('git', ['commit', '-m', `chore(taf): initialize ${targetBranch} branch [skip ci]`], execOptions);
|
|
66
|
-
core.info(`Created new branch: ${targetBranch}`);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Commit .taf file changes to git and push
|
|
72
|
-
*/
|
|
73
|
-
async function commitTafUpdate(cwd: string, message: string, targetBranch?: string): Promise<void> {
|
|
74
|
-
const execOptions = { cwd };
|
|
75
|
-
|
|
76
|
-
// Configure git
|
|
77
|
-
await exec.exec('git', ['config', '--global', 'user.name', 'faf-taf-git[bot]'], execOptions);
|
|
78
|
-
await exec.exec('git', ['config', '--global', 'user.email', 'faf-taf-git[bot]@users.noreply.github.com'], execOptions);
|
|
79
|
-
|
|
80
|
-
// Stage .taf file
|
|
81
|
-
await exec.exec('git', ['add', '.taf'], execOptions);
|
|
82
|
-
|
|
83
|
-
// Check if there are changes to commit
|
|
84
|
-
const { exitCode } = await exec.getExecOutput('git', ['diff', '--cached', '--name-only'], execOptions);
|
|
85
|
-
|
|
86
|
-
if (exitCode === 0) {
|
|
87
|
-
await exec.exec('git', ['commit', '-m', message], execOptions);
|
|
88
|
-
|
|
89
|
-
// Push to target branch or current branch
|
|
90
|
-
const pushArgs = targetBranch ? ['push', 'origin', targetBranch] : ['push'];
|
|
91
|
-
try {
|
|
92
|
-
await exec.exec('git', pushArgs, execOptions);
|
|
93
|
-
} catch (error) {
|
|
94
|
-
core.warning('Could not push changes (this is ok in some CI environments)');
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async function run(): Promise<void> {
|
|
100
|
-
try {
|
|
101
|
-
// Get GitHub Action inputs
|
|
102
|
-
const testOutputFile = core.getInput('test-output-file', { required: true });
|
|
103
|
-
const autoCommit = core.getInput('auto-commit') !== 'false';
|
|
104
|
-
const commitMessage = core.getInput('commit-message') || 'chore(taf): update .taf receipt [skip ci]';
|
|
105
|
-
const targetBranch = core.getInput('target-branch') || '';
|
|
106
|
-
const cwd = process.cwd();
|
|
107
|
-
|
|
108
|
-
core.info(`Reading test output from: ${testOutputFile}`);
|
|
109
|
-
|
|
110
|
-
// Read test output file (written by previous CI step)
|
|
111
|
-
const testOutputPath = path.isAbsolute(testOutputFile)
|
|
112
|
-
? testOutputFile
|
|
113
|
-
: path.join(cwd, testOutputFile);
|
|
114
|
-
|
|
115
|
-
if (!fs.existsSync(testOutputPath)) {
|
|
116
|
-
core.setFailed(`Test output file not found: ${testOutputPath}`);
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const testOutput = fs.readFileSync(testOutputPath, 'utf-8');
|
|
121
|
-
core.info(`Read ${testOutput.length} bytes from test output file`);
|
|
122
|
-
|
|
123
|
-
// Debug: Show sample of what we're parsing
|
|
124
|
-
const lines = testOutput.split('\n');
|
|
125
|
-
const testSummaryLine = lines.find(line => line.includes('Tests:'));
|
|
126
|
-
if (testSummaryLine) {
|
|
127
|
-
core.info(`Found summary line: ${testSummaryLine.substring(0, 200)}`);
|
|
128
|
-
} else {
|
|
129
|
-
core.warning('No "Tests:" summary line found in output');
|
|
130
|
-
// Show last 20 lines to help debug
|
|
131
|
-
core.info('Last 20 lines of output:');
|
|
132
|
-
lines.slice(-20).forEach((line, i) => {
|
|
133
|
-
core.info(` ${i}: ${line.substring(0, 150)}`);
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Parse test results
|
|
138
|
-
const testResults = parseTestOutput(testOutput);
|
|
139
|
-
|
|
140
|
-
if (!testResults) {
|
|
141
|
-
core.setFailed(UNPARSEABLE_OUTPUT);
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
core.info(`Parsed results: ${testResults.passed}/${testResults.total} tests passing`);
|
|
146
|
-
|
|
147
|
-
// Set GitHub Action outputs
|
|
148
|
-
core.setOutput('result', testResults.result);
|
|
149
|
-
core.setOutput('passed', testResults.passed.toString());
|
|
150
|
-
core.setOutput('failed', testResults.failed.toString());
|
|
151
|
-
core.setOutput('total', testResults.total.toString());
|
|
152
|
-
core.setOutput('skipped', (testResults.skipped ?? 0).toString());
|
|
153
|
-
|
|
154
|
-
// If target-branch is set, save .taf seed and switch branches
|
|
155
|
-
let tafSeed: string | null = null;
|
|
156
|
-
const tafPath = path.join(cwd, '.taf');
|
|
157
|
-
|
|
158
|
-
if (targetBranch) {
|
|
159
|
-
// Save existing .taf from source branch as seed for first-run
|
|
160
|
-
if (fs.existsSync(tafPath)) {
|
|
161
|
-
tafSeed = fs.readFileSync(tafPath, 'utf-8');
|
|
162
|
-
}
|
|
163
|
-
core.info(`Switching to target branch: ${targetBranch}`);
|
|
164
|
-
await switchToTargetBranch(cwd, targetBranch, tafSeed);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Check if .taf file exists (on current or target branch)
|
|
168
|
-
if (!fs.existsSync(tafPath)) {
|
|
169
|
-
core.setFailed('No .taf file found. Run `faf taf init` to create one.');
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Update .taf file
|
|
174
|
-
const updated = await updateTafFile(tafPath, testResults);
|
|
175
|
-
|
|
176
|
-
if (!updated) {
|
|
177
|
-
core.setOutput('taf-updated', 'false');
|
|
178
|
-
core.setFailed('.taf file was not updated');
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
core.setOutput('taf-updated', 'true');
|
|
183
|
-
core.info('✅ .taf file updated successfully');
|
|
184
|
-
|
|
185
|
-
// Commit changes if enabled
|
|
186
|
-
if (autoCommit) {
|
|
187
|
-
await commitTafUpdate(cwd, commitMessage, targetBranch || undefined);
|
|
188
|
-
core.info(`✅ Changes committed${targetBranch ? ` to ${targetBranch}` : ''}`);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
} catch (error) {
|
|
192
|
-
if (error instanceof Error) {
|
|
193
|
-
core.setFailed(error.message);
|
|
194
|
-
} else {
|
|
195
|
-
core.setFailed('Unknown error occurred');
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Run the GitHub Action
|
|
201
|
-
run();
|
package/src/parsers/bun.ts
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bun test output parser
|
|
3
|
-
*
|
|
4
|
-
* Extracts test counts from `bun test` CLI output.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { TestResults } from './jest';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Parse Bun test output to extract test results.
|
|
11
|
-
*
|
|
12
|
-
* Bun output format (summary block at the end of a run):
|
|
13
|
-
*
|
|
14
|
-
* ```
|
|
15
|
-
* 232 pass
|
|
16
|
-
* 1 skip
|
|
17
|
-
* 1 todo
|
|
18
|
-
* 0 fail
|
|
19
|
-
* 848 expect() calls
|
|
20
|
-
* Ran 234 tests across 9 files. [21.53s]
|
|
21
|
-
* ```
|
|
22
|
-
*
|
|
23
|
-
* Notes:
|
|
24
|
-
* - Anchored on `Ran <N> tests across <M> files.` — the canonical truthful
|
|
25
|
-
* total (includes skip + todo + pass + fail). Without this line, the parser
|
|
26
|
-
* returns null and the dispatcher falls through to jest/vitest.
|
|
27
|
-
* - Per-category counts (`pass`, `fail`, `skip`, `todo`) come from their own
|
|
28
|
-
* lines elsewhere in the same output. Singular forms — bun does not use
|
|
29
|
-
* `passed`/`failed`/`skipped` (intentional differentiator from jest/vitest).
|
|
30
|
-
* - `skipped` aggregates `skip` + `todo` (matches the convention vitest.ts
|
|
31
|
-
* already uses for parity in the shared TestResults shape).
|
|
32
|
-
*/
|
|
33
|
-
export function parseBunOutput(output: string): TestResults | null {
|
|
34
|
-
// Strip ANSI color codes + normalize line endings (parity with siblings).
|
|
35
|
-
// eslint-disable-next-line no-control-regex
|
|
36
|
-
let cleanOutput = output.replace(/\x1b\[[0-9;]*m/g, '');
|
|
37
|
-
cleanOutput = cleanOutput.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
38
|
-
|
|
39
|
-
// Canonical anchor: bun-unique "Ran N tests across M files." summary line.
|
|
40
|
-
// Without this, this isn't bun output — let the dispatcher try the others.
|
|
41
|
-
const ranMatch = cleanOutput.match(/^Ran\s+(\d+)\s+tests?\s+across\s+\d+\s+files?/m);
|
|
42
|
-
if (!ranMatch) return null;
|
|
43
|
-
|
|
44
|
-
const total = parseInt(ranMatch[1], 10);
|
|
45
|
-
if (total === 0) return null;
|
|
46
|
-
|
|
47
|
-
let passed = 0;
|
|
48
|
-
let failed = 0;
|
|
49
|
-
let skipped = 0;
|
|
50
|
-
|
|
51
|
-
// Per-line counts. Each line is `<leading-ws><N> <singular-word>` on its own.
|
|
52
|
-
// Anchored with ^...$ + multiline to avoid catching the "expect() calls" line
|
|
53
|
-
// or any noise inside test descriptions.
|
|
54
|
-
const passedMatch = cleanOutput.match(/^\s*(\d+)\s+pass\s*$/m);
|
|
55
|
-
if (passedMatch) passed = parseInt(passedMatch[1], 10);
|
|
56
|
-
|
|
57
|
-
const failedMatch = cleanOutput.match(/^\s*(\d+)\s+fail\s*$/m);
|
|
58
|
-
if (failedMatch) failed = parseInt(failedMatch[1], 10);
|
|
59
|
-
|
|
60
|
-
const skipMatch = cleanOutput.match(/^\s*(\d+)\s+skip\s*$/m);
|
|
61
|
-
if (skipMatch) skipped += parseInt(skipMatch[1], 10);
|
|
62
|
-
|
|
63
|
-
const todoMatch = cleanOutput.match(/^\s*(\d+)\s+todo\s*$/m);
|
|
64
|
-
if (todoMatch) skipped += parseInt(todoMatch[1], 10);
|
|
65
|
-
|
|
66
|
-
const result: 'PASSED' | 'FAILED' = failed > 0 ? 'FAILED' : 'PASSED';
|
|
67
|
-
|
|
68
|
-
return {
|
|
69
|
-
total,
|
|
70
|
-
passed,
|
|
71
|
-
failed,
|
|
72
|
-
skipped: skipped > 0 ? skipped : undefined,
|
|
73
|
-
result,
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Example Bun outputs for testing:
|
|
79
|
-
*
|
|
80
|
-
* All passing:
|
|
81
|
-
* ```
|
|
82
|
-
* 173 pass
|
|
83
|
-
* 0 fail
|
|
84
|
-
* Ran 173 tests across 8 files. [4.2s]
|
|
85
|
-
* ```
|
|
86
|
-
*
|
|
87
|
-
* Mixed (the grok-faf-mcp 1.4.9 shape):
|
|
88
|
-
* ```
|
|
89
|
-
* 232 pass
|
|
90
|
-
* 1 skip
|
|
91
|
-
* 1 todo
|
|
92
|
-
* 0 fail
|
|
93
|
-
* 848 expect() calls
|
|
94
|
-
* Ran 234 tests across 9 files. [21.53s]
|
|
95
|
-
* ```
|
|
96
|
-
*
|
|
97
|
-
* Failing:
|
|
98
|
-
* ```
|
|
99
|
-
* 170 pass
|
|
100
|
-
* 3 fail
|
|
101
|
-
* Ran 173 tests across 8 files. [5.1s]
|
|
102
|
-
* ```
|
|
103
|
-
*/
|
package/src/parsers/index.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unified test output parser
|
|
3
|
-
*
|
|
4
|
-
* Tries each framework parser in order until one succeeds.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { TestResults } from './jest';
|
|
8
|
-
import { parseBunOutput } from './bun';
|
|
9
|
-
import { parseJestOutput } from './jest';
|
|
10
|
-
import { parseJunitOutput } from './junit';
|
|
11
|
-
import { parseVitestOutput } from './vitest';
|
|
12
|
-
import { parseWjttcOutput } from './wjttc';
|
|
13
|
-
|
|
14
|
-
export { TestResults } from './jest';
|
|
15
|
-
export { parseJunitOutput } from './junit';
|
|
16
|
-
export { parseWjttcOutput } from './wjttc';
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Every parser, in the order parseTestOutput tries them. Order matters:
|
|
20
|
-
* 1. JUnit XML — a `<testsuite>` element; first, because a report's captured
|
|
21
|
-
* stdout can hold another runner's summary line.
|
|
22
|
-
* 2. Bun — `Ran N tests across M files` is bun-unique.
|
|
23
|
-
* 3. WJTTC — `pc-ai WJTTC` / suite `pc-ai-wjttc` (FAF bar; not Jest-shaped).
|
|
24
|
-
* 4. Jest — `Tests: … total`.
|
|
25
|
-
* 5. Vitest — `Tests … (N)`.
|
|
26
|
-
*/
|
|
27
|
-
const PARSERS: ReadonlyArray<readonly [name: string, parse: (output: string) => TestResults | null]> = [
|
|
28
|
-
['JUnit XML', parseJunitOutput],
|
|
29
|
-
['Bun', parseBunOutput],
|
|
30
|
-
['WJTTC', parseWjttcOutput],
|
|
31
|
-
['Jest', parseJestOutput],
|
|
32
|
-
['Vitest', parseVitestOutput],
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
export const SUPPORTED_PARSERS: readonly string[] = PARSERS.map(([name]) => name);
|
|
36
|
-
|
|
37
|
-
/** What the CLI and the Action report when no parser matches. */
|
|
38
|
-
export const UNPARSEABLE_OUTPUT = `Could not parse test output. Supported: ${SUPPORTED_PARSERS.join(', ')}.`;
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Parse test output from any supported framework: the first parser in
|
|
42
|
-
* PARSERS that recognises the output wins.
|
|
43
|
-
*/
|
|
44
|
-
export function parseTestOutput(output: string): TestResults | null {
|
|
45
|
-
for (const [, parse] of PARSERS) {
|
|
46
|
-
const results = parse(output);
|
|
47
|
-
if (results) return results;
|
|
48
|
-
}
|
|
49
|
-
return null;
|
|
50
|
-
}
|