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

levibostian / new-deployment-tool / 16025767270

02 Jul 2025 12:57PM UTC coverage: 70.793% (-0.9%) from 71.648%
16025767270

push

github

levibostian
refactor: make github comments from the CLI, not from action

to make sure that everyone gets this great feature even if you dont use the github action

67 of 85 branches covered (78.82%)

Branch coverage included in aggregate %.

7 of 18 new or added lines in 2 files covered. (38.89%)

7 existing lines in 2 files now uncovered.

585 of 836 relevant lines covered (69.98%)

9.09 hits per line

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

4.12
/lib/github-api.ts
1
import * as log from "./log.ts"
4✔
2
import * as cathy from "npm:cathy"
4✔
3

4
export interface GitHubRelease {
5
  tag: {
6
    name: string
7
    commit: {
8
      sha: string
9
    }
10
  }
11
  name: string
12
  created_at: Date
13
}
14

15
export interface GitHubCommit {
16
  sha: string
17
  message: string
18
  date: Date
19
}
20

21
interface GitHubCommitApiResponse {
22
  sha: string
23
  commit: {
24
    message: string
25
    committer: {
26
      date: string
27
    }
28
  }
29
}
30

31
export interface GitHubPullRequest {
32
  prNumber: number
33
  targetBranchName: string
34
  sourceBranchName: string
35
  title: string
36
  description: string
37
}
38

39
// Returns list of all open pull requests that are stacked on top of each other.
40
// Index 0 is the newest pull request.
41
const getPullRequestStack = async (
×
42
  { owner, repo, startingPrNumber }: { owner: string; repo: string; startingPrNumber: number },
×
43
): Promise<GitHubPullRequest[]> => {
44
  // get list of all open pull requests.
45
  const graphqlQuery = `
×
46
query($owner: String!, $repo: String!, $endCursor: String, $numberOfResults: Int!) {
47
  repository(owner: $owner, name: $repo) {
48
    pullRequests(first: $numberOfResults, states: [OPEN], after: $endCursor, orderBy: {field: CREATED_AT, direction: DESC}) {
49
      nodes {
50
        prNumber: number
51
        targetBranchName: baseRefName
52
        sourceBranchName: headRefName     
53
        title: title
54
        description: body # description in markdown. you can do html or text instead. 
55
      }
56
      pageInfo {        
57
        endCursor
58
        hasNextPage
59
      }
60
    }
61
  }
62
}
63
`
×
64

65
  let pullRequests: GitHubPullRequest[] = []
×
66

67
  await githubGraphqlRequestPaging<{
×
68
    data: {
69
      repository: {
70
        pullRequests: {
71
          nodes: {
72
            prNumber: number
73
            targetBranchName: string
74
            sourceBranchName: string
75
            title: string
76
            description: string
77
          }[]
78
          pageInfo: {
79
            endCursor: string
80
            hasNextPage: boolean
81
          }
82
        }
83
      }
84
    }
85
  }>(
86
    graphqlQuery,
×
87
    { owner, repo, numberOfResults: 100 },
×
88
    async (response) => {
×
89
      pullRequests.push(...response.data.repository.pullRequests.nodes)
×
90
      return true
×
91
    },
×
92
  )
93

94
  // Takes the list of all pull requests for the repo and puts into a list, starting at the startingBranch PR, and creates the stack. Going from starting to the top of the stack (example: PR 1 -> PR 2 -> PR 3).
95
  const startingPullRequest = pullRequests.find((pr) => pr.prNumber === startingPrNumber)
×
96
  if (!startingPullRequest) {
×
97
    throw new Error(
×
98
      `Could not get pull request stack because not able to find pull request for starting PR number, ${startingPrNumber}. This is unexpected.`,
×
99
    )
100
  }
×
101

102
  const prStack: GitHubPullRequest[] = [startingPullRequest]
×
103
  let sourceBranchSearchingFor = startingPullRequest.sourceBranchName
×
104

105
  while (true) {
×
106
    const nextPullRequest = pullRequests.find((pr) => pr.targetBranchName === sourceBranchSearchingFor)
×
107

108
    if (!nextPullRequest) {
×
109
      break
×
110
    }
×
111

112
    prStack.push(nextPullRequest)
×
113
    sourceBranchSearchingFor = nextPullRequest.sourceBranchName
×
114
  }
×
115

116
  return prStack
×
117
}
×
118

