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

levibostian / new-deployment-tool / 16143267533

08 Jul 2025 12:30PM UTC coverage: 66.972%. Remained the same
16143267533

Pull #73

github

levibostian
fix: simulated merge when running in non-github actions environments

When we run the tool in Circle CI, an error gets thrown saying that it can't Create the stack of pull requests successfully from the GitHub API response. I believe this is a issue with the environment variables available from GitHub Actions or CircleCI. and the data types not being converted successfully with the module that we use. Forcing the value to be a number instead of keeping it as is, I think, should fix this issue.
Pull Request #73: fix: simulated merge when running in non-github actions environments

67 of 92 branches covered (72.83%)

Branch coverage included in aggregate %.

0 of 1 new or added line in 1 file covered. (0.0%)

14 existing lines in 1 file now uncovered.

663 of 998 relevant lines covered (66.43%)

7.94 hits per line

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

82.64
/lib/git.ts
1
import { exec } from "./exec.ts"
2
import { Exec } from "./exec.ts"
3
import { GitHubCommit } from "./github-api.ts"
4
import * as log from "./log.ts"
2✔
5

6
export interface Git {
7
  checkoutBranch: (
8
    { exec, branch, createBranchIfNotExist }: { exec: Exec; branch: string; createBranchIfNotExist: boolean },
9
  ) => Promise<void>
10
  merge: (
11
    { exec, branchToMergeIn, commitTitle, commitMessage, fastForward }: {
12
      exec: Exec
13
      branchToMergeIn: string
14
      commitTitle: string
15
      commitMessage: string
16
      fastForward?: "--no-ff" | "--ff-only"
17
    },
18
  ) => Promise<void>
19
  pull: ({ exec }: { exec: Exec }) => Promise<void>
20
  setUser: (
21
    { exec, name, email }: { exec: Exec; name: string; email: string },
22
  ) => Promise<void>
23
  squash: (
24
    { exec, branchToSquash, branchMergingInto, commitTitle, commitMessage }: {
25
      exec: Exec
26
      branchToSquash: string
27
      branchMergingInto: string
28
      commitTitle: string
29
      commitMessage: string
30
    },
31
  ) => Promise<void>
32
  rebase: (
33
    { exec, branchToRebaseOnto }: { exec: Exec; branchToRebaseOnto: string },
34
  ) => Promise<void>
35
  getLatestCommitsSince({ exec, commit }: { exec: Exec; commit: GitHubCommit }): Promise<GitHubCommit[]>
36
  getLatestCommitOnBranch({ exec, branch }: { exec: Exec; branch: string }): Promise<GitHubCommit>
37
  createLocalBranchFromRemote: ({ exec, branch }: { exec: Exec; branch: string }) => Promise<void>
38
  getCommits: ({ exec, branch }: { exec: Exec; branch: string }) => Promise<GitHubCommit[]>
39
}
40

41
const checkoutBranch = async (
2✔
42
  { exec, branch, createBranchIfNotExist }: { exec: Exec; branch: string; createBranchIfNotExist: boolean },
2✔
43
): Promise<void> => {
44
  await exec.run({
16✔
45
    command: `git checkout ${createBranchIfNotExist ? "-b " : ""}${branch}`,
16✔
46
    input: undefined,
16✔
47
  })
16✔
48
}
2✔
49

50
const merge = async (
2✔
51
  { exec, branchToMergeIn, commitTitle, commitMessage, fastForward }: {
2✔
52
    exec: Exec
53
    branchToMergeIn: string
54
    commitTitle: string
55
    commitMessage: string
56
    fastForward?: "--no-ff" | "--ff-only"
57
  },
2✔
58
): Promise<void> => {
59
  await exec.run({
5✔
60
    command: `git merge ${branchToMergeIn} -m "${commitTitle}" -m "${commitMessage}" ${fastForward || ""}`,
×
61
    input: undefined,
5✔
62
  })
5✔
63
}
2✔
64

65
const pull = async ({ exec }: { exec: Exec }): Promise<void> => {
2✔
66
  await exec.run({
8✔
67
    command: `git pull`,
8✔
68
    input: undefined,
8✔
69
  })
8✔
70
}
2✔
71

72
const setUser = async (
2✔
73
  { exec, name, email }: { exec: Exec; name: string; email: string },
2✔
74
): Promise<void> => {
75
  await exec.run({
5✔
76
    command: `git config user.name "${name}"`,
5✔
77
    input: undefined,
5✔
78
  })
5✔
79

80
  await exec.run({
5✔
81
    command: `git config user.email "${email}"`,
5✔
82
    input: undefined,
5✔
83
  })
5✔
84
}
2✔
85

86
// Squash all commits of a branch into 1 commit
87
const squash = async (
2✔
88
  { exec, branchToSquash, branchMergingInto, commitTitle, commitMessage }: {
2✔
89
    exec: Exec
90
    branchToSquash: string
91
    branchMergingInto: string
92
    commitTitle: string
93
    commitMessage: string
94
  },
2✔
95
): Promise<void> => {
96
  // We need to find out how many commits 1 branch is ahead of the other to find out how many unique commits there are.
97
  const { stdout } = await exec.run({
3✔
98
    command: `git rev-list --count ${branchMergingInto}..${branchToSquash}`,
3✔
99
    input: undefined,
3✔
100
  })
3✔
101

102
  const numberOfCommitsAheadOfBranchMergingInto = parseInt(stdout.trim())
3✔
103

UNCOV
104
  if (numberOfCommitsAheadOfBranchMergingInto === 0) {
×
UNCOV
105
    log.message(`Branches ${branchToSquash} and ${branchMergingInto} are already up to date. No commits to squash.`)
×
UNCOV
106
    return
×
UNCOV
107
  }
×
108

109
  // Now that we know how many commits are ahead, we can squash all of those commits into 1 commit.
110
  await exec.run({
3✔
111
    command: `git reset --soft HEAD~${numberOfCommitsAheadOfBranchMergingInto}`,
3✔
112
    input: undefined,
3✔
113
  })
3✔
114
  await exec.run({
3✔
115
    command: `git commit -m "${commitTitle}" -m "${commitMessage}"`,
3✔
116
    input: undefined,
3✔
117
  })
3✔
118
}
2✔
119

