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

levibostian / new-deployment-tool / 15876458398

25 Jun 2025 12:33PM UTC coverage: 71.617% (-0.03%) from 71.648%
15876458398

Pull #68

github

levibostian
refactor: should run on other CI services now

change the runtime behavior to no longer only be compatible with github actions.

change deno permissions to allow access to all environment variables. because of the env-ci package we are using to try and be compatible with other environments, we need access to dozens of environment variables now. We might need to also give access to read and write to many more paths, but for now I want to leave it without.
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 %.

5 of 9 new or added lines in 3 files covered. (55.56%)

61 existing lines in 2 files now uncovered.

584 of 824 relevant lines covered (70.87%)

9.19 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 (
×
NEW
41
  { owner, repo, startingPrNumber }: { owner: string; repo: string; startingPrNumber: number },
×
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).
NEW
UNCOV
94
  const startingPullRequest = pullRequests.find((pr) => pr.prNumber === startingPrNumber)
×
95
  if (!startingPullRequest) {
×
96
    throw new Error(
×
NEW
97
      `Could not get pull request stack because not able to find pull request for starting PR number, ${startingPrNumber}. This is unexpected.`,
×
98
    )
UNCOV
99
  }
×
100

UNCOV
101
  const prStack: GitHubPullRequest[] = [startingPullRequest]
×
NEW
102
  let sourceBranchSearchingFor = startingPullRequest.sourceBranchName
×
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(
×
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