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

levibostian / new-deployment-tool / 15806498661

22 Jun 2025 12:18PM UTC coverage: 72.355% (+0.7%) from 71.648%
15806498661

Pull #61

github

web-flow
Merge 09d4c4b61 into d2a027e86
Pull Request #61: refactor: make exec.run more reliable and easier to use

66 of 82 branches covered (80.49%)

Branch coverage included in aggregate %.

12 of 16 new or added lines in 1 file covered. (75.0%)

1 existing line in 1 file now uncovered.

570 of 797 relevant lines covered (71.52%)

9.25 hits per line

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

76.54
/lib/exec.ts
1
import * as log from "./log.ts"
5✔
2
import { AnyStepInput } from "./types/environment.ts"
3
import $ from "@david/dax"
5✔
4

5
export interface RunResult {
6
  exitCode: number
7
  stdout: string
8
  output: Record<string, unknown> | undefined
9
}
10

11
export interface Exec {
12
  run: (
13
    { command, input, displayLogs }: {
14
      command: string
15
      input: AnyStepInput | undefined
16
      displayLogs?: boolean
17
      envVars?: { [key: string]: string }
18
      throwOnNonZeroExitCode?: boolean
19
    },
20
  ) => Promise<RunResult>
21
}
22

23
/*
24
Executes a command and returns the exit code and the stdout of the command.
25

26
The entire command is passed in as 1 string. This is for convenience but also because when used as a github action, the commands will be passed to the tool as a single string.
27
Then, the command is split into the command and the arguments. This is required by Deno.Command.
28
You cannot simply split the string by spaces to create the args list. Example, if you're given: `python3 -c "import os; print(os.getenv('INPUT'))"` we expect only 2 args: "-c" and "import ...".
29
We use a popular package to parse the string into the correct args list. See automated tests to verify that this works as expected.
30

31
To make this function testable, we not only have the stdout and stderr be piped to the console, but we return it from this function so tests can verify the output of the command.
32
*/
33
const run = async (
5✔
34
  { command, input, displayLogs, envVars, throwOnNonZeroExitCode }: {
5✔
35
    command: string
36
    input: AnyStepInput | undefined
37
    displayLogs?: boolean
38
    envVars?: { [key: string]: string }
39
    throwOnNonZeroExitCode?: boolean
40
  },
5✔
41
): Promise<RunResult> => {
UNCOV
42
  if (displayLogs) {
×
43
    log.message(` $> ${command}`)
×
44
  } else {
×
45
    log.debug(` $> ${command}`)
13✔
46
  }
13✔
47

48
  const environmentVariablesToPassToCommand: { [key: string]: string } = envVars || {}
13✔
49

50
  // For some features to work, we need to communicate with the command. We need to send data to it and read data that it produces.
51
  // We use JSON as the data format to communicate with the command since pretty much every language has built-in support for it.
52
  // Since we are creating subprocesses to run the command, we are limited in how we can communicate with the command.
53
  // One common way would be to ask the subprocess to stdout a JSON string that we simply read, but this tool tries to promote stdout
54
  // as a way to communicate with the user, not the tool. So instead, we write the JSON to a file and pass the file path to the command.
55
  let tempFilePathToCommunicateWithCommand: string | undefined
13✔
56
  let inputDataFileContents: string | undefined
13✔
57
  if (input) {
13✔
58
    tempFilePathToCommunicateWithCommand = await Deno.makeTempFile({
13✔
59
      prefix: "new-deployment-tool-",
13✔
60
      suffix: ".json",
13✔
61
    })
13✔
62
    inputDataFileContents = JSON.stringify(input)
13✔
63
    await Deno.writeTextFile(
13✔
64
      tempFilePathToCommunicateWithCommand,
13✔
65
      inputDataFileContents,
13✔
66
    )
67

68
    environmentVariablesToPassToCommand["DATA_FILE_PATH"] = tempFilePathToCommunicateWithCommand
13✔
69
  }
13✔
70

71
  const result = await $.raw`${command}`
13✔
72
    .env(environmentVariablesToPassToCommand)
13✔
73
    .stdout("piped")
13✔
74
    .stderr("piped")
13✔
75
    .noThrow()
13✔
76

77
  let capturedStdout = result.stdout.trim()
13✔
78
  let capturedStderr = result.stderr.trim()
13✔
79
  let code = result.code
13✔
80

NEW
81
  if (displayLogs) {
×
NEW
82
    log.message(capturedStdout)
×
NEW
83
    log.message(capturedStderr)
×
NEW
84
  } else {
×
85
    log.debug(capturedStdout)
13✔
86
    log.debug(capturedStderr)
13✔
87
  }
13✔
88

89
  let commandOutput: Record<string, unknown> | undefined = undefined
13✔
90

91
  if (tempFilePathToCommunicateWithCommand) {
13✔
92
    const outputDataFileContents = await Deno.readTextFile(
13✔
93
      tempFilePathToCommunicateWithCommand,
13✔
94
    )
95
    const commandOutputUntyped = JSON.parse(outputDataFileContents)
13✔
96
    // As long as the command wrote something to the file, we will use it.
97
    if (
13✔
98
      outputDataFileContents !== inputDataFileContents &&
13✔
99
      typeof commandOutputUntyped === "object" &&
13✔
100
      commandOutputUntyped !== null &&
13✔
101
      !Array.isArray(commandOutputUntyped)
13✔
102
    ) { // there is a chance that the command did not write to the file or they have a bug.
13✔
103
      commandOutput = commandOutputUntyped
14✔
104
    }
14✔
105
  }
13✔
106

107
  log.debug(
13✔
108
    `exit code, ${code}, command output: ${JSON.stringify(commandOutput)}`,
13✔
109
  )
110

111
  let shouldThrowError = true
13✔
112
  if (throwOnNonZeroExitCode !== undefined && throwOnNonZeroExitCode == false) {
×
113
    shouldThrowError = false
×
114
  }
×
115

116
  if (code !== 0 && shouldThrowError) {
×
117
    throw new Error(`Command: ${command}, failed with exit code: ${code}, output: ${capturedStdout}, stderr: ${capturedStderr}`)
×
118
  }
×
119

120
  return {
13✔
121
    exitCode: code,
13✔
122
    stdout: capturedStdout,
13✔
123
    output: commandOutput,
13✔
124
  }
13✔
125
}
5✔
126

127
export const exec: Exec = {
5✔
128
  run,
5✔
129
}
5✔
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