Skip to content

Commit c7af8cb

Browse files
committed
feat: add basic CLI entry point
1 parent ae28ec8 commit c7af8cb

14 files changed

Lines changed: 497 additions & 290 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ Thumbs.db
1717
lib/
1818

1919
# Only release tag contains dist directory
20-
/dist
20+
dist/
21+
dist-cli/
2122

2223
# Ignore coverage files
2324
coverage/

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
33
"scripts": {
44
"check": "biome migrate --write && biome check --fix",
55
"build": "ncc build --source-map --license licenses.txt src/index.ts",
6+
"build:cli": "ncc build --source-map src/cli.ts -o dist-cli",
67
"test": "vitest",
78
"test:coverage": "vitest run --coverage"
89
},
10+
"bin": {
11+
"coverage-visualizer": "dist-cli/index.js"
12+
},
913
"type": "module",
1014
"dependencies": {
1115
"@actions/core": "2.0.1",

src/action.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import * as core from '@actions/core'
2+
import type { Octokit } from '@octokit/action'
3+
import { processCoverage, type Logger } from './core/index.js'
4+
import { findPullRequestNumber, getBaseRef, postComment, type Context } from './github.js'
5+
6+
export type Inputs = {
7+
files: string
8+
updateComment: boolean
9+
showChangedLinesOnly: boolean
10+
showGlobOnly: string
11+
}
12+
13+
/**
14+
* Logger implementation that uses @actions/core for GitHub Actions.
15+
*/
16+
const actionLogger: Logger = {
17+
info: (message) => core.info(message),
18+
warning: (message) => core.warning(message),
19+
}
20+
21+
export const run = async (inputs: Inputs, octokit: Octokit, context: Context): Promise<void> => {
22+
// Get the base ref from GitHub context
23+
const baseRef = getBaseRef(context) ?? undefined
24+
25+
// Process coverage using the core module
26+
const result = await processCoverage(
27+
{
28+
files: inputs.files,
29+
showChangedLinesOnly: inputs.showChangedLinesOnly,
30+
showGlobOnly: inputs.showGlobOnly,
31+
baseRef,
32+
},
33+
actionLogger,
34+
)
35+
36+
if (!result) {
37+
return
38+
}
39+
40+
const { markdown, metrics } = result
41+
42+
// Set GitHub Actions outputs
43+
core.setOutput('line-coverage', metrics.lineCoverage.toFixed(2))
44+
core.setOutput('branch-coverage', metrics.branchCoverage.toFixed(2))
45+
core.setOutput('function-coverage', metrics.functionCoverage.toFixed(2))
46+
47+
// Find the pull request (needed for posting)
48+
const pullNumber = await findPullRequestNumber(octokit, context)
49+
50+
if (!pullNumber) {
51+
core.info('No pull request found for this commit, writing to step summary instead')
52+
await core.summary.addRaw(markdown).write()
53+
return
54+
}
55+
56+
// Post or update comment
57+
const { url, updated } = await postComment(octokit, context, pullNumber, markdown, inputs.updateComment)
58+
59+
if (updated) {
60+
core.info(`Updated existing comment: ${url}`)
61+
} else {
62+
core.info(`Created new comment: ${url}`)
63+
}
64+
65+
core.info('Coverage visualization posted successfully')
66+
}

src/cli.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
#!/usr/bin/env node
2+
import * as fs from 'node:fs/promises'
3+
import { parseArgs } from 'node:util'
4+
import { processCoverage, type Logger } from './core/index.js'
5+
6+
/**
7+
* Logger implementation for CLI that uses console.
8+
*/
9+
const cliLogger: Logger = {
10+
info: (message) => console.log(`[INFO] ${message}`),
11+
warning: (message) => console.warn(`[WARN] ${message}`),
12+
}
13+
14+
/**
15+
* Print usage information.
16+
*/
17+
function printHelp(): void {
18+
console.log(`
19+
coverage-visualizer - Generate coverage reports from Cobertura XML files
20+
21+
USAGE:
22+
coverage-visualizer --files <patterns> [options]
23+
24+
OPTIONS:
25+
--files <patterns> Coverage file patterns (required, comma or newline separated)
26+
--output <path> Output file path (optional, prints to stdout if not specified)
27+
--base-ref <ref> Git ref to compare against (e.g., origin/main, HEAD~1)
28+
--show-changed-only Filter to show only changed lines (requires --base-ref)
29+
--show-glob <pattern> Glob pattern to filter which files to show (default: **)
30+
--help Show this help message
31+
32+
EXAMPLES:
33+
# Generate report for all coverage files
34+
coverage-visualizer --files "coverage/*.xml"
35+
36+
# Save report to file
37+
coverage-visualizer --files "coverage/*.xml" --output report.md
38+
39+
# Show only changed lines compared to main branch
40+
coverage-visualizer --files "coverage/*.xml" --base-ref origin/main --show-changed-only
41+
42+
# Filter to specific directory
43+
coverage-visualizer --files "coverage/*.xml" --show-glob "src/components/**"
44+
`)
45+
}
46+
47+
async function main(): Promise<void> {
48+
const { values } = parseArgs({
49+
options: {
50+
files: { type: 'string', short: 'f' },
51+
output: { type: 'string', short: 'o' },
52+
'base-ref': { type: 'string', short: 'b' },
53+
'show-changed-only': { type: 'boolean', default: false },
54+
'show-glob': { type: 'string', default: '**' },
55+
help: { type: 'boolean', short: 'h', default: false },
56+
},
57+
strict: true,
58+
})
59+
60+
// Show help
61+
if (values.help) {
62+
printHelp()
63+
process.exit(0)
64+
}
65+
66+
// Validate required arguments
67+
if (!values.files) {
68+
console.error('Error: --files is required\n')
69+
printHelp()
70+
process.exit(1)
71+
}
72+
73+
// Validate base-ref is provided if show-changed-only is set
74+
if (values['show-changed-only'] && !values['base-ref']) {
75+
console.error('Error: --base-ref is required when using --show-changed-only\n')
76+
printHelp()
77+
process.exit(1)
78+
}
79+
80+
// Process coverage
81+
const result = await processCoverage(
82+
{
83+
files: values.files,
84+
showChangedLinesOnly: values['show-changed-only'] ?? false,
85+
showGlobOnly: values['show-glob'] ?? '**',
86+
...(values['base-ref'] && { baseRef: values['base-ref'] }),
87+
},
88+
cliLogger,
89+
)
90+
91+
if (!result) {
92+
process.exit(1)
93+
}
94+
95+
const { markdown, metrics } = result
96+
97+
// Output results
98+
if (values.output) {
99+
await fs.writeFile(values.output, markdown, 'utf-8')
100+
console.log(`\nReport written to: ${values.output}`)
101+
} else {
102+
console.log('\n' + markdown)
103+
}
104+
105+
// Print metrics summary
106+
console.log('\n--- Coverage Summary ---')
107+
console.log(`Line Coverage: ${metrics.lineCoverage.toFixed(2)}%`)
108+
console.log(`Branch Coverage: ${metrics.branchCoverage.toFixed(2)}%`)
109+
console.log(`Function Coverage: ${metrics.functionCoverage.toFixed(2)}%`)
110+
}
111+
112+
main().catch((error) => {
113+
console.error('Error:', error instanceof Error ? error.message : String(error))
114+
process.exit(1)
115+
})

src/core/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export {
2+
processCoverage,
3+
type Logger,
4+
type ProcessCoverageInputs,
5+
type ProcessCoverageResult,
6+
type CoverageMetrics,
7+
} from './process-coverage.js'

0 commit comments

Comments
 (0)