119
const getCommitsForBranch = async <T>(
×
120
  { sampleData, owner, repo, branch, processCommits }: {
×
121
    sampleData?: GitHubCommit[]
122
    owner: string
123
    repo: string
124
    branch: string
125
    processCommits: (data: GitHubCommit[]) => Promise<boolean>
126
  },
×
127
) => {
128
  if (sampleData) {
×
129
    await processCommits(sampleData)
×
130
    return
×
131
  }
×
132

133
  return await githubApiRequestPaging<GitHubCommitApiResponse[]>(
×
134
    `https://api.github.com/repos/${owner}/${repo}/commits?sha=${branch}&per_page=100`,
×
135
    async (apiResponse) => {
×
136
      return await processCommits(apiResponse.map((response) => {
×
137
        return {
×
138
          sha: response.sha,
×
139
          message: response.commit.message,
×
140
          date: new Date(response.commit.committer.date),
×
141
        }
×
142
      }))
×
143
    },
×
144
  )
145
}
×
146

147
// Make a GitHub Rest API request.
148
const githubApiRequest = async <T>(
×
149
  url: string,
×
150
  method: "GET" | "POST" = "GET",
×
151
  body: object | undefined = undefined,
×
152
) => {
153
  const headers = {
×
154
    "Authorization": `Bearer ${Deno.env.get("INPUT_GITHUB_TOKEN")}`,
×
155
    "Accept": "application/vnd.github.v3+json",
×
156
    "Content-Type": "application/json",
×
157
  }
×
158

159
  log.debug(
×
160
    `GitHub API request: ${method}:${url}, headers: ${JSON.stringify(headers)}, body: ${JSON.stringify(body)}`,
×
161
  )
162

163
  const response = await fetch(url, {
×
164
    method,
×
165
    headers,
×
166
    body: body ? JSON.stringify(body) : undefined,
×
167
  })
×
168

169
  if (!response.ok) {
×
170
    throw new Error(
×
171
      `Failed to call github API endpoint: ${url}, given error: ${response.statusText}`,
×
172
    )
173
  }
×
174

175
  const responseJsonBody: T = await response.json()
×
176

177
  return {
×
178
    status: response.status,
×
179
    body: responseJsonBody,
×
180
    headers: response.headers,
×
181
  }
×
182
}
×
183

184
// Make a GitHub Rest API request that supports paging. Takes in a function that gets called for each page of results.
185
// In that function, return true if you want to get the next page of results.
186
async function githubApiRequestPaging<RESPONSE>(
×
187
  initialUrl: string,
×
188
  processResponse: (data: RESPONSE) => Promise<boolean>,
×
189
): Promise<void> {
190
  let url = initialUrl
×
191
  let getNextPage = true
×
192

193
  while (getNextPage) {
×
194
    const response = await githubApiRequest<RESPONSE>(url)
×
195

196
    getNextPage = await processResponse(response.body)
×
197

198
    // for propagation, add nextLink to responseJsonBody. It's the URL that should be used in the next HTTP request to get the next page of results.
199
    const linkHeader = response.headers.get("Link")?.match(
×
200
      /<(.*?)>; rel="next"/,
×
201
    )
202
    const nextPageUrl = linkHeader ? linkHeader[1] : undefined
×
203

204
    if (!nextPageUrl) {
×
205
      getNextPage = false
×
206
    } else {
×
207
      url = nextPageUrl
×
208
    }
×
209
  }
×
210
}
×
211

212
/*
213

214
  // Make a GitHub GraphQL API request.
215

216
  Example:
217
  // const QUERY = `
218
  // query($owner: String!, $name: String!) {
219
  //   repository(owner: $owner, name: $name) {
220
  //     releases(first: 100, orderBy: {field: CREATED_AT, direction: DESC}) {
221
  //       nodes {
222
  //         name
223
  //         createdAt
224
  //         tag {
225
  //           name
226
  //           target {
227
  //             ... on Commit {
228
  //               oid
229
  //             }
230
  //           }
231
  //         }
232
  //       }
233
  //     }
234
  //   }
235
  // }
236
  // `;
237
  // const variables = {
238
  //   owner: 'REPO_OWNER', // Replace with the repository owner
239
  //   name: 'REPO_NAME'    // Replace with the repository name
240
  // };
241
*/
242
const githubGraphqlRequest = async <T>(query: string, variables: object) => {
×
243
  const headers = {
×
244
    "Authorization": `Bearer ${Deno.env.get("INPUT_GITHUB_TOKEN")}`,
×
245
    "Content-Type": "application/json",
×
246
  }
×
247

248
  const body = JSON.stringify({
×
249
    query,
×
250
    variables,
×
251
  })
×
252

253
  log.debug(
×
254
    `GitHub graphql request: headers: ${JSON.stringify(headers)}, body: ${body}`,
×
255
  )
256

257
  const response = await fetch("https://api.github.com/graphql", {
×
258
    method: "POST",
×
259
    headers,
×
260
    body,
×
261
  })
×
262

263
  if (!response.ok) {
×
264
    throw new Error(
×
265
      `Failed to call github graphql api. Given error: ${response.statusText}`,
×
266
    )
267
  }
×
268

269
  const responseJsonBody: T = await response.json()
×
270

271
  log.debug(`GitHub graphql response: ${JSON.stringify(responseJsonBody)}`)
×
272

273
  return {
×
274
    status: response.status,
×
275
    body: responseJsonBody,
×
276
    headers: response.headers,
×
277
  }
×
278
}
×
279