120
const rebase = async (
2✔
121
  { exec, branchToRebaseOnto }: { exec: Exec; branchToRebaseOnto: string },
2✔
122
): Promise<void> => {
123
  await exec.run({
4✔
124
    command: `git rebase ${branchToRebaseOnto}`,
4✔
125
    input: undefined,
4✔
126
  })
4✔
127
}
2✔
128

129
const getLatestCommitsSince = async (
2✔
130
  { exec, commit }: { exec: Exec; commit: GitHubCommit },
2✔
131
): Promise<GitHubCommit[]> => {
132
  const { stdout } = await exec.run({
5✔
133
    command: `git log --pretty=format:"%H|%s|%ci" ${commit.sha}..HEAD`,
5✔
134
    input: undefined,
5✔
135
  })
5✔
136

137
  return stdout.trim().split("\n").map((commitString) => {
5✔
138
    const [sha, message, dateString] = commitString.split("|")
8✔
139

140
    return { sha, message, date: new Date(dateString) }
40✔
141
  })
5✔
142
}
2✔
143

144
const getLatestCommitOnBranch = async (
2✔
145
  { exec, branch }: { exec: Exec; branch: string },
2✔
146
): Promise<GitHubCommit> => {
147
  const { stdout } = await exec.run({
5✔
148
    command: `git log -1 --pretty=format:"%H|%s|%ci" ${branch}`,
5✔
149
    input: undefined,
5✔
150
  })
5✔
151

152
  const [sha, message, dateString] = stdout.trim().split("|")
5✔
153

154
  return { sha, message, date: new Date(dateString) }
25✔
155
}
2✔
156

157
/**
158
 * Makes sure that we have a local branch that has all of the commits that the remote branch has.
159
 *
160
 * There are a lot of commands here, just to get a local branch of a remote branch. After many attempts to simplify this, we would hit many different errors.
161
 * I believe that complexity comes because we run this tool on a CI server where the git config might be different.
162
 * Running all of these commands and running each command by itself (example: not running `git checkout -b` to try and combine creating a branch and checking it out)
163
 * have given the most consistent results.
164
 */
165
const createLocalBranchFromRemote = async (
2✔
166
  { exec, branch }: { exec: Exec; branch: string },
2✔
167
): Promise<void> => {
168
  const currentBranchName = (await exec.run({
4✔
169
    command: `git branch --show-current`,
4✔
170
    input: undefined,
4✔
171
  })).stdout.trim()
4✔
172

173
  // Perform a fetch, otherwise you might get errors about origin branch not being found.
174
  await exec.run({
5✔
175
    command: `git fetch origin`,
5✔
176
    input: undefined,
5✔
177
  })
5✔
178

179
  // Create a local branch that tracks the remote branch.
180
  await exec.run({
5✔
181
    command: `git branch --track ${branch} origin/${branch}`,
5✔
182
    input: undefined,
5✔
183
  })
5✔
184

185
  // Checkout the branch so we can pull it.
186
  await exec.run({
5✔
187
    command: `git checkout ${branch}`,
5✔
188
    input: undefined,
5✔
189
  })
5✔
190

191
  // Pull the branch from the remote.
192
  // Adding --no-rebase to avoid an error that could happen when you run pull.
193
  // The error is: You have divergent branches and need to specify how to reconcile them.
194
  await exec.run({
5✔
195
    command: `git pull --no-rebase origin ${branch}`,
5✔
196
    input: undefined,
5✔
197
  })
5✔
198

199
  // Switch back to the branch we were on before.
200
  await exec.run({
5✔
201
    command: `git checkout ${currentBranchName}`,
5✔
202
    input: undefined,
5✔
203
  })
5✔
204
}
2✔
205

UNCOV
206
const getCommits = async (
×
UNCOV
207
  { exec, branch }: { exec: Exec; branch: string },
×
208
): Promise<GitHubCommit[]> => {
UNCOV
209
  const currentBranchName = (await exec.run({
×
UNCOV
210
    command: `git branch --show-current`,
×
UNCOV
211
    input: undefined,
×
UNCOV
212
  })).stdout.trim()
×
213

UNCOV
214
  await checkoutBranch({ exec, branch, createBranchIfNotExist: false })
×
215

UNCOV
216
  const { stdout } = await exec.run({
×
217
    command: `git log --pretty=format:"%H|%s|%ci"`,
×
218
    input: undefined,
×
UNCOV
219
  })
×
220

221
  const commits = stdout.trim().split("\n").map((commitString) => {
×
222
    const [sha, message, dateString] = commitString.split("|")
×
223

UNCOV
224
    return { sha, message, date: new Date(dateString) }
×
225
  })
×
226

227
  await checkoutBranch({ exec, branch: currentBranchName, createBranchIfNotExist: false })
×
228

229
  return commits
×
230
}
×
231

232
export const git: Git = {
2✔
233
  checkoutBranch,
2✔
234
  merge,
2✔
235
  pull,
2✔
236
  setUser,
2✔
237
  squash,
2✔
238
  rebase,
2✔
239
  getCommits,
2✔
240
  getLatestCommitsSince,
2✔
241
  getLatestCommitOnBranch,
2✔
242
  createLocalBranchFromRemote,
2✔
243
}
2✔
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