faf-taf-git 2.0.4 → 2.2.1
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/.github/workflows/ci.yml +53 -0
- package/.github/workflows/claude-code-review.yml.disabled +29 -0
- package/.github/workflows/claude.yml +39 -0
- package/.github/workflows/homebrew.yml +19 -0
- package/.taf +79 -2
- package/CHANGELOG.md +28 -0
- package/CLAUDE.md +29 -21
- package/CODE_OF_CONDUCT.md +21 -0
- package/CONTRIBUTING.md +3 -0
- package/README.md +69 -12
- package/SECURITY.md +19 -0
- package/action.yml +6 -1
- package/badge.svg +21 -0
- package/dist/badge.d.ts +23 -0
- package/dist/badge.d.ts.map +1 -0
- package/dist/badge.js +147 -0
- package/dist/badge.js.map +1 -0
- package/dist/cli.d.ts +5 -6
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +144 -193
- package/dist/cli.js.map +1 -1
- package/dist/index.js +26045 -20566
- package/dist/index.js.map +1 -1
- package/dist/licenses.txt +0 -22
- package/dist/parsers/bun.d.ts +59 -0
- package/dist/parsers/bun.d.ts.map +1 -0
- package/dist/parsers/bun.js +100 -0
- package/dist/parsers/bun.js.map +1 -0
- package/dist/parsers/index.d.ts +20 -0
- package/dist/parsers/index.d.ts.map +1 -0
- package/dist/parsers/index.js +26 -0
- package/dist/parsers/index.js.map +1 -0
- package/dist/parsers/vitest.d.ts +17 -0
- package/dist/parsers/vitest.d.ts.map +1 -0
- package/dist/parsers/vitest.js +61 -0
- package/dist/parsers/vitest.js.map +1 -0
- package/package.json +6 -7
- package/project.faf +20 -24
- package/src/badge.ts +138 -0
- package/src/cli.ts +154 -200
- package/src/index.ts +79 -10
- package/src/parsers/bun.ts +103 -0
- package/src/parsers/index.ts +27 -0
- package/src/parsers/vitest.ts +65 -0
- package/tests/badge.test.ts +260 -0
- package/tests/bun-parser.test.ts +173 -0
- package/tests/vitest-parser.test.ts +112 -0
- package/.faf +0 -126
package/src/cli.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* faf-taf-git -
|
|
3
|
+
* faf-taf-git - The Git-Native Receipt Printer
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* v2.x CLI: reads test output from file (pre-capture pattern)
|
|
6
|
+
* Platform-agnostic - works in ANY CI/CD environment or locally
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
10
|
import * as fs from 'fs';
|
|
11
|
-
import * as os from 'os';
|
|
12
11
|
import * as path from 'path';
|
|
13
|
-
import {
|
|
12
|
+
import { parseTestOutput } from './parsers';
|
|
14
13
|
import { updateTafFile } from './taf-core';
|
|
14
|
+
import { generateBadge } from './badge';
|
|
15
15
|
|
|
16
16
|
export interface CLIOptions {
|
|
17
|
-
|
|
17
|
+
file?: string;
|
|
18
18
|
autoCommit?: boolean;
|
|
19
19
|
commitMessage?: string;
|
|
20
20
|
cwd?: string;
|
|
@@ -35,12 +35,11 @@ export interface CLIResult {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* Main CLI function -
|
|
39
|
-
* Can be called from GitHub Actions, GitLab CI, or standalone
|
|
38
|
+
* Main CLI function - reads test output from file
|
|
40
39
|
*/
|
|
41
40
|
export async function runTafGit(options: CLIOptions = {}): Promise<CLIResult> {
|
|
42
41
|
const {
|
|
43
|
-
|
|
42
|
+
file,
|
|
44
43
|
autoCommit = false,
|
|
45
44
|
commitMessage = 'chore: update .taf with test results',
|
|
46
45
|
cwd = process.cwd(),
|
|
@@ -49,125 +48,40 @@ export async function runTafGit(options: CLIOptions = {}): Promise<CLIResult> {
|
|
|
49
48
|
} = options;
|
|
50
49
|
|
|
51
50
|
try {
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
// Parse command into executable and args
|
|
60
|
-
// For "npm test" -> ["npm", "test"]
|
|
61
|
-
const commandParts = command.split(' ');
|
|
62
|
-
const executable = commandParts[0];
|
|
63
|
-
const args = commandParts.slice(1);
|
|
64
|
-
|
|
65
|
-
const options: exec.ExecOptions = {
|
|
66
|
-
cwd,
|
|
67
|
-
silent: false, // Show output in logs
|
|
68
|
-
ignoreReturnCode: true // Don't throw on non-zero exit
|
|
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',
|
|
69
56
|
};
|
|
57
|
+
}
|
|
70
58
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const result = await exec.getExecOutput(executable, args, options);
|
|
74
|
-
|
|
75
|
-
// THE NUCLEAR CANARY: Write directly to disk, bypassing stdout/stderr streams
|
|
76
|
-
// Write to MULTIPLE locations to ensure we find it!
|
|
77
|
-
const canaryContent = [
|
|
78
|
-
`=========================================`,
|
|
79
|
-
`TAF DEBUG CANARY REPORT`,
|
|
80
|
-
`Timestamp: ${new Date().toISOString()}`,
|
|
81
|
-
`CWD: ${cwd}`,
|
|
82
|
-
`Process CWD: ${process.cwd()}`,
|
|
83
|
-
`=========================================`,
|
|
84
|
-
`Exit Code: ${result.exitCode}`,
|
|
85
|
-
`Stdout Length: ${result.stdout.length}`,
|
|
86
|
-
`Stderr Length: ${result.stderr.length}`,
|
|
87
|
-
`-----------------------------------------`,
|
|
88
|
-
`STDOUT PREVIEW (First 500 chars):`,
|
|
89
|
-
`${result.stdout.substring(0, 500)}`,
|
|
90
|
-
`-----------------------------------------`,
|
|
91
|
-
`STDERR PREVIEW (First 500 chars):`,
|
|
92
|
-
`${result.stderr.substring(0, 500)}`,
|
|
93
|
-
`=========================================`
|
|
94
|
-
].join('\n');
|
|
95
|
-
|
|
96
|
-
try {
|
|
97
|
-
// Try multiple locations!
|
|
98
|
-
const locations = [
|
|
99
|
-
path.join(cwd, 'TAF_DEBUG_CANARY.txt'), // Original cwd
|
|
100
|
-
path.join(process.cwd(), 'TAF_DEBUG_CANARY.txt'), // Process cwd
|
|
101
|
-
path.join(os.tmpdir(), 'TAF_DEBUG_CANARY.txt'), // OS temp directory
|
|
102
|
-
'TAF_DEBUG_CANARY.txt' // Current directory
|
|
103
|
-
];
|
|
104
|
-
|
|
105
|
-
let writeSuccess = false;
|
|
106
|
-
for (const loc of locations) {
|
|
107
|
-
try {
|
|
108
|
-
fs.writeFileSync(loc, canaryContent);
|
|
109
|
-
writeSuccess = true;
|
|
110
|
-
} catch (e) {
|
|
111
|
-
// Try next location
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (!writeSuccess) {
|
|
116
|
-
throw new Error('CRITICAL: Failed to write canary to ANY location!');
|
|
117
|
-
}
|
|
118
|
-
} catch (fsError: any) {
|
|
119
|
-
// If we can't write to disk, we are truly doomed
|
|
120
|
-
throw new Error(`CRITICAL: Failed to write canary file: ${fsError.message}`);
|
|
121
|
-
}
|
|
59
|
+
// Resolve file path
|
|
60
|
+
const filePath = path.isAbsolute(file) ? file : path.join(cwd, file);
|
|
122
61
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (verbose) {
|
|
130
|
-
logger(`Test command failed with error: ${error.message}`);
|
|
131
|
-
logger(`Captured output length: ${output.length} bytes`);
|
|
132
|
-
}
|
|
62
|
+
if (!fs.existsSync(filePath)) {
|
|
63
|
+
return {
|
|
64
|
+
success: false,
|
|
65
|
+
tafUpdated: false,
|
|
66
|
+
error: `Test output file not found: ${filePath}`,
|
|
67
|
+
};
|
|
133
68
|
}
|
|
134
69
|
|
|
135
|
-
|
|
136
|
-
if (verbose) {
|
|
137
|
-
try {
|
|
138
|
-
const fs = require('fs');
|
|
139
|
-
const debugPath = path.join(os.tmpdir(), 'taf-debug-output.txt');
|
|
140
|
-
fs.writeFileSync(debugPath, output);
|
|
141
|
-
logger(`DEBUG: Wrote ${output.length} bytes to ${debugPath}`);
|
|
142
|
-
const lines = output.split('\n');
|
|
143
|
-
const testLine = lines.find(l => l.includes('Tests:'));
|
|
144
|
-
logger(`DEBUG: Output has ${lines.length} lines, Found "Tests:": ${testLine ? 'YES' : 'NO'}`);
|
|
145
|
-
if (testLine) {
|
|
146
|
-
logger(`DEBUG: Tests line: "${testLine}"`);
|
|
147
|
-
} else {
|
|
148
|
-
logger(`DEBUG: Last 10 lines of output:`);
|
|
149
|
-
lines.slice(-10).forEach((line, i) => {
|
|
150
|
-
logger(` ${i}: ${line.substring(0, 100)}`);
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
} catch (err) {
|
|
154
|
-
logger(`DEBUG: Error writing debug file: ${err}`);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
70
|
+
const output = fs.readFileSync(filePath, 'utf-8');
|
|
71
|
+
if (verbose) logger(`Read ${output.length} bytes from ${filePath}`);
|
|
157
72
|
|
|
158
73
|
// Parse test output
|
|
159
|
-
const testResults =
|
|
74
|
+
const testResults = parseTestOutput(output);
|
|
160
75
|
|
|
161
76
|
if (!testResults) {
|
|
162
77
|
if (verbose) {
|
|
163
|
-
logger(`
|
|
164
|
-
logger(`
|
|
165
|
-
logger(`DEBUG: Output sample (last 500 chars): ${output.slice(-500)}`);
|
|
78
|
+
logger(`Parser returned null`);
|
|
79
|
+
logger(`Output sample (first 500 chars): ${output.slice(0, 500)}`);
|
|
166
80
|
}
|
|
167
81
|
return {
|
|
168
82
|
success: false,
|
|
169
83
|
tafUpdated: false,
|
|
170
|
-
error: 'Could not parse test output.
|
|
84
|
+
error: 'Could not parse test output. Supported: Jest, Vitest.',
|
|
171
85
|
};
|
|
172
86
|
}
|
|
173
87
|
|
|
@@ -195,12 +109,12 @@ export async function runTafGit(options: CLIOptions = {}): Promise<CLIResult> {
|
|
|
195
109
|
const updated = await updateTafFile(tafPath, testResults);
|
|
196
110
|
|
|
197
111
|
if (updated) {
|
|
198
|
-
if (verbose) logger('
|
|
112
|
+
if (verbose) logger('.taf file updated successfully');
|
|
199
113
|
|
|
200
114
|
// Commit changes if enabled
|
|
201
115
|
if (autoCommit) {
|
|
202
|
-
|
|
203
|
-
if (verbose) logger('
|
|
116
|
+
commitTafUpdate(cwd, commitMessage, verbose, logger);
|
|
117
|
+
if (verbose) logger('Changes committed to git');
|
|
204
118
|
}
|
|
205
119
|
|
|
206
120
|
return {
|
|
@@ -236,96 +150,115 @@ export async function runTafGit(options: CLIOptions = {}): Promise<CLIResult> {
|
|
|
236
150
|
}
|
|
237
151
|
|
|
238
152
|
/**
|
|
239
|
-
* Commit .taf file changes to git
|
|
153
|
+
* Commit .taf file changes to git
|
|
240
154
|
*/
|
|
241
|
-
|
|
155
|
+
function commitTafUpdate(cwd: string, message: string, verbose: boolean, logger: (msg: string) => void = console.log): void {
|
|
242
156
|
try {
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
// Stage .taf file
|
|
250
|
-
await exec.exec('git', ['add', '.taf'], execOptions);
|
|
251
|
-
|
|
252
|
-
// Check if there are changes to commit
|
|
253
|
-
let diffOutput = '';
|
|
254
|
-
const diffOptions: exec.ExecOptions = {
|
|
255
|
-
cwd,
|
|
256
|
-
listeners: {
|
|
257
|
-
stdout: (data: Buffer) => {
|
|
258
|
-
diffOutput += data.toString();
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
};
|
|
262
|
-
await exec.exec('git', ['diff', '--cached', '--name-only'], diffOptions);
|
|
263
|
-
|
|
264
|
-
if (diffOutput.trim().length > 0) {
|
|
265
|
-
await exec.exec('git', ['commit', '-m', message], execOptions);
|
|
266
|
-
|
|
267
|
-
// Try to push (might fail in some environments, that's ok)
|
|
268
|
-
try {
|
|
269
|
-
await exec.exec('git', ['push'], execOptions);
|
|
270
|
-
} catch (error) {
|
|
271
|
-
if (verbose) {
|
|
272
|
-
logger('Could not push changes (this is ok in some CI environments)');
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
} else {
|
|
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) {
|
|
276
163
|
if (verbose) logger('No changes to commit');
|
|
164
|
+
return;
|
|
277
165
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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)');
|
|
281
173
|
}
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if (verbose) logger(`Error committing changes: ${error}`);
|
|
282
176
|
throw error;
|
|
283
177
|
}
|
|
284
178
|
}
|
|
285
179
|
|
|
286
180
|
/**
|
|
287
|
-
*
|
|
181
|
+
* Handle the 'badge' subcommand
|
|
288
182
|
*/
|
|
289
|
-
|
|
290
|
-
|
|
183
|
+
function handleBadgeCommand(args: string[]): void {
|
|
184
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
185
|
+
console.log(`
|
|
186
|
+
faf-taf-git badge - Generate SVG badge from .taf history
|
|
291
187
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
};
|
|
188
|
+
USAGE:
|
|
189
|
+
npx faf-taf-git badge [OPTIONS]
|
|
295
190
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
191
|
+
OPTIONS:
|
|
192
|
+
--output <path> Write SVG to file (default: stdout)
|
|
193
|
+
--label <text> Badge label (default: TAF)
|
|
194
|
+
--taf <path> Path to .taf file (default: .taf)
|
|
195
|
+
--help, -h Show this help
|
|
196
|
+
`);
|
|
197
|
+
process.exit(0);
|
|
300
198
|
}
|
|
301
199
|
|
|
302
|
-
// Parse
|
|
303
|
-
|
|
304
|
-
|
|
200
|
+
// Parse --output
|
|
201
|
+
let outputPath: string | undefined;
|
|
202
|
+
const outputIndex = args.indexOf('--output');
|
|
203
|
+
if (outputIndex !== -1 && args[outputIndex + 1]) {
|
|
204
|
+
outputPath = args[outputIndex + 1];
|
|
305
205
|
}
|
|
306
206
|
|
|
307
|
-
// Parse
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
207
|
+
// Parse --label
|
|
208
|
+
let label: string | undefined;
|
|
209
|
+
const labelIndex = args.indexOf('--label');
|
|
210
|
+
if (labelIndex !== -1 && args[labelIndex + 1]) {
|
|
211
|
+
label = args[labelIndex + 1];
|
|
311
212
|
}
|
|
312
213
|
|
|
313
|
-
// Parse
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
214
|
+
// Parse --taf
|
|
215
|
+
let tafPath: string | undefined;
|
|
216
|
+
const tafIndex = args.indexOf('--taf');
|
|
217
|
+
if (tafIndex !== -1 && args[tafIndex + 1]) {
|
|
218
|
+
tafPath = args[tafIndex + 1];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const result = generateBadge({ tafPath, label });
|
|
222
|
+
|
|
223
|
+
if (!result.success || !result.svg) {
|
|
224
|
+
console.error(`Error: ${result.error || 'Failed to generate badge'}`);
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (outputPath) {
|
|
229
|
+
fs.writeFileSync(outputPath, result.svg, 'utf-8');
|
|
230
|
+
console.log(`Badge written to ${outputPath}`);
|
|
231
|
+
} else {
|
|
232
|
+
process.stdout.write(result.svg);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* CLI entry point
|
|
238
|
+
*/
|
|
239
|
+
async function main() {
|
|
240
|
+
const args = process.argv.slice(2);
|
|
241
|
+
|
|
242
|
+
// Subcommand: badge
|
|
243
|
+
if (args[0] === 'badge') {
|
|
244
|
+
handleBadgeCommand(args.slice(1));
|
|
245
|
+
return;
|
|
317
246
|
}
|
|
318
247
|
|
|
319
248
|
// Show help
|
|
320
249
|
if (args.includes('--help') || args.includes('-h')) {
|
|
321
250
|
console.log(`
|
|
322
|
-
faf-taf-git -
|
|
251
|
+
faf-taf-git - The Git-Native Receipt Printer
|
|
323
252
|
|
|
324
253
|
USAGE:
|
|
325
|
-
|
|
254
|
+
npm test 2>&1 | tee test-output.txt
|
|
255
|
+
npx faf-taf-git --file test-output.txt [OPTIONS]
|
|
256
|
+
|
|
257
|
+
COMMANDS:
|
|
258
|
+
badge Generate a shields.io-style SVG badge from .taf
|
|
326
259
|
|
|
327
260
|
OPTIONS:
|
|
328
|
-
--
|
|
261
|
+
--file <path> Path to test output file (required)
|
|
329
262
|
--commit Auto-commit .taf changes to git
|
|
330
263
|
--message <msg> Custom commit message
|
|
331
264
|
--cwd <dir> Working directory (default: current)
|
|
@@ -333,29 +266,23 @@ OPTIONS:
|
|
|
333
266
|
--help, -h Show this help
|
|
334
267
|
|
|
335
268
|
EXAMPLES:
|
|
336
|
-
# Run tests
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
# Custom test command
|
|
340
|
-
npx faf-taf-git --command "npm run test:unit"
|
|
269
|
+
# Run tests, capture output, generate receipt
|
|
270
|
+
npm test 2>&1 | tee test-output.txt
|
|
271
|
+
npx faf-taf-git --file test-output.txt
|
|
341
272
|
|
|
342
273
|
# Auto-commit changes
|
|
343
|
-
npx faf-taf-git --commit
|
|
274
|
+
npx faf-taf-git --file test-output.txt --commit
|
|
344
275
|
|
|
345
276
|
# Custom commit message
|
|
346
|
-
npx faf-taf-git --commit --message "test: update TAF"
|
|
277
|
+
npx faf-taf-git --file test-output.txt --commit --message "test: update TAF"
|
|
347
278
|
|
|
348
|
-
#
|
|
349
|
-
npx faf-taf-git --
|
|
279
|
+
# Generate badge
|
|
280
|
+
npx faf-taf-git badge --output badge.svg
|
|
350
281
|
|
|
351
282
|
PLATFORM SUPPORT:
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
✅ Jenkins
|
|
356
|
-
✅ CircleCI
|
|
357
|
-
✅ Local development
|
|
358
|
-
✅ Any CI/CD that runs Node.js
|
|
283
|
+
Works in ANY CI/CD that runs Node.js:
|
|
284
|
+
GitHub Actions, GitLab CI, Jenkins, CircleCI,
|
|
285
|
+
Bitbucket Pipelines, Azure Pipelines, local dev
|
|
359
286
|
|
|
360
287
|
LEARN MORE:
|
|
361
288
|
https://github.com/Wolfe-Jam/faf-taf-git
|
|
@@ -363,20 +290,47 @@ LEARN MORE:
|
|
|
363
290
|
process.exit(0);
|
|
364
291
|
}
|
|
365
292
|
|
|
293
|
+
const options: CLIOptions = {
|
|
294
|
+
verbose: args.includes('--verbose') || args.includes('-v'),
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// Parse --file
|
|
298
|
+
const fileIndex = args.indexOf('--file');
|
|
299
|
+
if (fileIndex !== -1 && args[fileIndex + 1]) {
|
|
300
|
+
options.file = args[fileIndex + 1];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Parse auto-commit flag
|
|
304
|
+
if (args.includes('--commit')) {
|
|
305
|
+
options.autoCommit = true;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Parse commit message
|
|
309
|
+
const messageIndex = args.indexOf('--message');
|
|
310
|
+
if (messageIndex !== -1 && args[messageIndex + 1]) {
|
|
311
|
+
options.commitMessage = args[messageIndex + 1];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Parse working directory
|
|
315
|
+
const cwdIndex = args.indexOf('--cwd');
|
|
316
|
+
if (cwdIndex !== -1 && args[cwdIndex + 1]) {
|
|
317
|
+
options.cwd = args[cwdIndex + 1];
|
|
318
|
+
}
|
|
319
|
+
|
|
366
320
|
// Run the tool
|
|
367
321
|
const result = await runTafGit(options);
|
|
368
322
|
|
|
369
323
|
if (result.success) {
|
|
370
324
|
if (result.testResults) {
|
|
371
|
-
console.log(
|
|
372
|
-
console.log(
|
|
325
|
+
console.log(`Tests: ${result.testResults.passed}/${result.testResults.total} passing`);
|
|
326
|
+
console.log(`Result: ${result.testResults.result}`);
|
|
373
327
|
}
|
|
374
328
|
if (result.tafUpdated) {
|
|
375
|
-
console.log('
|
|
329
|
+
console.log('.taf file updated');
|
|
376
330
|
}
|
|
377
331
|
process.exit(0);
|
|
378
332
|
} else {
|
|
379
|
-
console.error(
|
|
333
|
+
console.error(`Error: ${result.error}`);
|
|
380
334
|
if (result.testResults) {
|
|
381
335
|
console.log(`Tests: ${result.testResults.passed}/${result.testResults.total} passing`);
|
|
382
336
|
}
|
package/src/index.ts
CHANGED
|
@@ -9,13 +9,68 @@ import * as core from '@actions/core';
|
|
|
9
9
|
import * as exec from '@actions/exec';
|
|
10
10
|
import * as fs from 'fs';
|
|
11
11
|
import * as path from 'path';
|
|
12
|
-
import {
|
|
12
|
+
import { parseTestOutput } from './parsers';
|
|
13
13
|
import { updateTafFile } from './taf-core';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
16
|
+
* Switch to a dedicated target branch for TAF receipts.
|
|
17
|
+
* Creates the branch as an orphan on first run, seeded with existing .taf.
|
|
17
18
|
*/
|
|
18
|
-
async function
|
|
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> {
|
|
19
74
|
const execOptions = { cwd };
|
|
20
75
|
|
|
21
76
|
// Configure git
|
|
@@ -31,9 +86,10 @@ async function commitTafUpdate(cwd: string, message: string): Promise<void> {
|
|
|
31
86
|
if (exitCode === 0) {
|
|
32
87
|
await exec.exec('git', ['commit', '-m', message], execOptions);
|
|
33
88
|
|
|
34
|
-
//
|
|
89
|
+
// Push to target branch or current branch
|
|
90
|
+
const pushArgs = targetBranch ? ['push', 'origin', targetBranch] : ['push'];
|
|
35
91
|
try {
|
|
36
|
-
await exec.exec('git',
|
|
92
|
+
await exec.exec('git', pushArgs, execOptions);
|
|
37
93
|
} catch (error) {
|
|
38
94
|
core.warning('Could not push changes (this is ok in some CI environments)');
|
|
39
95
|
}
|
|
@@ -46,6 +102,7 @@ async function run(): Promise<void> {
|
|
|
46
102
|
const testOutputFile = core.getInput('test-output-file', { required: true });
|
|
47
103
|
const autoCommit = core.getInput('auto-commit') !== 'false';
|
|
48
104
|
const commitMessage = core.getInput('commit-message') || 'chore(taf): update .taf receipt [skip ci]';
|
|
105
|
+
const targetBranch = core.getInput('target-branch') || '';
|
|
49
106
|
const cwd = process.cwd();
|
|
50
107
|
|
|
51
108
|
core.info(`Reading test output from: ${testOutputFile}`);
|
|
@@ -78,10 +135,10 @@ async function run(): Promise<void> {
|
|
|
78
135
|
}
|
|
79
136
|
|
|
80
137
|
// Parse test results
|
|
81
|
-
const testResults =
|
|
138
|
+
const testResults = parseTestOutput(testOutput);
|
|
82
139
|
|
|
83
140
|
if (!testResults) {
|
|
84
|
-
core.setFailed('Could not parse test output.
|
|
141
|
+
core.setFailed('Could not parse test output. Supported: Jest, Vitest.');
|
|
85
142
|
return;
|
|
86
143
|
}
|
|
87
144
|
|
|
@@ -93,8 +150,20 @@ async function run(): Promise<void> {
|
|
|
93
150
|
core.setOutput('failed', testResults.failed.toString());
|
|
94
151
|
core.setOutput('total', testResults.total.toString());
|
|
95
152
|
|
|
96
|
-
//
|
|
153
|
+
// If target-branch is set, save .taf seed and switch branches
|
|
154
|
+
let tafSeed: string | null = null;
|
|
97
155
|
const tafPath = path.join(cwd, '.taf');
|
|
156
|
+
|
|
157
|
+
if (targetBranch) {
|
|
158
|
+
// Save existing .taf from source branch as seed for first-run
|
|
159
|
+
if (fs.existsSync(tafPath)) {
|
|
160
|
+
tafSeed = fs.readFileSync(tafPath, 'utf-8');
|
|
161
|
+
}
|
|
162
|
+
core.info(`Switching to target branch: ${targetBranch}`);
|
|
163
|
+
await switchToTargetBranch(cwd, targetBranch, tafSeed);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Check if .taf file exists (on current or target branch)
|
|
98
167
|
if (!fs.existsSync(tafPath)) {
|
|
99
168
|
core.setFailed('No .taf file found. Run `faf taf init` to create one.');
|
|
100
169
|
return;
|
|
@@ -114,8 +183,8 @@ async function run(): Promise<void> {
|
|
|
114
183
|
|
|
115
184
|
// Commit changes if enabled
|
|
116
185
|
if (autoCommit) {
|
|
117
|
-
await commitTafUpdate(cwd, commitMessage);
|
|
118
|
-
core.info(
|
|
186
|
+
await commitTafUpdate(cwd, commitMessage, targetBranch || undefined);
|
|
187
|
+
core.info(`✅ Changes committed${targetBranch ? ` to ${targetBranch}` : ''}`);
|
|
119
188
|
}
|
|
120
189
|
|
|
121
190
|
} catch (error) {
|