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.
Files changed (48) hide show
  1. package/.github/workflows/ci.yml +53 -0
  2. package/.github/workflows/claude-code-review.yml.disabled +29 -0
  3. package/.github/workflows/claude.yml +39 -0
  4. package/.github/workflows/homebrew.yml +19 -0
  5. package/.taf +79 -2
  6. package/CHANGELOG.md +28 -0
  7. package/CLAUDE.md +29 -21
  8. package/CODE_OF_CONDUCT.md +21 -0
  9. package/CONTRIBUTING.md +3 -0
  10. package/README.md +69 -12
  11. package/SECURITY.md +19 -0
  12. package/action.yml +6 -1
  13. package/badge.svg +21 -0
  14. package/dist/badge.d.ts +23 -0
  15. package/dist/badge.d.ts.map +1 -0
  16. package/dist/badge.js +147 -0
  17. package/dist/badge.js.map +1 -0
  18. package/dist/cli.d.ts +5 -6
  19. package/dist/cli.d.ts.map +1 -1
  20. package/dist/cli.js +144 -193
  21. package/dist/cli.js.map +1 -1
  22. package/dist/index.js +26045 -20566
  23. package/dist/index.js.map +1 -1
  24. package/dist/licenses.txt +0 -22
  25. package/dist/parsers/bun.d.ts +59 -0
  26. package/dist/parsers/bun.d.ts.map +1 -0
  27. package/dist/parsers/bun.js +100 -0
  28. package/dist/parsers/bun.js.map +1 -0
  29. package/dist/parsers/index.d.ts +20 -0
  30. package/dist/parsers/index.d.ts.map +1 -0
  31. package/dist/parsers/index.js +26 -0
  32. package/dist/parsers/index.js.map +1 -0
  33. package/dist/parsers/vitest.d.ts +17 -0
  34. package/dist/parsers/vitest.d.ts.map +1 -0
  35. package/dist/parsers/vitest.js +61 -0
  36. package/dist/parsers/vitest.js.map +1 -0
  37. package/package.json +6 -7
  38. package/project.faf +20 -24
  39. package/src/badge.ts +138 -0
  40. package/src/cli.ts +154 -200
  41. package/src/index.ts +79 -10
  42. package/src/parsers/bun.ts +103 -0
  43. package/src/parsers/index.ts +27 -0
  44. package/src/parsers/vitest.ts +65 -0
  45. package/tests/badge.test.ts +260 -0
  46. package/tests/bun-parser.test.ts +173 -0
  47. package/tests/vitest-parser.test.ts +112 -0
  48. 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 - Standalone CLI for TAF operations
3
+ * faf-taf-git - The Git-Native Receipt Printer
4
4
  *