280
// Make a GitHub GraphQL API request that supports paging. Takes in a function that gets called for each page of results.
281
// Assumptions when using this function:
282
// 1. Query must contain a pageInfo object:
283
// pageInfo {
284
//  endCursor
285
//  hasNextPage
286
// }
287
// 2. Query contains a variable called endCursor that is used to page through results.
288
// See: https://docs.github.com/en/graphql/guides/using-pagination-in-the-graphql-api to learn more about these assumptions.
289
async function githubGraphqlRequestPaging<RESPONSE>(
×
290
  query: string,
×
291
  variables: { [key: string]: string | number },
×
292
  processResponse: (data: RESPONSE) => Promise<boolean>,
×
293
): Promise<void> {
UNCOV
294
  function findPageInfo(
×
295
    // deno-lint-ignore no-explicit-any
UNCOV
296
    _obj: any,
×
297
  ): { hasNextPage: boolean; endCursor: string } {
298
    // Create a shallow copy of the object to avoid modifying the original
299
    const obj = { ..._obj }
×
300

301
    // nodes is the JSON response. It could be a really big object. Do not perform recursion on it, so let's delete it.
302
    delete obj["nodes"]
×
303

304
    for (const key in obj) {
×
305
      if (key === "pageInfo") {
×
306
        return obj[key] as { hasNextPage: boolean; endCursor: string }
×
307
      } else {
×
308
        return findPageInfo(obj[key])
×
309
      }
×
310
    }
×
311

312
    throw new Error(
×
313
      "pageInfo object not found in response. Did you forget to add pageInfo to your graphql query?",
×
314
    )
315
  }
×
316

317
  let getNextPage = true
×
318
  while (getNextPage) {
×
319
    const response = await githubGraphqlRequest<RESPONSE>(query, variables)
×
320

321
    getNextPage = await processResponse(response.body)
×
322

323
    const pageInfo = findPageInfo(response.body)
×
324

325
    log.debug(`pageInfo: ${JSON.stringify(pageInfo)}`)
×
326

327
    if (!pageInfo.hasNextPage) {
×
328
      getNextPage = false
×
329
    } else {
×
330
      variables["endCursor"] = pageInfo.endCursor
×
331
    }
×
332
  }
×
333
}
×
334

NEW
335
const postStatusUpdateOnPullRequest = async ({ message, owner, repo, prNumber, ciBuildId }: {
×
336
  message: string
337
  owner: string
338
  repo: string
339
  prNumber: number
340
  ciBuildId: string
NEW
341
}) => {
×
NEW
342
  await cathy.speak(message, {
×
NEW
343
    githubToken: Deno.env.get("INPUT_GITHUB_TOKEN")!,
×
NEW
344
    githubRepo: `${owner}/${repo}`,
×
NEW
345
    githubIssue: prNumber,
×
NEW
346
    updateExisting: true,
×
NEW
347
    appendToExisting: true,
×
NEW
348
    updateID: ["new-deployment-tool-deploy-run-output", `new-deployment-tool-deploy-run-output-${ciBuildId}`],
×
NEW
349
  })
×
NEW
350
}
×
351

352
export interface GitHubApi {
353
  getCommitsForBranch: typeof getCommitsForBranch
354
  getPullRequestStack: typeof getPullRequestStack
355
  postStatusUpdateOnPullRequest: typeof postStatusUpdateOnPullRequest
356
}
357

358
export const GitHubApiImpl: GitHubApi = {
4✔
359
  getCommitsForBranch,
4✔
360
  getPullRequestStack,
4✔
361
  postStatusUpdateOnPullRequest,
4✔
362
}
4✔
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