• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ggilder / codecoverage / 21153135063

19 Jan 2026 10:29PM UTC coverage: 72.61% (+1.9%) from 70.68%
21153135063

push

github

web-flow
feat: support pull requests with more than 300 changed files (#32)

* Fix API error for PRs with 300+ files

Use paginated pulls.listFiles API instead of pulls.get with diff format,
which has a 300 file limit. Add parsePatch function to handle individual
file patches from the listFiles response.

* Rebuild dist bundle with listFiles API changes

* Simplify parsePatch to only accept string parameter

Move undefined check to the caller in getPullRequestDiff where file.patch
may be undefined from the GitHub API response.

---------

Co-authored-by: Claude <noreply@anthropic.com>

83 of 121 branches covered (68.6%)

Branch coverage included in aggregate %.

21 of 28 new or added lines in 2 files covered. (75.0%)

4 existing lines in 1 file now uncovered.

312 of 423 relevant lines covered (73.76%)

3.48 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

68.97
/src/utils/github.ts
1
import * as core from '@actions/core'
2✔
2
import {parsePatch} from './diff'
2✔
3
import * as github from '@actions/github'
1✔
4
import {
2✔
5
  CoverageFile,
1✔
6
  LineRange,
7
  coalesceLineNumbers,
1✔
8
  intersectLineRanges
9
} from './general'
10
import {Octokit} from 'octokit'
1✔
11

1!
12
export class GithubUtil {
1✔
13
  private client: Octokit
14

1✔
15
  constructor(token: string, baseUrl: string) {
1✔
16
    if (!token) {
3✔
17
      throw new Error('GITHUB_TOKEN is missing')
1✔
18
    }
1✔
19
    this.client = new Octokit({auth: token, baseUrl})
2!
20
  }
3✔
21

22
  getPullRequestRef(): string {
1✔
23
    const pullRequest = github.context.payload.pull_request
24
    return pullRequest
25
      ? pullRequest.head.ref
26
      : github.context.ref.replace('refs/heads/', '')
27
  }
28

29
  async getPullRequestDiff(): Promise<PullRequestFiles> {
1✔
30
    const pull_number = github.context.issue.number
31
    const prFiles: PullRequestFiles = {}
32

33
    // Use paginated listFiles API to handle PRs with more than 300 files
NEW
34
    const iterator = this.client.paginate.iterator(
×
NEW
35
      this.client.rest.pulls.listFiles,
×
NEW
36
      {
×
NEW
37
        ...github.context.repo,
×
38
        pull_number,
NEW
39
        per_page: 100
×
NEW
40
      }
×
41
    )
42

43
    for await (const response of iterator) {
NEW
44
      for (const file of response.data) {
×
45
        if (file.patch) {
46
          const addedLines = parsePatch(file.patch)
47
          if (addedLines.length > 0) {
48
            prFiles[file.filename] = coalesceLineNumbers(addedLines)
49
          }
50
        }
51
      }
52
    }
53

54
    return prFiles
55
  }
56

57
  /**
58
   * https://docs.github.com/en/rest/checks/runs?apiVersion=2022-11-28#create-a-check-run
59
   * https://docs.github.com/en/rest/checks/runs?apiVersion=2022-11-28#update-a-check-run
60
   */
61
  async annotate(input: InputAnnotateParams): Promise<number> {
1✔
62
    if (input.annotations.length === 0) {
63
      return 0
UNCOV
64
    }
×
65
    // github API lets you post 50 annotations at a time
66
    const chunkSize = 50
67
    const chunks: Annotations[][] = []
×
UNCOV
68
    for (let i = 0; i < input.annotations.length; i += chunkSize) {
×
UNCOV
69
      chunks.push(input.annotations.slice(i, i + chunkSize))
×
UNCOV
70
    }
×
71
    let lastResponseStatus = 0
72
    let checkId
73
    for (let i = 0; i < chunks.length; i++) {
74
      let status: 'in_progress' | 'completed' | 'queued' = 'in_progress'
75
      let conclusion:
76
        | 'success'
77
        | 'action_required'
78
        | 'cancelled'
79
        | 'failure'
80
        | 'neutral'
81
        | 'skipped'
82
        | 'stale'
83
        | 'timed_out'
84
        | undefined = undefined
85
      if (i === chunks.length - 1) {
86
        status = 'completed'
87
        conclusion = 'success'
88
      }
89
      const params = {
90
        ...github.context.repo,
91
        name: 'Annotate',
92
        head_sha: input.referenceCommitHash,
93
        status,
94
        ...(conclusion && {conclusion}),
95
        output: {
96
          title: 'Coverage Tool',
97
          summary: 'Missing Coverage',
98
          annotations: chunks[i]
99
        }
100
      }
101
      let response
102
      if (i === 0) {
103
        response = await this.client.rest.checks.create({
104
          ...params
105
        })
106
        checkId = response.data.id
107
      } else {
108
        response = await this.client.rest.checks.update({
109
          ...params,
110
          check_run_id: checkId,
111
          status: 'in_progress' as const
112
        })
113
      }
114
      core.info(response.data.output.annotations_url)
115
      lastResponseStatus = response.status
116
    }
117
    return lastResponseStatus
118
  }
119

120
  buildAnnotations(
1✔
121
    coverageFiles: CoverageFile[],
1✔
122
    pullRequestFiles: PullRequestFiles
1✔
123
  ): Annotations[] {
1✔
124
    const annotations: Annotations[] = []
1✔
125
    for (const current of coverageFiles) {
1✔
126
      // Only annotate relevant files
127
      const prFileRanges = pullRequestFiles[current.fileName]
3✔
128
      if (prFileRanges) {
3✔
129
        const coverageRanges = coalesceLineNumbers(current.missingLineNumbers)
2✔
130
        const uncoveredRanges = intersectLineRanges(
2✔
131
          coverageRanges,
2✔
132
          prFileRanges
2✔
133
        )
2✔
134

135
        // Only annotate relevant line ranges
136
        for (const uRange of uncoveredRanges) {
2✔
137
          const message =
4✔
138
            uRange.end_line > uRange.start_line
4✔
139
              ? 'These lines are not covered by a test'
1✔
140
              : 'This line is not covered by a test'
3✔
141
          annotations.push({
4✔
142
            path: current.fileName,
4✔
143
            start_line: uRange.start_line,
4✔
144
            end_line: uRange.end_line,
4✔
145
            annotation_level: 'warning',
4✔
146
            message
4✔
147
          })
4✔
148
        }
4✔
149
      }
2✔
150
    }
3✔
151
    core.info(`Annotation count: ${annotations.length}`)
1✔
152
    return annotations
1✔
153
  }
1✔
154
}
1✔
155

156
type InputAnnotateParams = {
157
  referenceCommitHash: string
158
  annotations: Annotations[]
159
}
160

161
type Annotations = {
162
  path: string
163
  start_line: number
164
  end_line: number
165
  start_column?: number
166
  end_column?: number
167
  annotation_level: 'notice' | 'warning' | 'failure'
168
  message: string
169
}
170

171
type PullRequestFiles = {
172
  [key: string]: LineRange[]
173
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc