Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ outputs:
description: The overall line coverage percentage
branch-coverage:
description: The overall branch coverage percentage
pr-line-coverage:
description: The line coverage of the changes in this PR. Equal to the overall line coverage, if not executed on a PR
pr-branch-coverage:
description: The branch coverage of the changes in this PR. Equal to the overall line coverage, if not executed on a PR

runs:
using: 'node24'
Expand Down
10 changes: 6 additions & 4 deletions src/action.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as core from '@actions/core'
import type { Octokit } from '@octokit/action'
import { processCoverage, type Logger } from './core/index.js'
import { findPullRequestNumber, getComparisonShas, postComment, type Context } from './github.js'
import { type Logger, processCoverage } from './core/index.js'
import { type Context, findPullRequestNumber, getComparisonShas, postComment } from './github.js'

export type Inputs = {
files: string
Expand Down Expand Up @@ -39,11 +39,13 @@ export const run = async (inputs: Inputs, octokit: Octokit, context: Context): P
return
}

const { markdown, lineCoverage, branchCoverage } = result
const { markdown, lineCoverage, branchCoverage, lineCoveragePr, branchCoveragePr } = result

// Set GitHub Actions outputs
core.setOutput('line-coverage', lineCoverage.toFixed(2))
core.setOutput('branch-coverage', branchCoverage.toFixed(2))
core.setOutput('branch-coverage', branchCoverage?.toFixed(2))
core.setOutput('pr-line-coverage', lineCoveragePr.toFixed(2))
core.setOutput('pr-branch-coverage', branchCoveragePr?.toFixed(2))

// Find the pull request (needed for posting)
const pullNumber = await findPullRequestNumber(octokit, context)
Expand Down
14 changes: 8 additions & 6 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
#!/usr/bin/env node
import * as fs from 'node:fs/promises'
import { execFile } from 'node:child_process'
import * as fs from 'node:fs/promises'
import { parseArgs, promisify } from 'node:util'
import { processCoverage } from './core/index.js'
import { isValidGitRef } from './filter/changed-lines.js'
import { createCliLogger } from './core/process-coverage.js'
import { isValidGitRef } from './filter/changed-lines.js'

const execFileAsync = promisify(execFile)

Expand Down Expand Up @@ -121,20 +121,22 @@ async function main(): Promise<void> {
process.exit(1)
}

const { markdown, lineCoverage, branchCoverage } = result
const { markdown, lineCoverage, branchCoverage, lineCoveragePr, branchCoveragePr } = result

// Output results
if (values.output) {
await fs.writeFile(values.output, markdown, 'utf-8')
console.log(`\nReport written to: ${values.output}`)
} else {
console.log('\n' + markdown)
console.log(`\n${markdown}`)
}

// Print metrics summary
console.log('\n--- Coverage Summary ---')
console.log(`Line Coverage: ${lineCoverage.toFixed(2)}%`)
console.log(`Branch Coverage: ${branchCoverage.toFixed(2)}%`)
console.log(`Line Coverage: ${lineCoverage.toFixed(2)}%`)
console.log(`Branch Coverage: ${branchCoverage?.toFixed(2)}%`)
console.log(`Line Coverage PR: ${lineCoveragePr.toFixed(2)}%`)
console.log(`Branch Coverage PR: ${branchCoveragePr?.toFixed(2)}%`)
}

