Skip to content

Commit 27d4979

Browse files
committed
git.ts: extract spawn logic into a wrapper and add tests
1 parent 2c7b814 commit 27d4979

File tree

5 files changed

+181
-72
lines changed

5 files changed

+181
-72
lines changed

dist/index.js

Lines changed: 55 additions & 36 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/__tests__/git.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import {mkdirSync, rmdirSync, existsSync} from 'fs'
2+
import * as git from '../git'
3+
import * as spawn from '../spawn'
4+
import * as core from '@actions/core'
5+
6+
describe('git', () => {
7+
// We don't want to _actually_ spawn external commands, so we mock the function
8+
let spawnSpy: jest.SpyInstance
9+
// Capture the startGroup calls
10+
let coreSpy: jest.SpyInstance
11+
12+
beforeEach(() => {
13+
coreSpy = jest.spyOn(core, 'startGroup')
14+
spawnSpy = jest.spyOn(spawn, 'spawn').mockResolvedValue({
15+
// 0 is the exit code for success
16+
exitCode: 0
17+
})
18+
// We don't want to _actually_ clone the repo, so we mock the function
19+
jest.spyOn(git, 'clone').mockResolvedValue()
20+
21+
// The script removes the .tmp folder at the end, so let's create it
22+
mkdirSync('.tmp')
23+
})
24+
25+
afterEach(() => {
26+
// Clean up the .tmp folder if the script didn't already do it
27+
if (existsSync('.tmp')) {
28+
rmdirSync('.tmp', {recursive: true})
29+
}
30+
})
31+
32+
test('getViaGit build-installers x86_64', async () => {
33+
const flavor = 'build-installers'
34+
const architecture = 'x86_64'
35+
const outputDirectory = 'outputDirectory'
36+
const {artifactName, download} = await git.getViaGit(flavor, architecture)
37+
38+
expect(artifactName).toEqual('git-sdk-64-build-installers')
39+
40+
await download(outputDirectory, true)
41+
42+
expect(coreSpy).toHaveBeenCalledWith(`Creating ${flavor} artifact`)
43+
expect(spawnSpy).toHaveBeenCalledWith(
44+
expect.stringContaining('/bash.exe'),
45+
expect.arrayContaining([
46+
'.tmp/build-extra/please.sh',
47+
'create-sdk-artifact',
48+
`--architecture=${architecture}`,
49+
`--out=${outputDirectory}`
50+
]),
51+
expect.any(Object)
52+
)
53+
})
54+
55+
test('getViaGit full x86_64', async () => {
56+
const flavor = 'full'
57+
const architecture = 'x86_64'
58+
const outputDirectory = 'outputDirectory'
59+
const {artifactName, download} = await git.getViaGit(flavor, architecture)
60+
61+
expect(artifactName).toEqual('git-sdk-64-full')
62+
63+
await download(outputDirectory, true)
64+
65+
expect(coreSpy).toHaveBeenCalledWith(`Checking out git-sdk-64`)
66+
expect(spawnSpy).toHaveBeenCalledWith(
67+
expect.stringContaining('/git.exe'),
68+
expect.arrayContaining([
69+
'--git-dir=.tmp',
70+
'worktree',
71+
'add',
72+
outputDirectory
73+
]),
74+
expect.any(Object)
75+
)
76+
})
77+
})

src/git.ts

Lines changed: 19 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as core from '@actions/core'
2-
import {ChildProcess, spawn} from 'child_process'
2+
import {spawn, SpawnReturnArgs} from './spawn'
33
import {Octokit} from '@octokit/rest'
44
import {delimiter} from 'path'
55
import * as fs from 'fs'
@@ -44,14 +44,14 @@ export function getArtifactMetadata(
4444
return {repo, artifactName}
4545
}
4646