5
- * Platform-agnostic core that works in ANY CI/CD environment
6
- * Can be used standalone or as a library by wrappers
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 * as exec from '@actions/exec';
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 { parseJestOutput } from './parsers/jest';
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
- command?: string;
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 - platform-agnostic
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
- command = 'npm test',
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 (verbose) console.log(`Running test command: ${command}`);
53
-
54
- // Run tests and capture output using @actions/exec
55
- let output = '';
56
- let exitCode = 0;
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
- // --- [START NUCLEAR CANARY] ---
72
- // Use getExecOutput to capture stdout/stderr
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
- // Continue with original logic
124
- exitCode = result.exitCode;
125
- output = result.stdout + result.stderr;
126
- // --- [END NUCLEAR CANARY] ---
127
- } catch (error: any) {
128
- exitCode = 1;
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
- // Debug: Write entire output to file for inspection
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 = parseJestOutput(output);
74
+ const testResults = parseTestOutput(output);
160
75
 
161
76
  if (!testResults) {
162
77
  if (verbose) {
163
- logger(`DEBUG: Parser returned null`);
164
- logger(`DEBUG: Output sample (first 500 chars): ${output.slice(0, 500)}`);
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. Ensure you are using Jest.',
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('✅ .taf file updated successfully');
112
+ if (verbose) logger('.taf file updated successfully');
199
113
 
200
114
  // Commit changes if enabled
201
115
  if (autoCommit) {
202
- await commitTafUpdate(cwd, commitMessage, verbose, logger);
203
- if (verbose) logger('✅ Changes committed to git');
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 (platform-agnostic)
153
+ * Commit .taf file changes to git
240
154
  */
241
- async function commitTafUpdate(cwd: string, message: string, verbose: boolean, logger: (msg: string) => void = console.log): Promise<void> {
155
+ function commitTafUpdate(cwd: string, message: string, verbose: boolean, logger: (msg: string) => void = console.log): void {
242
156
  try {
243
- const execOptions: exec.ExecOptions = { cwd };
244
-
245
- // Configure git if needed
246
- await exec.exec('git', ['config', '--global', 'user.name', 'faf-taf-git[bot]'], execOptions);
247
- await exec.exec('git', ['config', '--global', 'user.email', 'faf-taf-git[bot]@users.noreply.github.com'], execOptions);
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
- } catch (error) {
279
- if (verbose) {
280
- logger(`Error committing changes: ${error}`);
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
- * CLI entry point - parse args and run
181
+ * Handle the 'badge' subcommand
288
182
  */
289
- async function main() {
290
- const args = process.argv.slice(2);
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
- const options: CLIOptions = {
293
- verbose: args.includes('--verbose') || args.includes('-v'),
294
- };
188
+ USAGE:
189
+ npx faf-taf-git badge [OPTIONS]
295
190
 
296
- // Parse command flag
297
- const commandIndex = args.indexOf('--command');
298
- if (commandIndex !== -1 && args[commandIndex + 1]) {
299
- options.command = args[commandIndex + 1];
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 auto-commit flag
303
- if (args.includes('--commit')) {
304
- options.autoCommit = true;
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 commit message
308
- const messageIndex = args.indexOf('--message');
309
- if (messageIndex !== -1 && args[messageIndex + 1]) {
310
- options.commitMessage = args[messageIndex + 1];
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 working directory
314
- const cwdIndex = args.indexOf('--cwd');
315
- if (cwdIndex !== -1 && args[cwdIndex + 1]) {
316
- options.cwd = args[cwdIndex + 1];
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 - Platform-agnostic TAF updater
251
+ faf-taf-git - The Git-Native Receipt Printer
323
252
 
324
253
  USAGE:
325
- npx faf-taf-git [OPTIONS]
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
- --command <cmd> Test command to run (default: npm test)
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 and update .taf
337
- npx faf-taf-git
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
- # Verbose mode
349
- npx faf-taf-git --verbose
279
+ # Generate badge
280
+ npx faf-taf-git badge --output badge.svg
350
281
 
351
282
  PLATFORM SUPPORT:
352
- ✅ GitHub Actions
353
- ✅ GitLab CI
354
- ✅ Bitbucket Pipelines
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(`✅ Tests: ${result.testResults.passed}/${result.testResults.total} passing`);
372
- console.log(`✅ Result: ${result.testResults.result}`);
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('✅ .taf file updated');
329
+ console.log('.taf file updated');
376
330
  }
377
331
  process.exit(0);
378
332
  } else {
379
- console.error(`❌ Error: ${result.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 { parseJestOutput } from './parsers/jest';
12
+ import { parseTestOutput } from './parsers';
13
13
  import { updateTafFile } from './taf-core';
14
14
 
15
15
  /**
16
- * Commit .taf file changes to git
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 commitTafUpdate(cwd: string, message: string): Promise<void> {
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
- // Try to push (might fail in some environments, that's ok)
89
+ // Push to target branch or current branch
90
+ const pushArgs = targetBranch ? ['push', 'origin', targetBranch] : ['push'];
35
91
  try {
36
- await exec.exec('git', ['push'], execOptions);
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 = parseJestOutput(testOutput);
138
+ const testResults = parseTestOutput(testOutput);
82
139
 
83
140
  if (!testResults) {
84
- core.setFailed('Could not parse test output. Ensure you are using Jest.');
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
- // Check if .taf file exists
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('✅ Changes committed to git');
186
+ await commitTafUpdate(cwd, commitMessage, targetBranch || undefined);
187
+ core.info(`✅ Changes committed${targetBranch ? ` to ${targetBranch}` : ''}`);
119
188
  }
120
189
 
121
190
  } catch (error) {