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

levibostian / new-deployment-tool / 15876053592

25 Jun 2025 12:15PM UTC coverage: 71.648%. Remained the same
15876053592

Pull #68

github

web-flow
Merge 409be8169 into 2f103da89
Pull Request #68: feat: tool is a cli. run it by passing in args.

67 of 85 branches covered (78.82%)

Branch coverage included in aggregate %.

17 of 20 new or added lines in 4 files covered. (85.0%)

62 existing lines in 2 files now uncovered.

585 of 825 relevant lines covered (70.91%)

9.21 hits per line

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

3.18
/lib/github-api.ts
1
import * as log from "./log.ts"
4✔
2

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

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

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

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

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

UNCOV
64
  let pullRequests: GitHubPullRequest[] = []
×
65

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

93
  // 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).
UNCOV
94
  const startingPullRequest = pullRequests.find((pr) => pr.sourceBranchName === startingBranch)
×
NEW
95
  if (!startingPullRequest) {
×
96
    throw new Error(
×
97
      `Could not get pull request stack because not able to find pull request for starting branch, ${startingBranch}. This is unexpected.`,
×
98
    )
UNCOV
99
  }
×
100

UNCOV
101
  const prStack: GitHubPullRequest[] = [startingPullRequest]
×
102
  let sourceBranchSearchingFor = startingBranch
×
103

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

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

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

UNCOV
115
  return prStack
×
116
}
×
117

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

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

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

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

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

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

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

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

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

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

UNCOV
195
    getNextPage = await processResponse(response.body)
×
196

197
    // 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.
UNCOV
198
    const linkHeader = response.headers.get("Link")?.match(
×
199
      /<(.*?)>; rel="next"/,
×
200
    )
UNCOV
201
    const nextPageUrl = linkHeader ? linkHeader[1] : undefined
×
202

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

211
/*
212

213
  // Make a GitHub GraphQL API request.
214

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

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

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

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

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

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

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

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

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

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

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

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

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

UNCOV
320
    getNextPage = await processResponse(response.body)
×
321

UNCOV
322
    const pageInfo = findPageInfo(response.body)
×
323

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

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

334
export interface GitHubApi {
335
  getCommitsForBranch: typeof getCommitsForBranch
336
  getPullRequestStack: typeof getPullRequestStack
337
}
338

339
export const GitHubApiImpl: GitHubApi = {
4✔
340
  getCommitsForBranch,
4✔
341
  getPullRequestStack,
4✔
342
}
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