main().catch((error) => {
Expand Down
2 changes: 1 addition & 1 deletion src/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export {
processCoverage,
type Logger,
type ProcessCoverageInputs,
type ProcessCoverageResult,
processCoverage,
} from './process-coverage.js'
43 changes: 29 additions & 14 deletions src/core/process-coverage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import * as fs from 'node:fs/promises'
import * as path from 'node:path'
import {
CoberturaCoverageParser,
CoverageMetrics,
type CoverageMetrics,
type CoverageReport,
FileCoverage,
LineCoverage,
LineCoverageState,
PackageCoverage,
type FileCoverage,
type LineCoverage,
type LineCoverageState,
type PackageCoverage,
} from '../coverage/index.js'
import { filterByChangedLines, filterByGlob, getChangedLinesFromGit, type ChangedLinesMap } from '../filter/index.js'
import type { PercentageCoverageMetrics } from '../coverage/model.js'
import { type ChangedLinesMap, filterByChangedLines, filterByGlob, getChangedLinesFromGit } from '../filter/index.js'
import { generateMarkdown } from '../markdown/index.js'

/**
Expand Down Expand Up @@ -53,15 +54,16 @@ export type ProcessCoverageInputs = {
/** Explicit head commit SHA for comparison */
headSha?: string | undefined
}

/**
* Result of coverage processing.
*/
export type ProcessCoverageResult = {
/** Generated markdown report */
markdown: string
lineCoverage: number
branchCoverage: number
branchCoverage: number | undefined
lineCoveragePr: number
branchCoveragePr: number | undefined
}

/**
Expand Down Expand Up @@ -151,13 +153,26 @@ export async function processCoverage(
logger.debug?.(`Generating markdown for ${file.resolvedPath} with ${file.lines.length} changed lines`)
}
}
// Generate Markdown from filtered report
const markdown = generateMarkdown(filteredPackages, fileContents)

// Calculate overall metrics for outputs (from original merged report for accuracy)
const metrics = calculateOverallMetrics(mergedPackages)
const overallMetrics = calculateOverallMetrics(mergedPackages)
const prMetrics = calculateOverallMetrics(filteredPackages)
logger.info(
`Calculated overall metrics (LineCoverage: ${overallMetrics.lineCoverage}, BranchCoverage: ${overallMetrics.branchCoverage})`,
)
logger.info(
`Calculated PR metrics (LineCoverage: ${prMetrics.lineCoverage}, BranchCoverage: ${prMetrics.branchCoverage})`,
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Generate Markdown from filtered report
const markdown = generateMarkdown(filteredPackages, fileContents, overallMetrics, {}, logger)

return { markdown, lineCoverage: metrics.lineCoverage, branchCoverage: metrics.branchCoverage }
return {
markdown,
lineCoverage: overallMetrics.lineCoverage,
branchCoverage: overallMetrics.branchCoverage,
lineCoveragePr: prMetrics.lineCoverage,
branchCoveragePr: prMetrics.branchCoverage,
}
}

async function firstExistingDirectory(paths: readonly string[]): Promise<string | undefined> {
Expand Down Expand Up @@ -272,7 +287,7 @@ async function mergeReportAndResolveSources(
/**
* Calculate overall coverage metrics from a merged packages.
*/
function calculateOverallMetrics(packages: PackageCoverage[]): { lineCoverage: number; branchCoverage: number } {
function calculateOverallMetrics(packages: PackageCoverage[]): PercentageCoverageMetrics {
let lineCovered = 0
let lineTotal = 0
let branchCovered = 0
Expand Down
5 changes: 5 additions & 0 deletions src/coverage/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ export type CoverageMetrics = {
total: number
}

export type PercentageCoverageMetrics = {
lineCoverage: number
branchCoverage?: number | undefined
}

export type FileCoverage = {
/** Display path (relative to source root, for markdown output) */
filename: string
Expand Down
2 changes: 1 addition & 1 deletion src/filter/filter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as path from 'node:path'
import { FileCoverage, PackageCoverage } from '../coverage/model.js'
import type { Logger } from '../core/index.js'
import type { FileCoverage, PackageCoverage } from '../coverage/model.js'
import type { ChangedLinesMap } from './model.js'

/**
Expand Down
46 changes: 36 additions & 10 deletions src/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,24 +38,50 @@ const getEnv = (name: string): string => {

/**
* Find the pull request number associated with the current context.
* Only returns PRs that are still open (not merged or closed).
*/
export async function findPullRequestNumber(octokit: Octokit, context: Context): Promise<number | null> {
// Check if we're already in a pull_request event
// Fast path: pull_request event payload
if ('pull_request' in context.payload) {
const prPayload = context.payload as { pull_request?: { number: number } }
if (prPayload.pull_request?.number) {
return prPayload.pull_request.number
const pr = (context.payload as { pull_request?: { number: number; state?: string } }).pull_request
if (pr?.number) {
if (pr.state !== 'open') return null
return pr.number
}
}

// Otherwise, find PRs associated with this commit
const { data: pulls } = await octokit.rest.repos.listPullRequestsAssociatedWithCommit({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: context.sha,
})
let pulls: { number: number; state?: string }[]
try {
const res = await octokit.rest.repos.listPullRequestsAssociatedWithCommit({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: context.sha,
})
pulls = res.data
} catch {
return null
}

for (const pull of pulls) {
const prNumber = pull.number
if (!prNumber) continue

return pulls[0]?.number ?? null
// Prefer using state if present
if (pull.state === 'open') return prNumber
if (pull.state && pull.state !== 'open') continue

// Fallback: fetch PR details if state is missing/unknown
const { data: pr } = await octokit.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
})

if (pr.state === 'open') return prNumber
}

return null
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as core from '@actions/core'
import { getContext, getOctokit } from './github.js'
import { run } from './action.js'
import { getContext, getOctokit } from './github.js'

try {
await run(
Expand Down
Loading