47-
async function clone(
47+
export async function clone(
4848
url: string,
4949
destination: string,
5050
verbose: number | boolean,
5151
cloneExtraOptions: string[] = []
5252
): Promise<void> {
5353
if (verbose) core.info(`Cloning ${url} to ${destination}`)
54-
const child = spawn(
54+
const child = await spawn(
5555
gitExePath,
5656
[
5757
'clone',
@@ -69,22 +69,16 @@ async function clone(
6969
stdio: [undefined, 'inherit', 'inherit']
7070
}
7171
)
72-
return new Promise<void>((resolve, reject) => {
73-
child.on('close', code => {
74-
if (code === 0) {
75-
resolve()
76-
} else {
77-
reject(new Error(`git clone: exited with code ${code}`))
78-
}
79-
})
80-
})
72+
if (child.exitCode !== 0) {
73+
throw new Error(`git clone: exited with code ${child.exitCode}`)
74+
}
8175
}
8276

8377
async function updateHEAD(
8478
bareRepositoryPath: string,
8579
headSHA: string
8680
): Promise<void> {
87-
const child = spawn(
81+
const child = await spawn(
8882
gitExePath,
8983
['--git-dir', bareRepositoryPath, 'update-ref', 'HEAD', headSHA],
9084
{
@@ -94,15 +88,9 @@ async function updateHEAD(
9488
stdio: [undefined, 'inherit', 'inherit']
9589
}
9690
)
97-
return new Promise<void>((resolve, reject) => {
98-
child.on('close', code => {
99-
if (code === 0) {
100-
resolve()
101-
} else {
102-
reject(new Error(`git: exited with code ${code}`))
103-
}
104-
})
105-
})
91+
if (child.exitCode !== 0) {
92+
throw new Error(`git: exited with code ${child.exitCode}`)
93+
}
10694
}
10795

10896
export async function getViaGit(
@@ -171,10 +159,10 @@ export async function getViaGit(
171159
])
172160
core.endGroup()
173161

174-
let child: ChildProcess
162+
let child: SpawnReturnArgs
175163
if (flavor === 'full') {
176164
core.startGroup(`Checking out ${repo}`)
177-
child = spawn(
165+
child = await spawn(
178166
gitExePath,
179167
[`--git-dir=.tmp`, 'worktree', 'add', outputDirectory, head_sha],
180168
{
@@ -196,7 +184,7 @@ export async function getViaGit(
196184

197185
core.startGroup(`Creating ${flavor} artifact`)
198186
const traceArg = verbose ? ['-x'] : []
199-
child = spawn(
187+
child = await spawn(
200188
`${gitForWindowsUsrBinPath}/bash.exe`,
201189
[
202190
...traceArg,
@@ -222,16 +210,12 @@ export async function getViaGit(
222210
}
223211
)
224212
}
225-
return new Promise<void>((resolve, reject) => {
226-
child.on('close', code => {
227-
core.endGroup()
228-
if (code === 0) {
229-
fs.rm('.tmp', {recursive: true}, () => resolve())
230-
} else {
231-
reject(new Error(`process exited with code ${code}`))
232-
}
233-
})
234-
})
213+
core.endGroup()
214+
if (child.exitCode === 0) {
215+
fs.rmSync('.tmp', {recursive: true})
216+
} else {
217+
throw new Error(`process exited with code ${child.exitCode}`)
218+
}
235219
}
236220
}
237221
}

src/spawn.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import {
2+
spawn as SpawnInternal,
3+
SpawnOptionsWithStdioTuple,
4+
StdioNull,
5+
StdioPipe
6+
} from 'child_process'
7+
8+
export type SpawnReturnArgs = {
9+
exitCode: number | null
10+
}
11+
12+
/**
13+
* Simple wrapper around NodeJS's "child_process.spawn" function.
14+
* Since we only use the exit code, we only expose that.
15+
*/
16+
export async function spawn(
17+
command: string,
18+
args: readonly string[],
19+
options: SpawnOptionsWithStdioTuple<StdioPipe, StdioNull, StdioNull>
20+
): Promise<SpawnReturnArgs> {
21+
const child = SpawnInternal(command, args, options)
22+
23+
return new Promise<SpawnReturnArgs>((resolve, reject) => {
24+
child.on('error', reject)
25+
child.on('close', code => {
26+
resolve({exitCode: code})
27+
})
28+
})
29+
}

0 commit comments

Comments
 (0)