From 095ce5d0bbce0cdb2ad33eb047a44f7779427073 Mon Sep 17 00:00:00 2001 From: Orion Edwards Date: Mon, 18 Jul 2022 17:52:00 +1200 Subject: [PATCH] fix: Environment variables from the GitHub action context were not passed through to the underlying Octopus CLI fix: StdError and the process exit code returned by the CLI are now shown in Github Action runs refactor: Sync codebase with create-release-action closes #248 #249 --- .prettierrc.json | 7 +- __tests__/integration/cleanup-helper.ts | 12 +- __tests__/integration/integration.test.ts | 208 ++++-- __tests__/test-helpers.ts | 22 + __tests__/unit/cli-utils.test.ts | 123 ++++ .../ctopus-cli-commandline-generation.test.ts | 312 --------- ...octopus-cli-commandline-generation.test.ts | 80 +++ .../unit/octopus-cli-output-parsing.test.ts | 45 +- dist/index.js | 241 ++++--- package-lock.json | 604 ++++++++++-------- package.json | 8 +- src/cli-util.ts | 65 ++ src/input-parameters.ts | 6 +- src/main.ts | 19 +- src/octopus-cli-wrapper.ts | 292 ++++----- tsconfig.json | 4 +- 16 files changed, 1087 insertions(+), 961 deletions(-) create mode 100644 __tests__/test-helpers.ts create mode 100644 __tests__/unit/cli-utils.test.ts delete mode 100644 __tests__/unit/ctopus-cli-commandline-generation.test.ts create mode 100644 __tests__/unit/octopus-cli-commandline-generation.test.ts create mode 100644 src/cli-util.ts diff --git a/.prettierrc.json b/.prettierrc.json index c34bafcb..e51c0399 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,10 +1,11 @@ { - "printWidth": 80, + "printWidth": 120, "tabWidth": 2, "useTabs": false, "semi": false, "singleQuote": true, "trailingComma": "none", - "bracketSpacing": false, - "arrowParens": "avoid" + "bracketSpacing": true, + "arrowParens": "avoid", + "endOfLine": "auto" } diff --git a/__tests__/integration/cleanup-helper.ts b/__tests__/integration/cleanup-helper.ts index d8ff6b10..fbbee2b7 100644 --- a/__tests__/integration/cleanup-helper.ts +++ b/__tests__/integration/cleanup-helper.ts @@ -3,7 +3,7 @@ type Callback = () => unknown export class CleanupHelper { #actions: Callback[] = [] - add(callback: Callback): void { + add(callback: Callback) { this.#actions.push(callback) } @@ -15,11 +15,15 @@ export class CleanupHelper { toExecute.reverse() this.#actions = [] - for (const a of toExecute) { + for (let a of toExecute) { try { const result = a() - if (result && result instanceof Promise) { - await result + if (result && typeof result === 'object') { + const resultObj: any = result + if (typeof resultObj?.then === 'function') { + // it's a promise! + await resultObj + } } } catch (e: unknown) { console.error(`ERROR DURING CLEANUP!\n${e}`) diff --git a/__tests__/integration/integration.test.ts b/__tests__/integration/integration.test.ts index fbc373bd..08c8e4a5 100644 --- a/__tests__/integration/integration.test.ts +++ b/__tests__/integration/integration.test.ts @@ -1,11 +1,7 @@ -import {makeInputParameters} from '../../src/input-parameters' -import { - Client, - ClientConfiguration, - Repository -} from '@octopusdeploy/api-client' -import {randomBytes} from 'crypto' -import {CleanupHelper} from './cleanup-helper' +import { makeInputParameters } from '../../src/input-parameters' +import { Client, ClientConfiguration, Repository } from '@octopusdeploy/api-client' +import { randomBytes } from 'crypto' +import { CleanupHelper } from './cleanup-helper' import { GuidedFailureMode, PackageRequirement, @@ -14,24 +10,44 @@ import { StartTrigger, TenantedDeploymentMode } from '@octopusdeploy/message-contracts' -import {RunConditionForAction} from '@octopusdeploy/message-contracts/dist/runConditionForAction' -import {RunbookEnvironmentScope} from '@octopusdeploy/message-contracts/dist/runbookEnvironmentScope' -import {setOutput} from '@actions/core' -import {OctopusCliWrapper} from '../../src/octopus-cli-wrapper' +import { RunConditionForAction, RunbookEnvironmentScope } from '@octopusdeploy/message-contracts' +import { setOutput } from '@actions/core' +import { runRunbook } from '../../src/octopus-cli-wrapper' +import { CaptureOutput } from '../test-helpers' +import { platform, tmpdir } from 'os' +import { chmodSync, mkdirSync, rmSync, writeFileSync } from 'fs' +import { join as pathJoin } from 'path' const octoExecutable = process.env.OCTOPUS_TEST_CLI_PATH || 'octo' // if 'octo' isn't in your system path, you can override it for tests here +const isWindows = platform().includes('win') + const apiClientConfig: ClientConfiguration = { - apiKey: - process.env.OCTOPUS_TEST_APIKEY || 'API-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', + apiKey: process.env.OCTOPUS_TEST_APIKEY || 'API-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', apiUri: process.env.OCTOPUS_TEST_URL || 'http://localhost:8050' } +// experimental. Should probably be a custom jest matcher +function expectMatchAll(actual: string[], expected: (string | RegExp)[]) { + for (let i = 0; i < Math.min(expected.length, actual.length); i++) { + const a = actual[i] + const e = expected[i] + if (e instanceof RegExp) { + expect(a).toMatch(e) + } else { + expect(a).toEqual(e) + } + } + expect(actual.length).toEqual(expected.length) +} + describe('integration tests', () => { const runId = randomBytes(16).toString('hex') + const globalCleanup = new CleanupHelper() + const localProjectName = `project${runId}` - const standardInput = makeInputParameters({ + const standardInputParameters = makeInputParameters({ project: localProjectName, apiKey: apiClientConfig.apiKey, server: apiClientConfig.apiUri @@ -54,12 +70,9 @@ describe('integration tests', () => { LifecycleId: lifeCycle.Id, ProjectGroupId: projectGroup.Id }) - standardInput.project = project.Id + standardInputParameters.project = project.Id globalCleanup.add(async () => repository.projects.del(project)) - const deploymentProcess = await repository.deploymentProcesses.get( - project.DeploymentProcessId, - undefined - ) + const deploymentProcess = await repository.deploymentProcesses.get(project.DeploymentProcessId, undefined) deploymentProcess.Steps = [ { Condition: RunCondition.Success, @@ -68,7 +81,7 @@ describe('integration tests', () => { StartTrigger: StartTrigger.StartAfterPrevious, Id: '', Name: `step1-${runId}`, - Properties: {'Octopus.Action.TargetRoles': 'deploy'}, + Properties: { 'Octopus.Action.TargetRoles': 'deploy' }, Actions: [ { Id: '', @@ -101,10 +114,7 @@ describe('integration tests', () => { ] } ] - await repository.deploymentProcesses.saveToProject( - project, - deploymentProcess - ) + await repository.deploymentProcesses.saveToProject(project, deploymentProcess) const runbook = await repository.runbooks.create({ ProjectId: project.Id, @@ -119,10 +129,7 @@ describe('integration tests', () => { } }) globalCleanup.add(async () => repository.runbooks.del(runbook)) - const runbookProcess = await repository.runbookProcess.get( - runbook.RunbookProcessId, - undefined - ) + const runbookProcess = await repository.runbookProcess.get(runbook.RunbookProcessId, undefined) runbookProcess.Steps = [ { Condition: RunCondition.Success, @@ -131,7 +138,7 @@ describe('integration tests', () => { StartTrigger: StartTrigger.StartAfterPrevious, Id: '', Name: `Run a Script`, - Properties: {'Octopus.Action.TargetRoles': 'deploy'}, + Properties: { 'Octopus.Action.TargetRoles': 'deploy' }, Actions: [ { Id: '', @@ -171,39 +178,146 @@ describe('integration tests', () => { snapshot = await repository.runbookSnapshots.create(snapshot, { publish: true }) - standardInput.runbook = runbook.Id + standardInputParameters.runbook = runbook.Id const env = await repository.environments.create({ Name: `Test-${runId}` }) globalCleanup.add(async () => repository.environments.del(env)) - standardInput.environments = env.Id - // Added sometime to wait for the runbook to finish running before cleanup - globalCleanup.add(async () => new Promise(r => setTimeout(r, 2500))) + standardInputParameters.environments = env.Id + + globalCleanup.add(async () => { + // Added some time to wait for the runbook to finish running before cleanup + return new Promise(r => setTimeout(r, 2500)) + }) }) afterAll(async () => { if (process.env.GITHUB_ACTIONS) { - setOutput('gha_selftest_project_name', standardInput.project) - setOutput('gha_selftest_environments', standardInput.environments) - setOutput('gha_selftest_runbook', standardInput.runbook) + setOutput('gha_selftest_project_name', standardInputParameters.project) + setOutput('gha_selftest_environments', standardInputParameters.environments) + setOutput('gha_selftest_runbook', standardInputParameters.runbook) } else { await globalCleanup.cleanup() } }) test('can run runbook', async () => { - const messages: string[] = [] - const w = new OctopusCliWrapper( - standardInput, - {}, - m => messages.push(m), - m => messages.push(m) - ) - await w.runRunbook(octoExecutable) - console.log('Got: ', messages) + const output = new CaptureOutput() + await runRunbook({ parameters: standardInputParameters, env: {} }, output, octoExecutable) + + console.log('Got: ', output.getAllMessages()) // The CLI outputs a diffrent amount of inputs with diffrent values // everytime it runs. As we will be moving away from // the CLI we will just check that the CLI outpus something. - expect(messages.length).toBeGreaterThan(0) + expect(output.infos.length).toBeGreaterThan(0) + }) + + test('fails with error if CLI executable not found', async () => { + const output = new CaptureOutput() + try { + await runRunbook({ parameters: standardInputParameters, env: {} }, output, 'not-octo') + throw new Error('should not get here: expecting runRunbook to throw an exception') + } catch (err: any) { + expect(err.message).toMatch( + // regex because the error prints the underlying nodejs error which has different text on different platforms, and we're not worried about + // asserting on that + new RegExp( + "Octopus CLI executable missing. Ensure you have added the 'OctopusDeploy/install-octopus-cli-action@v1' step to your GitHub actions workflow" + ) + ) + } + + expect(output.getAllMessages()).toEqual([]) + }) + + test('fails picks up stderr from executable as well as return codes', async () => { + const output = new CaptureOutput() + + let tmpDirPath = pathJoin(tmpdir(), runId) + mkdirSync(tmpDirPath) + + let exePath: string + if (isWindows) { + const fileContents = + '@echo off\n' + 'echo An informational Message\n' + 'echo An error message 1>&2\n' + 'exit /b 37' + exePath = pathJoin(tmpDirPath, 'erroring_executable.cmd') + writeFileSync(exePath, fileContents) + } else { + const fileContents = 'echo An informational Message\n' + '>&2 echo "An error message "\n' + '(exit 37)' + exePath = pathJoin(tmpDirPath, 'erroring_executable.sh') + writeFileSync(exePath, fileContents) + chmodSync(exePath, '755') + } + + const expectedExitCode = 37 + try { + await runRunbook({ parameters: standardInputParameters, env: {} }, output, exePath) + throw new Error('should not get here: expecting runRunbook to throw an exception') + } catch (err: any) { + expect(err.message).toMatch( + new RegExp(`The process .*erroring_executable.* failed with exit code ${expectedExitCode}`) + ) + } finally { + rmSync(tmpDirPath, { recursive: true }) + } + + expect(output.infos).toEqual(['An informational Message']) + expect(output.warns).toEqual(['An error message ']) // trailing space is deliberate because of windows bat file + }) + + test('fails with error if CLI returns an error code', async () => { + const output = new CaptureOutput() + + const expectedExitCode = isWindows ? 4294967295 : 255 // Process should return -1 which maps to 4294967295 on windows or 255 on linux + const cliInputs = { + parameters: makeInputParameters({ + // no project + apiKey: apiClientConfig.apiKey, + server: apiClientConfig.apiUri + }), + env: {} + } + + try { + await runRunbook(cliInputs, output, octoExecutable) + throw new Error('should not get here: expecting runRunbook to throw an exception') + } catch (err: any) { + expect(err.message).toMatch( + // regex because when run locally the output logs 'octo' but in GHA it logs '/opt/hostedtoolcache/octo/9.1.3/x64/octo' + new RegExp(`The process .*octo.* failed with exit code ${expectedExitCode}`) + ) + } + + expect(output.warns).toEqual([]) + console.log('Got: ', output.getAllMessages()) + expect(output.infos.length).toBeGreaterThan(0) + }) + + test('fails with error if CLI returns an error code (bad auth)', async () => { + const output = new CaptureOutput() + + const expectedExitCode = isWindows ? 4294967291 : 2 // Process should return -3 which maps to 4294967291 on windows or 2 on linux + + const cliInputs = { + parameters: makeInputParameters({ + project: localProjectName, + apiKey: apiClientConfig.apiKey + 'ZZZ', + server: apiClientConfig.apiUri + }), + env: {} + } + + try { + await runRunbook(cliInputs, output, octoExecutable) + throw new Error('should not get here: expecting runRunbook to throw an exception') + } catch (err: any) { + expect(err.message).toMatch( + // regex because when run locally the output logs 'octo' but in GHA it logs '/opt/hostedtoolcache/octo/9.1.3/x64/octo' + new RegExp(`The process .*octo.* failed with exit code ${expectedExitCode}`) + ) + } + + expect(output.warns).toEqual([]) + expect(output.infos[output.infos.length - 1]).toEqual('Exit code: -5') }) }) diff --git a/__tests__/test-helpers.ts b/__tests__/test-helpers.ts new file mode 100644 index 00000000..32121a61 --- /dev/null +++ b/__tests__/test-helpers.ts @@ -0,0 +1,22 @@ +import { CliOutput } from '../src/cli-util' + +export class CaptureOutput implements CliOutput { + infos: string[] + warns: string[] + + constructor() { + this.infos = [] + this.warns = [] + } + + info(message: string) { + this.infos.push(message) + } + warn(message: string) { + this.warns.push(message) + } + + getAllMessages(): string[] { + return this.infos.concat(this.warns) + } +} diff --git a/__tests__/unit/cli-utils.test.ts b/__tests__/unit/cli-utils.test.ts new file mode 100644 index 00000000..7e34f75f --- /dev/null +++ b/__tests__/unit/cli-utils.test.ts @@ -0,0 +1,123 @@ +import { pickupConfigurationValue, pickupConfigurationValueExtended } from '../../src/cli-util' +import { CaptureOutput } from '../test-helpers' + +describe('pickupConfigurationValueExtended', () => { + let output: CaptureOutput + let values: any[] + beforeEach(() => { + output = new CaptureOutput() + values = [] + }) + + test('pickup from input', () => { + pickupConfigurationValueExtended(output, {}, 'VALUE FromInput', '', '', v => values.push(v)) + + expect(values).toEqual(['VALUE FromInput']) + + expect(output.getAllMessages()).toEqual([]) // no messages so we don't need to disambiguate types + }) + + test('pickup from env', () => { + pickupConfigurationValueExtended( + output, + { ENV_VAR_NAME: 'VALUE FromEnv' }, + '', + 'OLD_ENV_VAR_NAME', + 'ENV_VAR_NAME', + v => values.push(v) + ) + + expect(values).toEqual(['VALUE FromEnv']) + + expect(output.getAllMessages()).toEqual([]) // no messages so we don't need to disambiguate types + }) + + test('pickup from deprecated env', () => { + pickupConfigurationValueExtended( + output, + { OLD_ENV_VAR_NAME: 'VALUE FromDeprecatedEnv' }, + '', + 'OLD_ENV_VAR_NAME', + 'ENV_VAR_NAME', + v => values.push(v) + ) + + expect(values).toEqual(['VALUE FromDeprecatedEnv']) + + expect(output.infos).toEqual([]) // no messages so we don't need to disambiguate types + expect(output.warns).toEqual(['Detected Deprecated OLD_ENV_VAR_NAME environment variable. Prefer ENV_VAR_NAME']) + }) + + test('input wins over env', () => { + pickupConfigurationValueExtended( + output, + { ENV_VAR_NAME: 'VALUE FromEnv' }, + 'VALUE FromInput', + 'OLD_ENV_VAR_NAME', + 'ENV_VAR_NAME', + v => values.push(v) + ) + + expect(values).toEqual(['VALUE FromInput']) + + expect(output.infos).toEqual([]) // no messages so we don't need to disambiguate types + expect(output.warns).toEqual([]) + }) + + test('env wins over deprecated env', () => { + pickupConfigurationValueExtended( + output, + { ENV_VAR_NAME: 'VALUE FromEnv', OLD_ENV_VAR_NAME: 'VALUE FromDeprecatedEnv' }, + '', + 'OLD_ENV_VAR_NAME', + 'ENV_VAR_NAME', + v => values.push(v) + ) + + expect(values).toEqual(['VALUE FromEnv']) + + expect(output.infos).toEqual([]) // no messages so we don't need to disambiguate types + expect(output.warns).toEqual(['Detected Deprecated OLD_ENV_VAR_NAME environment variable. Prefer ENV_VAR_NAME']) + }) + + test('input wins over deprecated env and still warns', () => { + pickupConfigurationValueExtended( + output, + { ENV_VAR_NAME: 'VALUE FromEnv', OLD_ENV_VAR_NAME: 'VALUE FromDeprecatedEnv' }, + 'VALUE FromInput', + 'OLD_ENV_VAR_NAME', + 'ENV_VAR_NAME', + v => values.push(v) + ) + + expect(values).toEqual(['VALUE FromInput']) + + expect(output.infos).toEqual([]) // no messages so we don't need to disambiguate types + expect(output.warns).toEqual(['Detected Deprecated OLD_ENV_VAR_NAME environment variable. Prefer ENV_VAR_NAME']) + }) +}) + +describe('pickupConfigurationValue', () => { + let values: any[] + beforeEach(() => { + values = [] + }) + + test('pickup from input', () => { + pickupConfigurationValue({}, 'VALUE FromInput', '', v => values.push(v)) + + expect(values).toEqual(['VALUE FromInput']) + }) + + test('pickup from env', () => { + pickupConfigurationValue({ ENV_VAR_NAME: 'VALUE FromEnv' }, '', 'ENV_VAR_NAME', v => values.push(v)) + + expect(values).toEqual(['VALUE FromEnv']) + }) + + test('input wins over env', () => { + pickupConfigurationValue({ ENV_VAR_NAME: 'VALUE FromEnv' }, 'VALUE FromInput', 'ENV_VAR_NAME', v => values.push(v)) + + expect(values).toEqual(['VALUE FromInput']) + }) +}) diff --git a/__tests__/unit/ctopus-cli-commandline-generation.test.ts b/__tests__/unit/ctopus-cli-commandline-generation.test.ts deleted file mode 100644 index a8b6f46c..00000000 --- a/__tests__/unit/ctopus-cli-commandline-generation.test.ts +++ /dev/null @@ -1,312 +0,0 @@ -import {makeInputParameters} from '../../src/input-parameters' -import {OctopusCliWrapper} from '../../src/octopus-cli-wrapper' - -test('no parameters', () => { - const w = new OctopusCliWrapper( - makeInputParameters(), - {}, - console.info, - console.warn - ) - - const launchInfo = w.generateLaunchConfig() - expect(launchInfo.args).toEqual(['run-runbook']) -}) - -test('all the parameters', () => { - const i = makeInputParameters({ - project: 'projectZ', - apiKey: 'API FOOBAR', - proxy: 'some-proxy', - proxyPassword: 'some-proxy-pass', - proxyUsername: 'some-proxy-user', - server: 'http://octopusServer', - space: 'Space-61', - environments: 'hello,world', - runbook: 'some-runbook', - variables: ['testing', 'variables'] - }) - - const w = new OctopusCliWrapper(i, {}, console.info, console.warn) - - const launchInfo = w.generateLaunchConfig() - expect(launchInfo.env).toEqual({ - OCTOPUS_CLI_API_KEY: 'API FOOBAR', - OCTOPUS_CLI_SERVER: 'http://octopusServer' - }) - - expect(launchInfo.args).toEqual([ - 'run-runbook', - '--proxy=some-proxy', - '--proxyUser=some-proxy-user', - '--proxyPass=some-proxy-pass', - '--space=Space-61', - '--project=projectZ', - '--runbook=some-runbook', - '--environment=hello', - '--environment=world', - '--variable=testing', - '--variable=variables' - ]) -}) - -// this is an indirect test of pickupConfigurationValueExtended -describe('pickup api key', () => { - let infoMessages: string[] - let warnMessages: string[] - beforeEach(() => { - infoMessages = [] - warnMessages = [] - }) - - test('api key from input', () => { - const w = new OctopusCliWrapper( - makeInputParameters({apiKey: 'API FromInput'}), - {}, - m => infoMessages.push(m), - m => warnMessages.push(m) - ) - - const launchInfo = w.generateLaunchConfig() - expect(launchInfo.args).toEqual(['run-runbook']) - expect(launchInfo.env).toEqual({ - OCTOPUS_CLI_API_KEY: 'API FromInput' - }) - - expect(infoMessages).toEqual([]) - expect(warnMessages).toEqual([]) - }) - - test('api key from new env var', () => { - const w = new OctopusCliWrapper( - makeInputParameters(), - {OCTOPUS_API_KEY: 'API FromEnv'}, - m => infoMessages.push(m), - m => warnMessages.push(m) - ) - - const launchInfo = w.generateLaunchConfig() - expect(launchInfo.args).toEqual(['run-runbook']) - expect(launchInfo.env).toEqual({ - OCTOPUS_CLI_API_KEY: 'API FromEnv' - }) - - expect(infoMessages).toEqual([]) - expect(warnMessages).toEqual([]) - }) - - test('api key from deprecated env var', () => { - const w = new OctopusCliWrapper( - makeInputParameters(), - {OCTOPUS_CLI_API_KEY: 'API FromDeprecatedEnv'}, - m => infoMessages.push(m), - m => warnMessages.push(m) - ) - - const launchInfo = w.generateLaunchConfig() - expect(launchInfo.args).toEqual(['run-runbook']) - expect(launchInfo.env).toEqual({ - OCTOPUS_CLI_API_KEY: 'API FromDeprecatedEnv' - }) - - expect(infoMessages).toEqual([]) - expect(warnMessages).toEqual([ - 'Detected Deprecated OCTOPUS_CLI_API_KEY environment variable. Prefer OCTOPUS_API_KEY' - ]) - }) - - test('input wins over env', () => { - const w = new OctopusCliWrapper( - makeInputParameters({apiKey: 'API FromInput'}), - {OCTOPUS_API_KEY: 'API FromEnv'}, - m => infoMessages.push(m), - m => warnMessages.push(m) - ) - - const launchInfo = w.generateLaunchConfig() - expect(launchInfo.args).toEqual(['run-runbook']) - expect(launchInfo.env).toEqual({ - OCTOPUS_CLI_API_KEY: 'API FromInput' - }) - - expect(infoMessages).toEqual([]) - expect(warnMessages).toEqual([]) - }) - - test('env wins over deprecated env', () => { - const w = new OctopusCliWrapper( - makeInputParameters(), - { - OCTOPUS_API_KEY: 'API FromEnv', - OCTOPUS_CLI_API_KEY: 'API FromDeprecatedEnv' - }, - m => infoMessages.push(m), - m => warnMessages.push(m) - ) - - const launchInfo = w.generateLaunchConfig() - expect(launchInfo.args).toEqual(['run-runbook']) - expect(launchInfo.env).toEqual({ - OCTOPUS_CLI_API_KEY: 'API FromEnv' - }) - - expect(infoMessages).toEqual([]) - expect(warnMessages).toEqual([ - // still logs the warning even though we aren't using OCTOPUS_CLI_API_KEY - 'Detected Deprecated OCTOPUS_CLI_API_KEY environment variable. Prefer OCTOPUS_API_KEY' - ]) - }) - - test('input wins over both env and deprecated env', () => { - const w = new OctopusCliWrapper( - makeInputParameters({apiKey: 'API FromInput'}), - { - OCTOPUS_API_KEY: 'API FromEnv', - OCTOPUS_CLI_API_KEY: 'API FromDeprecatedEnv' - }, - m => infoMessages.push(m), - m => warnMessages.push(m) - ) - - const launchInfo = w.generateLaunchConfig() - expect(launchInfo.args).toEqual(['run-runbook']) - expect(launchInfo.env).toEqual({ - OCTOPUS_CLI_API_KEY: 'API FromInput' - }) - - expect(infoMessages).toEqual([]) - expect(warnMessages).toEqual([ - // still logs the warning even though we aren't using OCTOPUS_CLI_API_KEY - 'Detected Deprecated OCTOPUS_CLI_API_KEY environment variable. Prefer OCTOPUS_API_KEY' - ]) - }) -}) - -// because this shares logic with api key, we don't need all the exhaustive unit test cases for it -test('pickup host', () => { - const infoMessages: string[] = [] - const warnMessages: string[] = [] - - const w = new OctopusCliWrapper( - makeInputParameters({server: 'server-FromInput'}), - { - OCTOPUS_CLI_SERVER: 'server-FromDeprecatedEnv', - OCTOPUS_HOST: 'server-FromEnv' - }, - m => infoMessages.push(m), - m => warnMessages.push(m) - ) - - const launchInfo = w.generateLaunchConfig() - expect(launchInfo.args).toEqual(['run-runbook']) - expect(launchInfo.env).toEqual({ - OCTOPUS_CLI_SERVER: 'server-FromInput' - }) - - expect(infoMessages).toEqual([]) - expect(warnMessages).toEqual([ - 'Detected Deprecated OCTOPUS_CLI_SERVER environment variable. Prefer OCTOPUS_HOST' - ]) -}) - -describe('pickup proxy settings', () => { - // we can test all 3 proxy settings in one go because there's not much danger of this missing a bug here - const messages: string[] = [] - - test('pickup from input', () => { - const w = new OctopusCliWrapper( - makeInputParameters({ - proxy: 'proxy-FromInput', - proxyUsername: 'proxyUser-FromInput', - proxyPassword: 'proxyPass-FromInput' - }), - {}, - m => messages.push(m), - m => messages.push(m) - ) - - const launchInfo = w.generateLaunchConfig() - expect(launchInfo.args).toEqual([ - 'run-runbook', - '--proxy=proxy-FromInput', - '--proxyUser=proxyUser-FromInput', - '--proxyPass=proxyPass-FromInput' - ]) - expect(launchInfo.env).toEqual({}) - - expect(messages).toEqual([]) // no messages so we don't need to disambiguate types - }) - - test('pickup from env', () => { - const w = new OctopusCliWrapper( - makeInputParameters(), - { - OCTOPUS_PROXY: 'proxy-FromEnv', - OCTOPUS_PROXY_USERNAME: 'proxyUser-FromEnv', - OCTOPUS_PROXY_PASSWORD: 'proxyPass-FromEnv' - }, - m => messages.push(m), - m => messages.push(m) - ) - - const launchInfo = w.generateLaunchConfig() - expect(launchInfo.args).toEqual([ - 'run-runbook', - '--proxy=proxy-FromEnv', - '--proxyUser=proxyUser-FromEnv', - '--proxyPass=proxyPass-FromEnv' - ]) - expect(launchInfo.env).toEqual({}) - - expect(messages).toEqual([]) // no messages so we don't need to disambiguate types - }) - - test('input wins over env', () => { - const w = new OctopusCliWrapper( - makeInputParameters({ - proxy: 'proxy-FromInput', - proxyUsername: 'proxyUser-FromInput', - proxyPassword: 'proxyPass-FromInput' - }), - { - OCTOPUS_PROXY: 'proxy-FromEnv', - OCTOPUS_PROXY_USERNAME: 'proxyUser-FromEnv', - OCTOPUS_PROXY_PASSWORD: 'proxyPass-FromEnv' - }, - m => messages.push(m), - m => messages.push(m) - ) - - const launchInfo = w.generateLaunchConfig() - expect(launchInfo.args).toEqual([ - 'run-runbook', - '--proxy=proxy-FromInput', - '--proxyUser=proxyUser-FromInput', - '--proxyPass=proxyPass-FromInput' - ]) - expect(launchInfo.env).toEqual({}) - - expect(messages).toEqual([]) // no messages so we don't need to disambiguate types - }) -}) - -// because this shares logic with proxy, we don't need all the exhaustive unit test cases for it -test('pickup space', () => { - const messages: string[] = [] - - const w = new OctopusCliWrapper( - makeInputParameters({space: 'space-FromInput'}), - {OCTOPUS_SPACE: 'space-FromEnv'}, - m => messages.push(m), - m => messages.push(m) - ) - - const launchInfo = w.generateLaunchConfig() - expect(launchInfo.args).toEqual(['run-runbook', '--space=space-FromInput']) - expect(launchInfo.env).toEqual({}) - - expect(messages).toEqual([]) -}) - -// other options such as releaseNotes are simple string values with no environment variable behaviour. -// they are already covered by 'test all the parameters' so no need for specific tests for them diff --git a/__tests__/unit/octopus-cli-commandline-generation.test.ts b/__tests__/unit/octopus-cli-commandline-generation.test.ts new file mode 100644 index 00000000..c8474c9d --- /dev/null +++ b/__tests__/unit/octopus-cli-commandline-generation.test.ts @@ -0,0 +1,80 @@ +import { makeInputParameters } from '../../src/input-parameters' +import { generateLaunchConfig } from '../../src/octopus-cli-wrapper' + +test('no parameters', () => { + const launchInfo = generateLaunchConfig({ parameters: makeInputParameters(), env: {} }, console) + expect(launchInfo.args).toEqual(['run-runbook']) +}) + +test('all the parameters', () => { + const i = makeInputParameters({ + project: 'projectZ', + apiKey: 'API FOOBAR', + proxy: 'some-proxy', + proxyPassword: 'some-proxy-pass', + proxyUsername: 'some-proxy-user', + server: 'http://octopusServer', + space: 'Space-61', + environments: 'hello,world', + runbook: 'some-runbook', + variables: ['testing', 'variables'] + }) + + const launchInfo = generateLaunchConfig({ parameters: i, env: {} }, console) + expect(launchInfo.env).toEqual({ + OCTOPUS_CLI_API_KEY: 'API FOOBAR', + OCTOPUS_CLI_SERVER: 'http://octopusServer' + }) + + expect(launchInfo.args).toEqual([ + 'run-runbook', + '--proxy=some-proxy', + '--proxyUser=some-proxy-user', + '--proxyPass=some-proxy-pass', + '--space=Space-61', + '--project=projectZ', + '--runbook=some-runbook', + '--environment=hello', + '--environment=world', + '--variable=testing', + '--variable=variables' + ]) +}) + +test('all the parameters where env has the values', () => { + const i = makeInputParameters({ + project: 'projectZ', + environments: 'hello,world', + runbook: 'some-runbook', + variables: ['testing', 'variables'] + }) + + const env = { + OCTOPUS_API_KEY: 'API FOOBAR', + OCTOPUS_HOST: 'http://octopusServer', + OCTOPUS_SPACE: 'Space-61', + OCTOPUS_PROXY: 'some-proxy', + OCTOPUS_PROXY_USERNAME: 'some-proxy-user', + OCTOPUS_PROXY_PASSWORD: 'some-proxy-pass' + } + + const launchInfo = generateLaunchConfig({ parameters: i, env: env }, console) + expect(launchInfo.env).toEqual({ + OCTOPUS_CLI_API_KEY: 'API FOOBAR', + OCTOPUS_CLI_SERVER: 'http://octopusServer' + }) + + expect(launchInfo.args).toEqual([ + 'run-runbook', + '--proxy=some-proxy', + '--proxyUser=some-proxy-user', + '--proxyPass=some-proxy-pass', + '--space=Space-61', + '--project=projectZ', + '--runbook=some-runbook', + '--environment=hello', + '--environment=world', + '--variable=testing', + '--variable=variables' + ]) +}) diff --git a/__tests__/unit/octopus-cli-output-parsing.test.ts b/__tests__/unit/octopus-cli-output-parsing.test.ts index da47a828..3f97dec8 100644 --- a/__tests__/unit/octopus-cli-output-parsing.test.ts +++ b/__tests__/unit/octopus-cli-output-parsing.test.ts @@ -1,23 +1,16 @@ -import {makeInputParameters} from '../../src/input-parameters' -import {OctopusCliWrapper} from '../../src/octopus-cli-wrapper' +import { OctopusCliOutputHandler } from '../../src/octopus-cli-wrapper' +import { CaptureOutput } from '../test-helpers' -let infoMessages: string[] -let warnMessages: string[] -let w: OctopusCliWrapper +var output: CaptureOutput +var w: OctopusCliOutputHandler beforeEach(() => { - infoMessages = [] - warnMessages = [] - w = new OctopusCliWrapper( - makeInputParameters(), - {}, - msg => infoMessages.push(msg), - msg => warnMessages.push(msg) - ) + output = new CaptureOutput() + w = new OctopusCliOutputHandler(output) }) afterEach(() => { - expect(warnMessages).toEqual([]) // none of our tests here should generate warnings + expect(output.warns).toEqual([]) // none of our tests here should generate warnings }) test('standard commandline processing', () => { @@ -25,12 +18,32 @@ test('standard commandline processing', () => { w.stdline('Handshaking with Octopus Server') w.stdline('Authenticated as: magic user that should not be revealed') w.stdline('Done!') - expect(infoMessages).toEqual([ + expect(output.infos).toEqual([ '🐙 Using Octopus Deploy CLI 123...', '🤝 Handshaking with Octopus Deploy', '✅ Authenticated', '🎉 Runbook complete!' ]) + expect(output.warns).toEqual([]) +}) + +test('standard error processing also removes blank lines', () => { + w.errline('') + w.errline('FAILED') + w.errline('') + + expect(output.infos).toEqual([]) + expect(output.warns).toEqual(['FAILED']) + output.warns = [] // so the afterEach doesn't trip +}) + +test('other lines just get passed through', () => { + w.stdline('Creating release...!') // note trailing ! means the earlier thing doesn't match + w.stdline('foo') + w.stdline('bar') + w.stdline('baz') + + expect(output.infos).toEqual(['Creating release...!', 'foo', 'bar', 'baz']) }) test('filters blank lines', () => { @@ -38,5 +51,5 @@ test('filters blank lines', () => { w.stdline('foo') w.stdline('') - expect(infoMessages).toEqual(['foo']) + expect(output.infos).toEqual(['foo']) }) diff --git a/dist/index.js b/dist/index.js index e6dd6f53..29f6e7cc 100644 --- a/dist/index.js +++ b/dist/index.js @@ -3207,6 +3207,55 @@ if (process.env.NODE_DEBUG && /\btunnel\b/.test(process.env.NODE_DEBUG)) { exports.debug = debug; // for test +/***/ }), + +/***/ 996: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +// Things in this file are shared across more than one of our github actions; set up +// for easy copy-paste transfer across repos +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.pickupConfigurationValue = exports.pickupConfigurationValueExtended = void 0; +// Picks up a config value from GHA Input or environment, supports mapping +// of an obsolete env var to a newer one (e.g. OCTOPUS_CLI_SERVER vs OCTOPUS_HOST) +function pickupConfigurationValueExtended(output, env, valueFromInputParameters, inputObsoleteEnvKey, inputNewEnvKey, valueHandler) { + // we always want to log the warning for a deprecated environment variable, even if the parameter comes in via inputParameter + let result; + const deprecatedValue = env[inputObsoleteEnvKey]; + if (deprecatedValue && deprecatedValue.length > 0) { + output.warn(`Detected Deprecated ${inputObsoleteEnvKey} environment variable. Prefer ${inputNewEnvKey}`); + result = deprecatedValue; + } + const value = env[inputNewEnvKey]; + // deliberately not 'else if' because if both OCTOPUS_CLI_API_KEY and OCTOPUS_API_KEY are set we want the latter to win + if (value && value.length > 0) { + result = value; + } + if (valueFromInputParameters.length > 0) { + result = valueFromInputParameters; + } + if (result) { + valueHandler(result); + } +} +exports.pickupConfigurationValueExtended = pickupConfigurationValueExtended; +// Picks up a config value from GHA Input or environment +function pickupConfigurationValue(env, valueFromInputParameters, inputNewEnvKey, valueHandler) { + if (valueFromInputParameters.length > 0) { + valueHandler(valueFromInputParameters); + } + else { + const value = env[inputNewEnvKey]; + if (value && value.length > 0) { + valueHandler(value); + } + } +} +exports.pickupConfigurationValue = pickupConfigurationValue; + + /***/ }), /***/ 519: @@ -3273,11 +3322,13 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); const input_parameters_1 = __nccwpck_require__(519); const core_1 = __nccwpck_require__(186); const octopus_cli_wrapper_1 = __nccwpck_require__(856); +// GitHub actions entrypoint function run() { return __awaiter(this, void 0, void 0, function* () { try { - const wrapper = new octopus_cli_wrapper_1.OctopusCliWrapper((0, input_parameters_1.getInputParameters)(), process.env, msg => (0, core_1.info)(msg), msg => (0, core_1.warning)(msg)); - yield wrapper.runRunbook(); + const inputs = { parameters: (0, input_parameters_1.getInputParameters)(), env: process.env }; + const outputs = { info: s => (0, core_1.info)(s), warn: s => (0, core_1.warning)(s) }; + yield (0, octopus_cli_wrapper_1.runRunbook)(inputs, outputs, 'octo'); } catch (e) { if (e instanceof Error) { @@ -3306,133 +3357,111 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }); }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.OctopusCliWrapper = void 0; +exports.runRunbook = exports.OctopusCliOutputHandler = exports.generateLaunchConfig = void 0; const exec_1 = __nccwpck_require__(514); -class OctopusCliWrapper { - constructor(parameters, env, logInfo, logWarn) { - this.inputParameters = parameters; - this.env = env; - this.logInfo = logInfo; - this.logWarn = logWarn; - } - // When the Octopus CLI writes to stdout, we capture the text via this function +const cli_util_1 = __nccwpck_require__(996); +// Converts incoming environment and inputParameters into a set of commandline args + env vars to run the Octopus CLI +function generateLaunchConfig(inputs, output) { + const launchArgs = ['run-runbook']; + const launchEnv = {}; + const parameters = inputs.parameters; + (0, cli_util_1.pickupConfigurationValueExtended)(output, inputs.env, parameters.apiKey, 'OCTOPUS_CLI_API_KEY', 'OCTOPUS_API_KEY', value => (launchEnv['OCTOPUS_CLI_API_KEY'] = value)); + (0, cli_util_1.pickupConfigurationValueExtended)(output, inputs.env, parameters.server, 'OCTOPUS_CLI_SERVER', 'OCTOPUS_HOST', value => (launchEnv['OCTOPUS_CLI_SERVER'] = value)); + (0, cli_util_1.pickupConfigurationValue)(inputs.env, parameters.proxy, 'OCTOPUS_PROXY', value => launchArgs.push(`--proxy=${value}`)); + (0, cli_util_1.pickupConfigurationValue)(inputs.env, parameters.proxyUsername, 'OCTOPUS_PROXY_USERNAME', value => launchArgs.push(`--proxyUser=${value}`)); + (0, cli_util_1.pickupConfigurationValue)(inputs.env, parameters.proxyPassword, 'OCTOPUS_PROXY_PASSWORD', value => launchArgs.push(`--proxyPass=${value}`)); + (0, cli_util_1.pickupConfigurationValue)(inputs.env, parameters.space, 'OCTOPUS_SPACE', value => launchArgs.push(`--space=${value}`)); + if (parameters.project.length > 0) + launchArgs.push(`--project=${parameters.project}`); + if (parameters.runbook.length > 0) + launchArgs.push(`--runbook=${parameters.runbook}`); + if (parameters.environments.length > 0) { + for (const iterator of parameters.environments.split(',')) { + if (iterator.length > 0) { + launchArgs.push(`--environment=${iterator}`); + } + } + } + for (const variable of parameters.variables) { + variable.split(',').map(v => launchArgs.push(`--variable=${v}`)); + } + return { args: launchArgs, env: launchEnv }; +} +exports.generateLaunchConfig = generateLaunchConfig; +// consumes stdline and errline from the child process +// and transforms/buffers output as needed +class OctopusCliOutputHandler { + constructor(output) { + this.output = output; + } + // public: attach this to the process errline + errline(line) { + if (line.length === 0) { + return; + } + this.output.warn(line); + } + // public: attach this to the process stdline stdline(line) { if (line.length === 0) return; if (line.includes('Octopus Deploy Command Line Tool')) { const version = line.split('version ')[1]; - this.logInfo(`🐙 Using Octopus Deploy CLI ${version}...`); + this.output.info(`🐙 Using Octopus Deploy CLI ${version}...`); return; } if (line.includes('Handshaking with Octopus Server')) { - this.logInfo(`🤝 Handshaking with Octopus Deploy`); + this.output.info(`🤝 Handshaking with Octopus Deploy`); return; } if (line.includes('Authenticated as:')) { - this.logInfo(`✅ Authenticated`); + this.output.info(`✅ Authenticated`); return; } if (line === 'Done!') { - this.logInfo(`🎉 Runbook complete!`); + this.output.info(`🎉 Runbook complete!`); return; } - this.logInfo(line); - } - // Picks up a config value from GHA Input or environment, supports mapping - // of an obsolete env var to a newer one (e.g. OCTOPUS_CLI_SERVER vs OCTOPUS_HOST) - pickupConfigurationValueExtended(inputParameter, inputObsoleteEnvKey, inputNewEnvKey, valueHandler) { - // we always want to log the warning for a deprecated environment variable, even if the parameter comes in via inputParameter - let result; - const deprecatedValue = this.env[inputObsoleteEnvKey]; - if (deprecatedValue && deprecatedValue.length > 0) { - this.logWarn(`Detected Deprecated ${inputObsoleteEnvKey} environment variable. Prefer ${inputNewEnvKey}`); - result = deprecatedValue; - } - const value = this.env[inputNewEnvKey]; - // deliberately not 'else if' because if both OCTOPUS_CLI_API_KEY and OCTOPUS_API_KEY are set we want the latter to win - if (value && value.length > 0) { - result = value; - } - if (inputParameter.length > 0) { - result = inputParameter; - } - if (result) { - valueHandler(result); - } + this.output.info(line); } - // Picks up a config value from GHA Input or environment - pickupConfigurationValue(inputParameter, inputNewEnvKey, valueHandler) { - if (inputParameter.length > 0) { - valueHandler(inputParameter); +} +exports.OctopusCliOutputHandler = OctopusCliOutputHandler; +// This invokes the CLI to do the work. +// Returns the release number assigned by the octopus server +// This shells out to 'octo' and expects to be running in GHA, so you can't unit test it; integration tests only. +function runRunbook(inputs, output, octoExecutable) { + return __awaiter(this, void 0, void 0, function* () { + const outputHandler = new OctopusCliOutputHandler(output); + const cliLaunchConfiguration = generateLaunchConfig(inputs, output); + // the launch config will only have the specific few env vars that the script wants to set. + // Need to merge with the rest of the environment variables, otherwise we will pass a + // stripped environment through to the CLI and it won't have meaningful things like HOME and PATH + const envCopy = Object.assign({}, process.env); + Object.assign(envCopy, cliLaunchConfiguration.env); + const options = { + listeners: { + stdline: input => outputHandler.stdline(input), + errline: input => outputHandler.errline(input) + }, + env: envCopy, + silent: true + }; + try { + yield (0, exec_1.exec)(octoExecutable, cliLaunchConfiguration.args, options); } - else { - const value = this.env[inputNewEnvKey]; - if (value && value.length > 0) { - valueHandler(value); - } - } - } - // Converts incoming environment and inputParameters into a set of commandline args + env vars to run the Octopus CLI - generateLaunchConfig() { - // Note: this is specialised to only work for run-runbook, but feels like it wants to be more generic and reusable? - // Given we have multiple github actions and each lives in its own repo, what's our strategy for sharing here? - const launchArgs = ['run-runbook']; - const launchEnv = {}; - const parameters = this.inputParameters; - this.pickupConfigurationValueExtended(parameters.apiKey, 'OCTOPUS_CLI_API_KEY', 'OCTOPUS_API_KEY', value => (launchEnv['OCTOPUS_CLI_API_KEY'] = value)); - this.pickupConfigurationValueExtended(parameters.server, 'OCTOPUS_CLI_SERVER', 'OCTOPUS_HOST', value => (launchEnv['OCTOPUS_CLI_SERVER'] = value)); - this.pickupConfigurationValue(parameters.proxy, 'OCTOPUS_PROXY', value => launchArgs.push(`--proxy=${value}`)); - this.pickupConfigurationValue(parameters.proxyUsername, 'OCTOPUS_PROXY_USERNAME', value => launchArgs.push(`--proxyUser=${value}`)); - this.pickupConfigurationValue(parameters.proxyPassword, 'OCTOPUS_PROXY_PASSWORD', value => launchArgs.push(`--proxyPass=${value}`)); - this.pickupConfigurationValue(parameters.space, 'OCTOPUS_SPACE', value => launchArgs.push(`--space=${value}`)); - if (parameters.project.length > 0) - launchArgs.push(`--project=${parameters.project}`); - if (parameters.runbook.length > 0) - launchArgs.push(`--runbook=${parameters.runbook}`); - if (parameters.environments.length > 0) { - for (const iterator of parameters.environments.split(',')) { - if (iterator.length > 0) { - launchArgs.push(`--environment=${iterator}`); + catch (e) { + if (e instanceof Error) { + // catch some particular messages and rethrow more convenient ones + if (e.message.includes('Unable to locate executable file')) { + throw new Error(`Octopus CLI executable missing. Ensure you have added the 'OctopusDeploy/install-octopus-cli-action@v1' step to your GitHub actions workflow.\nError: ${e.message}`); } } + // rethrow, so our Promise is rejected. The GHA shim in index.ts will catch this and call setFailed + throw e; } - for (const variable of parameters.variables) { - variable.split(',').map(v => launchArgs.push(`--variable=${v}`)); - } - return { args: launchArgs, env: launchEnv }; - } - // This invokes the CLI to do the work. - // Returns the release number assigned by the octopus server - // This shells out to 'octo' and expects to be running in GHA, so you can't unit test it; integration tests only. - runRunbook(octoExecutable = 'octo') { - return __awaiter(this, void 0, void 0, function* () { - this.logInfo('🔣 Parsing inputs...'); - const cliLaunchConfiguration = this.generateLaunchConfig(); - const options = { - listeners: { - stdline: input => this.stdline(input) - }, - env: cliLaunchConfiguration.env, - silent: true - }; - try { - yield (0, exec_1.exec)(octoExecutable, cliLaunchConfiguration.args, options); - } - catch (e) { - if (e instanceof Error) { - if (e.message.includes('Unable to locate executable file')) { - throw new Error('Octopus CLI executable missing. Please ensure you have added the `OctopusDeploy/install-octopus-cli-action@v1` step to your GitHub actions script before this.'); - } - if (e.message.includes('failed with exit code')) { - throw new Error('Octopus CLI returned an error code. Please check your GitHub actions log for more detail'); - } - } - throw e; - } - }); - } + }); } -exports.OctopusCliWrapper = OctopusCliWrapper; +exports.runRunbook = runRunbook; /***/ }), diff --git a/package-lock.json b/package-lock.json index fea32b52..339cb3d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,19 +13,19 @@ "@actions/http-client": "^2.0.1", "@actions/tool-cache": "^2.0.1", "@octopusdeploy/api-client": "^1.3.1", - "@octopusdeploy/message-contracts": "^1.3.0" + "@octopusdeploy/message-contracts": "^1.3.1" }, "devDependencies": { - "@types/jest": "^28.1.4", + "@types/jest": "^28.1.6", "@types/node": "^18.0.6", "@types/tmp": "^0.2.3", "@typescript-eslint/eslint-plugin": "^5.30.6", "@typescript-eslint/parser": "^5.30.6", "@vercel/ncc": "^0.34.0", - "eslint": "^8.19.0", + "eslint": "^8.20.0", "eslint-plugin-github": "^4.3.6", "eslint-plugin-jest": "^26.6.0", - "jest": "^28.1.2", + "jest": "^28.1.3", "jest-circus": "^28.1.3", "jest-junit": "^14.0.0", "js-yaml": "^4.1.0", @@ -744,37 +744,37 @@ } }, "node_modules/@jest/core": { - "version": "28.1.2", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-28.1.2.tgz", - "integrity": "sha512-Xo4E+Sb/nZODMGOPt2G3cMmCBqL4/W2Ijwr7/mrXlq4jdJwcFQ/9KrrJZT2adQRk2otVBXXOz1GRQ4Z5iOgvRQ==", + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-28.1.3.tgz", + "integrity": "sha512-CIKBrlaKOzA7YG19BEqCw3SLIsEwjZkeJzf5bdooVnW4bH5cktqe3JX+G2YV1aK5vP8N9na1IGWFzYaTp6k6NA==", "dev": true, "dependencies": { - "@jest/console": "^28.1.1", - "@jest/reporters": "^28.1.2", - "@jest/test-result": "^28.1.1", - "@jest/transform": "^28.1.2", - "@jest/types": "^28.1.1", + "@jest/console": "^28.1.3", + "@jest/reporters": "^28.1.3", + "@jest/test-result": "^28.1.3", + "@jest/transform": "^28.1.3", + "@jest/types": "^28.1.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "ci-info": "^3.2.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", - "jest-changed-files": "^28.0.2", - "jest-config": "^28.1.2", - "jest-haste-map": "^28.1.1", - "jest-message-util": "^28.1.1", + "jest-changed-files": "^28.1.3", + "jest-config": "^28.1.3", + "jest-haste-map": "^28.1.3", + "jest-message-util": "^28.1.3", "jest-regex-util": "^28.0.2", - "jest-resolve": "^28.1.1", - "jest-resolve-dependencies": "^28.1.2", - "jest-runner": "^28.1.2", - "jest-runtime": "^28.1.2", - "jest-snapshot": "^28.1.2", - "jest-util": "^28.1.1", - "jest-validate": "^28.1.1", - "jest-watcher": "^28.1.1", + "jest-resolve": "^28.1.3", + "jest-resolve-dependencies": "^28.1.3", + "jest-runner": "^28.1.3", + "jest-runtime": "^28.1.3", + "jest-snapshot": "^28.1.3", + "jest-util": "^28.1.3", + "jest-validate": "^28.1.3", + "jest-watcher": "^28.1.3", "micromatch": "^4.0.4", - "pretty-format": "^28.1.1", + "pretty-format": "^28.1.3", "rimraf": "^3.0.0", "slash": "^3.0.0", "strip-ansi": "^6.0.0" @@ -863,16 +863,16 @@ } }, "node_modules/@jest/reporters": { - "version": "28.1.2", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-28.1.2.tgz", - "integrity": "sha512-/whGLhiwAqeCTmQEouSigUZJPVl7sW8V26EiboImL+UyXznnr1a03/YZ2BX8OlFw0n+Zlwu+EZAITZtaeRTxyA==", + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-28.1.3.tgz", + "integrity": "sha512-JuAy7wkxQZVNU/V6g9xKzCGC5LVXx9FDcABKsSXp5MiKPEE2144a/vXTEDoyzjUpZKfVwp08Wqg5A4WfTMAzjg==", "dev": true, "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^28.1.1", - "@jest/test-result": "^28.1.1", - "@jest/transform": "^28.1.2", - "@jest/types": "^28.1.1", + "@jest/console": "^28.1.3", + "@jest/test-result": "^28.1.3", + "@jest/transform": "^28.1.3", + "@jest/types": "^28.1.3", "@jridgewell/trace-mapping": "^0.3.13", "@types/node": "*", "chalk": "^4.0.0", @@ -885,9 +885,9 @@ "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "^28.1.1", - "jest-util": "^28.1.1", - "jest-worker": "^28.1.1", + "jest-message-util": "^28.1.3", + "jest-util": "^28.1.3", + "jest-worker": "^28.1.3", "slash": "^3.0.0", "string-length": "^4.0.1", "strip-ansi": "^6.0.0", @@ -948,14 +948,14 @@ } }, "node_modules/@jest/test-sequencer": { - "version": "28.1.1", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-28.1.1.tgz", - "integrity": "sha512-nuL+dNSVMcWB7OOtgb0EGH5AjO4UBCt68SLP08rwmC+iRhyuJWS9MtZ/MpipxFwKAlHFftbMsydXqWre8B0+XA==", + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-28.1.3.tgz", + "integrity": "sha512-NIMPEqqa59MWnDi1kvXXpYbqsfQmSJsIbnd85mdVGkiDfQ9WQQTXOLsvISUfonmnBT+w85WEgneCigEEdHDFxw==", "dev": true, "dependencies": { - "@jest/test-result": "^28.1.1", + "@jest/test-result": "^28.1.3", "graceful-fs": "^4.2.9", - "jest-haste-map": "^28.1.1", + "jest-haste-map": "^28.1.3", "slash": "^3.0.0" }, "engines": { @@ -1154,9 +1154,9 @@ } }, "node_modules/@octopusdeploy/message-contracts": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@octopusdeploy/message-contracts/-/message-contracts-1.3.0.tgz", - "integrity": "sha512-B6Ws2a0oqECn8av/4Ifmajo6toj7zsvaZWPHh1jxhmnoORM+OZ4XxWOPP2kuf7oETyCZPVuo3iGAVzYLBVk2Xg==" + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@octopusdeploy/message-contracts/-/message-contracts-1.3.1.tgz", + "integrity": "sha512-QtciE5YWWsZ3XhjbZy1Cv4lZsAEL8o4COco6W3H27lzSTHsofnIJPWBwZIoRilQqPxIkI8s7/zbIPjJlTcQAyg==" }, "node_modules/@sinclair/typebox": { "version": "0.24.19", @@ -1257,9 +1257,9 @@ } }, "node_modules/@types/jest": { - "version": "28.1.4", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-28.1.4.tgz", - "integrity": "sha512-telv6G5N7zRJiLcI3Rs3o+ipZ28EnE+7EvF0pSrt2pZOMnAVI/f+6/LucDxOvcBcTeTL3JMF744BbVQAVBUQRA==", + "version": "28.1.6", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-28.1.6.tgz", + "integrity": "sha512-0RbGAFMfcBJKOmqRazM8L98uokwuwD5F8rHrv/ZMbrZBwVOWZUyPG6VFNscjYr/vjM3Vu4fRrCPbOs42AfemaQ==", "dev": true, "dependencies": { "jest-matcher-utils": "^28.0.0", @@ -1716,15 +1716,15 @@ } }, "node_modules/babel-jest": { - "version": "28.1.2", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-28.1.2.tgz", - "integrity": "sha512-pfmoo6sh4L/+5/G2OOfQrGJgvH7fTa1oChnuYH2G/6gA+JwDvO8PELwvwnofKBMNrQsam0Wy/Rw+QSrBNewq2Q==", + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-28.1.3.tgz", + "integrity": "sha512-epUaPOEWMk3cWX0M/sPvCHHCe9fMFAa/9hXEgKP8nFfNl/jlGkE9ucq9NqkZGXLDduCJYS0UvSlPUwC0S+rH6Q==", "dev": true, "dependencies": { - "@jest/transform": "^28.1.2", + "@jest/transform": "^28.1.3", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^28.1.1", + "babel-preset-jest": "^28.1.3", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" @@ -1753,9 +1753,9 @@ } }, "node_modules/babel-plugin-jest-hoist": { - "version": "28.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-28.1.1.tgz", - "integrity": "sha512-NovGCy5Hn25uMJSAU8FaHqzs13cFoOI4lhIujiepssjCKRsAo3TA734RDWSGxuFTsUJXerYOqQQodlxgmtqbzw==", + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-28.1.3.tgz", + "integrity": "sha512-Ys3tUKAmfnkRUpPdpa98eYrAR0nV+sSFUZZEGuQ2EbFd1y4SOLtD5QDNHAq+bb9a+bbXvYQC4b+ID/THIMcU6Q==", "dev": true, "dependencies": { "@babel/template": "^7.3.3", @@ -1791,12 +1791,12 @@ } }, "node_modules/babel-preset-jest": { - "version": "28.1.1", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-28.1.1.tgz", - "integrity": "sha512-FCq9Oud0ReTeWtcneYf/48981aTfXYuB9gbU4rBNNJVBSQ6ssv7E6v/qvbBxtOWwZFXjLZwpg+W3q7J6vhH25g==", + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-28.1.3.tgz", + "integrity": "sha512-L+fupJvlWAHbQfn74coNX3zf60LXMJsezNvvx8eIh7iOR1luJ1poxYgQk1F8PYtNq/6QODDHCqsSnTFSWC491A==", "dev": true, "dependencies": { - "babel-plugin-jest-hoist": "^28.1.1", + "babel-plugin-jest-hoist": "^28.1.3", "babel-preset-current-node-syntax": "^1.0.0" }, "engines": { @@ -2261,9 +2261,9 @@ } }, "node_modules/eslint": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.19.0.tgz", - "integrity": "sha512-SXOPj3x9VKvPe81TjjUJCYlV4oJjQw68Uek+AM0X4p+33dj2HY5bpTZOgnQHcG2eAm1mtCU9uNMnJi7exU/kYw==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.20.0.tgz", + "integrity": "sha512-d4ixhz5SKCa1D6SCPrivP7yYVi7nyD6A4vs6HIAul9ujBzcEmZVM3/0NN/yu5nKhmO1wjp5xQ46iRfmDGlOviA==", "dev": true, "dependencies": { "@eslint/eslintrc": "^1.3.0", @@ -3637,9 +3637,9 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.4.tgz", - "integrity": "sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", "dev": true, "dependencies": { "html-escaper": "^2.0.0", @@ -3650,15 +3650,15 @@ } }, "node_modules/jest": { - "version": "28.1.2", - "resolved": "https://registry.npmjs.org/jest/-/jest-28.1.2.tgz", - "integrity": "sha512-Tuf05DwLeCh2cfWCQbcz9UxldoDyiR1E9Igaei5khjonKncYdc6LDfynKCEWozK0oLE3GD+xKAo2u8x/0s6GOg==", + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest/-/jest-28.1.3.tgz", + "integrity": "sha512-N4GT5on8UkZgH0O5LUavMRV1EDEhNTL0KEfRmDIeZHSV7p2XgLoY9t9VDUgL6o+yfdgYHVxuz81G8oB9VG5uyA==", "dev": true, "dependencies": { - "@jest/core": "^28.1.2", - "@jest/types": "^28.1.1", + "@jest/core": "^28.1.3", + "@jest/types": "^28.1.3", "import-local": "^3.0.2", - "jest-cli": "^28.1.2" + "jest-cli": "^28.1.3" }, "bin": { "jest": "bin/jest.js" @@ -3676,18 +3676,33 @@ } }, "node_modules/jest-changed-files": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-28.0.2.tgz", - "integrity": "sha512-QX9u+5I2s54ZnGoMEjiM2WeBvJR2J7w/8ZUmH2um/WLAuGAYFQcsVXY9+1YL6k0H/AGUdH8pXUAv6erDqEsvIA==", + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-28.1.3.tgz", + "integrity": "sha512-esaOfUWJXk2nfZt9SPyC8gA1kNfdKLkQWyzsMlqq8msYSlNKfmZxfRgZn4Cd4MGVUF+7v6dBs0d5TOAKa7iIiA==", "dev": true, "dependencies": { "execa": "^5.0.0", - "throat": "^6.0.1" + "p-limit": "^3.1.0" }, "engines": { "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, + "node_modules/jest-changed-files/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/jest-circus": { "version": "28.1.3", "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-28.1.3.tgz", @@ -3734,21 +3749,21 @@ } }, "node_modules/jest-cli": { - "version": "28.1.2", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-28.1.2.tgz", - "integrity": "sha512-l6eoi5Do/IJUXAFL9qRmDiFpBeEJAnjJb1dcd9i/VWfVWbp3mJhuH50dNtX67Ali4Ecvt4eBkWb4hXhPHkAZTw==", + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-28.1.3.tgz", + "integrity": "sha512-roY3kvrv57Azn1yPgdTebPAXvdR2xfezaKKYzVxZ6It/5NCxzJym6tUI5P1zkdWhfUYkxEI9uZWcQdaFLo8mJQ==", "dev": true, "dependencies": { - "@jest/core": "^28.1.2", - "@jest/test-result": "^28.1.1", - "@jest/types": "^28.1.1", + "@jest/core": "^28.1.3", + "@jest/test-result": "^28.1.3", + "@jest/types": "^28.1.3", "chalk": "^4.0.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", "import-local": "^3.0.2", - "jest-config": "^28.1.2", - "jest-util": "^28.1.1", - "jest-validate": "^28.1.1", + "jest-config": "^28.1.3", + "jest-util": "^28.1.3", + "jest-validate": "^28.1.3", "prompts": "^2.0.1", "yargs": "^17.3.1" }, @@ -3768,31 +3783,31 @@ } }, "node_modules/jest-config": { - "version": "28.1.2", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-28.1.2.tgz", - "integrity": "sha512-g6EfeRqddVbjPVBVY4JWpUY4IvQoFRIZcv4V36QkqzE0IGhEC/VkugFeBMAeUE7PRgC8KJF0yvJNDeQRbamEVA==", + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-28.1.3.tgz", + "integrity": "sha512-MG3INjByJ0J4AsNBm7T3hsuxKQqFIiRo/AUqb1q9LRKI5UU6Aar9JHbr9Ivn1TVwfUD9KirRoM/T6u8XlcQPHQ==", "dev": true, "dependencies": { "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^28.1.1", - "@jest/types": "^28.1.1", - "babel-jest": "^28.1.2", + "@jest/test-sequencer": "^28.1.3", + "@jest/types": "^28.1.3", + "babel-jest": "^28.1.3", "chalk": "^4.0.0", "ci-info": "^3.2.0", "deepmerge": "^4.2.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-circus": "^28.1.2", - "jest-environment-node": "^28.1.2", + "jest-circus": "^28.1.3", + "jest-environment-node": "^28.1.3", "jest-get-type": "^28.0.2", "jest-regex-util": "^28.0.2", - "jest-resolve": "^28.1.1", - "jest-runner": "^28.1.2", - "jest-util": "^28.1.1", - "jest-validate": "^28.1.1", + "jest-resolve": "^28.1.3", + "jest-runner": "^28.1.3", + "jest-util": "^28.1.3", + "jest-validate": "^28.1.3", "micromatch": "^4.0.4", "parse-json": "^5.2.0", - "pretty-format": "^28.1.1", + "pretty-format": "^28.1.3", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, @@ -3856,17 +3871,17 @@ } }, "node_modules/jest-environment-node": { - "version": "28.1.2", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-28.1.2.tgz", - "integrity": "sha512-oYsZz9Qw27XKmOgTtnl0jW7VplJkN2oeof+SwAwKFQacq3CLlG9u4kTGuuLWfvu3J7bVutWlrbEQMOCL/jughw==", + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-28.1.3.tgz", + "integrity": "sha512-ugP6XOhEpjAEhGYvp5Xj989ns5cB1K6ZdjBYuS30umT4CQEETaxSiPcZ/E1kFktX4GkrcM4qu07IIlDYX1gp+A==", "dev": true, "dependencies": { - "@jest/environment": "^28.1.2", - "@jest/fake-timers": "^28.1.2", - "@jest/types": "^28.1.1", + "@jest/environment": "^28.1.3", + "@jest/fake-timers": "^28.1.3", + "@jest/types": "^28.1.3", "@types/node": "*", - "jest-mock": "^28.1.1", - "jest-util": "^28.1.1" + "jest-mock": "^28.1.3", + "jest-util": "^28.1.3" }, "engines": { "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" @@ -3931,13 +3946,13 @@ } }, "node_modules/jest-leak-detector": { - "version": "28.1.1", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-28.1.1.tgz", - "integrity": "sha512-4jvs8V8kLbAaotE+wFR7vfUGf603cwYtFf1/PYEsyX2BAjSzj8hQSVTP6OWzseTl0xL6dyHuKs2JAks7Pfubmw==", + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-28.1.3.tgz", + "integrity": "sha512-WFVJhnQsiKtDEo5lG2mM0v40QWnBM+zMdHHyJs8AWZ7J0QZJS59MsyKeJHWhpBZBH32S48FOVvGyOFT1h0DlqA==", "dev": true, "dependencies": { "jest-get-type": "^28.0.2", - "pretty-format": "^28.1.1" + "pretty-format": "^28.1.3" }, "engines": { "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" @@ -4038,50 +4053,65 @@ } }, "node_modules/jest-resolve-dependencies": { - "version": "28.1.2", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-28.1.2.tgz", - "integrity": "sha512-OXw4vbOZuyRTBi3tapWBqdyodU+T33ww5cPZORuTWkg+Y8lmsxQlVu3MWtJh6NMlKRTHQetF96yGPv01Ye7Mbg==", + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-28.1.3.tgz", + "integrity": "sha512-qa0QO2Q0XzQoNPouMbCc7Bvtsem8eQgVPNkwn9LnS+R2n8DaVDPL/U1gngC0LTl1RYXJU0uJa2BMC2DbTfFrHA==", "dev": true, "dependencies": { "jest-regex-util": "^28.0.2", - "jest-snapshot": "^28.1.2" + "jest-snapshot": "^28.1.3" }, "engines": { "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, "node_modules/jest-runner": { - "version": "28.1.2", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-28.1.2.tgz", - "integrity": "sha512-6/k3DlAsAEr5VcptCMdhtRhOoYClZQmxnVMZvZ/quvPGRpN7OBQYPIC32tWSgOnbgqLXNs5RAniC+nkdFZpD4A==", + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-28.1.3.tgz", + "integrity": "sha512-GkMw4D/0USd62OVO0oEgjn23TM+YJa2U2Wu5zz9xsQB1MxWKDOlrnykPxnMsN0tnJllfLPinHTka61u0QhaxBA==", "dev": true, "dependencies": { - "@jest/console": "^28.1.1", - "@jest/environment": "^28.1.2", - "@jest/test-result": "^28.1.1", - "@jest/transform": "^28.1.2", - "@jest/types": "^28.1.1", + "@jest/console": "^28.1.3", + "@jest/environment": "^28.1.3", + "@jest/test-result": "^28.1.3", + "@jest/transform": "^28.1.3", + "@jest/types": "^28.1.3", "@types/node": "*", "chalk": "^4.0.0", "emittery": "^0.10.2", "graceful-fs": "^4.2.9", "jest-docblock": "^28.1.1", - "jest-environment-node": "^28.1.2", - "jest-haste-map": "^28.1.1", - "jest-leak-detector": "^28.1.1", - "jest-message-util": "^28.1.1", - "jest-resolve": "^28.1.1", - "jest-runtime": "^28.1.2", - "jest-util": "^28.1.1", - "jest-watcher": "^28.1.1", - "jest-worker": "^28.1.1", - "source-map-support": "0.5.13", - "throat": "^6.0.1" + "jest-environment-node": "^28.1.3", + "jest-haste-map": "^28.1.3", + "jest-leak-detector": "^28.1.3", + "jest-message-util": "^28.1.3", + "jest-resolve": "^28.1.3", + "jest-runtime": "^28.1.3", + "jest-util": "^28.1.3", + "jest-watcher": "^28.1.3", + "jest-worker": "^28.1.3", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" }, "engines": { "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, + "node_modules/jest-runner/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/jest-runtime": { "version": "28.1.3", "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-28.1.3.tgz", @@ -4211,18 +4241,18 @@ } }, "node_modules/jest-watcher": { - "version": "28.1.1", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-28.1.1.tgz", - "integrity": "sha512-RQIpeZ8EIJMxbQrXpJQYIIlubBnB9imEHsxxE41f54ZwcqWLysL/A0ZcdMirf+XsMn3xfphVQVV4EW0/p7i7Ug==", + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-28.1.3.tgz", + "integrity": "sha512-t4qcqj9hze+jviFPUN3YAtAEeFnr/azITXQEMARf5cMwKY2SMBRnCQTXLixTl20OR6mLh9KLMrgVJgJISym+1g==", "dev": true, "dependencies": { - "@jest/test-result": "^28.1.1", - "@jest/types": "^28.1.1", + "@jest/test-result": "^28.1.3", + "@jest/types": "^28.1.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "emittery": "^0.10.2", - "jest-util": "^28.1.1", + "jest-util": "^28.1.3", "string-length": "^4.0.1" }, "engines": { @@ -5342,12 +5372,6 @@ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, - "node_modules/throat": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz", - "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==", - "dev": true - }, "node_modules/tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", @@ -6312,37 +6336,37 @@ } }, "@jest/core": { - "version": "28.1.2", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-28.1.2.tgz", - "integrity": "sha512-Xo4E+Sb/nZODMGOPt2G3cMmCBqL4/W2Ijwr7/mrXlq4jdJwcFQ/9KrrJZT2adQRk2otVBXXOz1GRQ4Z5iOgvRQ==", + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-28.1.3.tgz", + "integrity": "sha512-CIKBrlaKOzA7YG19BEqCw3SLIsEwjZkeJzf5bdooVnW4bH5cktqe3JX+G2YV1aK5vP8N9na1IGWFzYaTp6k6NA==", "dev": true, "requires": { - "@jest/console": "^28.1.1", - "@jest/reporters": "^28.1.2", - "@jest/test-result": "^28.1.1", - "@jest/transform": "^28.1.2", - "@jest/types": "^28.1.1", + "@jest/console": "^28.1.3", + "@jest/reporters": "^28.1.3", + "@jest/test-result": "^28.1.3", + "@jest/transform": "^28.1.3", + "@jest/types": "^28.1.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "ci-info": "^3.2.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", - "jest-changed-files": "^28.0.2", - "jest-config": "^28.1.2", - "jest-haste-map": "^28.1.1", - "jest-message-util": "^28.1.1", + "jest-changed-files": "^28.1.3", + "jest-config": "^28.1.3", + "jest-haste-map": "^28.1.3", + "jest-message-util": "^28.1.3", "jest-regex-util": "^28.0.2", - "jest-resolve": "^28.1.1", - "jest-resolve-dependencies": "^28.1.2", - "jest-runner": "^28.1.2", - "jest-runtime": "^28.1.2", - "jest-snapshot": "^28.1.2", - "jest-util": "^28.1.1", - "jest-validate": "^28.1.1", - "jest-watcher": "^28.1.1", + "jest-resolve": "^28.1.3", + "jest-resolve-dependencies": "^28.1.3", + "jest-runner": "^28.1.3", + "jest-runtime": "^28.1.3", + "jest-snapshot": "^28.1.3", + "jest-util": "^28.1.3", + "jest-validate": "^28.1.3", + "jest-watcher": "^28.1.3", "micromatch": "^4.0.4", - "pretty-format": "^28.1.1", + "pretty-format": "^28.1.3", "rimraf": "^3.0.0", "slash": "^3.0.0", "strip-ansi": "^6.0.0" @@ -6405,16 +6429,16 @@ } }, "@jest/reporters": { - "version": "28.1.2", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-28.1.2.tgz", - "integrity": "sha512-/whGLhiwAqeCTmQEouSigUZJPVl7sW8V26EiboImL+UyXznnr1a03/YZ2BX8OlFw0n+Zlwu+EZAITZtaeRTxyA==", + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-28.1.3.tgz", + "integrity": "sha512-JuAy7wkxQZVNU/V6g9xKzCGC5LVXx9FDcABKsSXp5MiKPEE2144a/vXTEDoyzjUpZKfVwp08Wqg5A4WfTMAzjg==", "dev": true, "requires": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^28.1.1", - "@jest/test-result": "^28.1.1", - "@jest/transform": "^28.1.2", - "@jest/types": "^28.1.1", + "@jest/console": "^28.1.3", + "@jest/test-result": "^28.1.3", + "@jest/transform": "^28.1.3", + "@jest/types": "^28.1.3", "@jridgewell/trace-mapping": "^0.3.13", "@types/node": "*", "chalk": "^4.0.0", @@ -6427,9 +6451,9 @@ "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "^28.1.1", - "jest-util": "^28.1.1", - "jest-worker": "^28.1.1", + "jest-message-util": "^28.1.3", + "jest-util": "^28.1.3", + "jest-worker": "^28.1.3", "slash": "^3.0.0", "string-length": "^4.0.1", "strip-ansi": "^6.0.0", @@ -6470,14 +6494,14 @@ } }, "@jest/test-sequencer": { - "version": "28.1.1", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-28.1.1.tgz", - "integrity": "sha512-nuL+dNSVMcWB7OOtgb0EGH5AjO4UBCt68SLP08rwmC+iRhyuJWS9MtZ/MpipxFwKAlHFftbMsydXqWre8B0+XA==", + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-28.1.3.tgz", + "integrity": "sha512-NIMPEqqa59MWnDi1kvXXpYbqsfQmSJsIbnd85mdVGkiDfQ9WQQTXOLsvISUfonmnBT+w85WEgneCigEEdHDFxw==", "dev": true, "requires": { - "@jest/test-result": "^28.1.1", + "@jest/test-result": "^28.1.3", "graceful-fs": "^4.2.9", - "jest-haste-map": "^28.1.1", + "jest-haste-map": "^28.1.3", "slash": "^3.0.0" } }, @@ -6636,9 +6660,9 @@ } }, "@octopusdeploy/message-contracts": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@octopusdeploy/message-contracts/-/message-contracts-1.3.0.tgz", - "integrity": "sha512-B6Ws2a0oqECn8av/4Ifmajo6toj7zsvaZWPHh1jxhmnoORM+OZ4XxWOPP2kuf7oETyCZPVuo3iGAVzYLBVk2Xg==" + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@octopusdeploy/message-contracts/-/message-contracts-1.3.1.tgz", + "integrity": "sha512-QtciE5YWWsZ3XhjbZy1Cv4lZsAEL8o4COco6W3H27lzSTHsofnIJPWBwZIoRilQqPxIkI8s7/zbIPjJlTcQAyg==" }, "@sinclair/typebox": { "version": "0.24.19", @@ -6739,9 +6763,9 @@ } }, "@types/jest": { - "version": "28.1.4", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-28.1.4.tgz", - "integrity": "sha512-telv6G5N7zRJiLcI3Rs3o+ipZ28EnE+7EvF0pSrt2pZOMnAVI/f+6/LucDxOvcBcTeTL3JMF744BbVQAVBUQRA==", + "version": "28.1.6", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-28.1.6.tgz", + "integrity": "sha512-0RbGAFMfcBJKOmqRazM8L98uokwuwD5F8rHrv/ZMbrZBwVOWZUyPG6VFNscjYr/vjM3Vu4fRrCPbOs42AfemaQ==", "dev": true, "requires": { "jest-matcher-utils": "^28.0.0", @@ -7048,15 +7072,15 @@ } }, "babel-jest": { - "version": "28.1.2", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-28.1.2.tgz", - "integrity": "sha512-pfmoo6sh4L/+5/G2OOfQrGJgvH7fTa1oChnuYH2G/6gA+JwDvO8PELwvwnofKBMNrQsam0Wy/Rw+QSrBNewq2Q==", + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-28.1.3.tgz", + "integrity": "sha512-epUaPOEWMk3cWX0M/sPvCHHCe9fMFAa/9hXEgKP8nFfNl/jlGkE9ucq9NqkZGXLDduCJYS0UvSlPUwC0S+rH6Q==", "dev": true, "requires": { - "@jest/transform": "^28.1.2", + "@jest/transform": "^28.1.3", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^28.1.1", + "babel-preset-jest": "^28.1.3", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" @@ -7076,9 +7100,9 @@ } }, "babel-plugin-jest-hoist": { - "version": "28.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-28.1.1.tgz", - "integrity": "sha512-NovGCy5Hn25uMJSAU8FaHqzs13cFoOI4lhIujiepssjCKRsAo3TA734RDWSGxuFTsUJXerYOqQQodlxgmtqbzw==", + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-28.1.3.tgz", + "integrity": "sha512-Ys3tUKAmfnkRUpPdpa98eYrAR0nV+sSFUZZEGuQ2EbFd1y4SOLtD5QDNHAq+bb9a+bbXvYQC4b+ID/THIMcU6Q==", "dev": true, "requires": { "@babel/template": "^7.3.3", @@ -7108,12 +7132,12 @@ } }, "babel-preset-jest": { - "version": "28.1.1", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-28.1.1.tgz", - "integrity": "sha512-FCq9Oud0ReTeWtcneYf/48981aTfXYuB9gbU4rBNNJVBSQ6ssv7E6v/qvbBxtOWwZFXjLZwpg+W3q7J6vhH25g==", + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-28.1.3.tgz", + "integrity": "sha512-L+fupJvlWAHbQfn74coNX3zf60LXMJsezNvvx8eIh7iOR1luJ1poxYgQk1F8PYtNq/6QODDHCqsSnTFSWC491A==", "dev": true, "requires": { - "babel-plugin-jest-hoist": "^28.1.1", + "babel-plugin-jest-hoist": "^28.1.3", "babel-preset-current-node-syntax": "^1.0.0" } }, @@ -7456,9 +7480,9 @@ "dev": true }, "eslint": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.19.0.tgz", - "integrity": "sha512-SXOPj3x9VKvPe81TjjUJCYlV4oJjQw68Uek+AM0X4p+33dj2HY5bpTZOgnQHcG2eAm1mtCU9uNMnJi7exU/kYw==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.20.0.tgz", + "integrity": "sha512-d4ixhz5SKCa1D6SCPrivP7yYVi7nyD6A4vs6HIAul9ujBzcEmZVM3/0NN/yu5nKhmO1wjp5xQ46iRfmDGlOviA==", "dev": true, "requires": { "@eslint/eslintrc": "^1.3.0", @@ -8442,9 +8466,9 @@ } }, "istanbul-reports": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.4.tgz", - "integrity": "sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", "dev": true, "requires": { "html-escaper": "^2.0.0", @@ -8452,25 +8476,36 @@ } }, "jest": { - "version": "28.1.2", - "resolved": "https://registry.npmjs.org/jest/-/jest-28.1.2.tgz", - "integrity": "sha512-Tuf05DwLeCh2cfWCQbcz9UxldoDyiR1E9Igaei5khjonKncYdc6LDfynKCEWozK0oLE3GD+xKAo2u8x/0s6GOg==", + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest/-/jest-28.1.3.tgz", + "integrity": "sha512-N4GT5on8UkZgH0O5LUavMRV1EDEhNTL0KEfRmDIeZHSV7p2XgLoY9t9VDUgL6o+yfdgYHVxuz81G8oB9VG5uyA==", "dev": true, "requires": { - "@jest/core": "^28.1.2", - "@jest/types": "^28.1.1", + "@jest/core": "^28.1.3", + "@jest/types": "^28.1.3", "import-local": "^3.0.2", - "jest-cli": "^28.1.2" + "jest-cli": "^28.1.3" } }, "jest-changed-files": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-28.0.2.tgz", - "integrity": "sha512-QX9u+5I2s54ZnGoMEjiM2WeBvJR2J7w/8ZUmH2um/WLAuGAYFQcsVXY9+1YL6k0H/AGUdH8pXUAv6erDqEsvIA==", + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-28.1.3.tgz", + "integrity": "sha512-esaOfUWJXk2nfZt9SPyC8gA1kNfdKLkQWyzsMlqq8msYSlNKfmZxfRgZn4Cd4MGVUF+7v6dBs0d5TOAKa7iIiA==", "dev": true, "requires": { "execa": "^5.0.0", - "throat": "^6.0.1" + "p-limit": "^3.1.0" + }, + "dependencies": { + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + } } }, "jest-circus": { @@ -8512,51 +8547,51 @@ } }, "jest-cli": { - "version": "28.1.2", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-28.1.2.tgz", - "integrity": "sha512-l6eoi5Do/IJUXAFL9qRmDiFpBeEJAnjJb1dcd9i/VWfVWbp3mJhuH50dNtX67Ali4Ecvt4eBkWb4hXhPHkAZTw==", + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-28.1.3.tgz", + "integrity": "sha512-roY3kvrv57Azn1yPgdTebPAXvdR2xfezaKKYzVxZ6It/5NCxzJym6tUI5P1zkdWhfUYkxEI9uZWcQdaFLo8mJQ==", "dev": true, "requires": { - "@jest/core": "^28.1.2", - "@jest/test-result": "^28.1.1", - "@jest/types": "^28.1.1", + "@jest/core": "^28.1.3", + "@jest/test-result": "^28.1.3", + "@jest/types": "^28.1.3", "chalk": "^4.0.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", "import-local": "^3.0.2", - "jest-config": "^28.1.2", - "jest-util": "^28.1.1", - "jest-validate": "^28.1.1", + "jest-config": "^28.1.3", + "jest-util": "^28.1.3", + "jest-validate": "^28.1.3", "prompts": "^2.0.1", "yargs": "^17.3.1" } }, "jest-config": { - "version": "28.1.2", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-28.1.2.tgz", - "integrity": "sha512-g6EfeRqddVbjPVBVY4JWpUY4IvQoFRIZcv4V36QkqzE0IGhEC/VkugFeBMAeUE7PRgC8KJF0yvJNDeQRbamEVA==", + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-28.1.3.tgz", + "integrity": "sha512-MG3INjByJ0J4AsNBm7T3hsuxKQqFIiRo/AUqb1q9LRKI5UU6Aar9JHbr9Ivn1TVwfUD9KirRoM/T6u8XlcQPHQ==", "dev": true, "requires": { "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^28.1.1", - "@jest/types": "^28.1.1", - "babel-jest": "^28.1.2", + "@jest/test-sequencer": "^28.1.3", + "@jest/types": "^28.1.3", + "babel-jest": "^28.1.3", "chalk": "^4.0.0", "ci-info": "^3.2.0", "deepmerge": "^4.2.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-circus": "^28.1.2", - "jest-environment-node": "^28.1.2", + "jest-circus": "^28.1.3", + "jest-environment-node": "^28.1.3", "jest-get-type": "^28.0.2", "jest-regex-util": "^28.0.2", - "jest-resolve": "^28.1.1", - "jest-runner": "^28.1.2", - "jest-util": "^28.1.1", - "jest-validate": "^28.1.1", + "jest-resolve": "^28.1.3", + "jest-runner": "^28.1.3", + "jest-util": "^28.1.3", + "jest-validate": "^28.1.3", "micromatch": "^4.0.4", "parse-json": "^5.2.0", - "pretty-format": "^28.1.1", + "pretty-format": "^28.1.3", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" } @@ -8596,17 +8631,17 @@ } }, "jest-environment-node": { - "version": "28.1.2", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-28.1.2.tgz", - "integrity": "sha512-oYsZz9Qw27XKmOgTtnl0jW7VplJkN2oeof+SwAwKFQacq3CLlG9u4kTGuuLWfvu3J7bVutWlrbEQMOCL/jughw==", + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-28.1.3.tgz", + "integrity": "sha512-ugP6XOhEpjAEhGYvp5Xj989ns5cB1K6ZdjBYuS30umT4CQEETaxSiPcZ/E1kFktX4GkrcM4qu07IIlDYX1gp+A==", "dev": true, "requires": { - "@jest/environment": "^28.1.2", - "@jest/fake-timers": "^28.1.2", - "@jest/types": "^28.1.1", + "@jest/environment": "^28.1.3", + "@jest/fake-timers": "^28.1.3", + "@jest/types": "^28.1.3", "@types/node": "*", - "jest-mock": "^28.1.1", - "jest-util": "^28.1.1" + "jest-mock": "^28.1.3", + "jest-util": "^28.1.3" } }, "jest-get-type": { @@ -8656,13 +8691,13 @@ } }, "jest-leak-detector": { - "version": "28.1.1", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-28.1.1.tgz", - "integrity": "sha512-4jvs8V8kLbAaotE+wFR7vfUGf603cwYtFf1/PYEsyX2BAjSzj8hQSVTP6OWzseTl0xL6dyHuKs2JAks7Pfubmw==", + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-28.1.3.tgz", + "integrity": "sha512-WFVJhnQsiKtDEo5lG2mM0v40QWnBM+zMdHHyJs8AWZ7J0QZJS59MsyKeJHWhpBZBH32S48FOVvGyOFT1h0DlqA==", "dev": true, "requires": { "jest-get-type": "^28.0.2", - "pretty-format": "^28.1.1" + "pretty-format": "^28.1.3" } }, "jest-matcher-utils": { @@ -8735,42 +8770,53 @@ } }, "jest-resolve-dependencies": { - "version": "28.1.2", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-28.1.2.tgz", - "integrity": "sha512-OXw4vbOZuyRTBi3tapWBqdyodU+T33ww5cPZORuTWkg+Y8lmsxQlVu3MWtJh6NMlKRTHQetF96yGPv01Ye7Mbg==", + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-28.1.3.tgz", + "integrity": "sha512-qa0QO2Q0XzQoNPouMbCc7Bvtsem8eQgVPNkwn9LnS+R2n8DaVDPL/U1gngC0LTl1RYXJU0uJa2BMC2DbTfFrHA==", "dev": true, "requires": { "jest-regex-util": "^28.0.2", - "jest-snapshot": "^28.1.2" + "jest-snapshot": "^28.1.3" } }, "jest-runner": { - "version": "28.1.2", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-28.1.2.tgz", - "integrity": "sha512-6/k3DlAsAEr5VcptCMdhtRhOoYClZQmxnVMZvZ/quvPGRpN7OBQYPIC32tWSgOnbgqLXNs5RAniC+nkdFZpD4A==", + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-28.1.3.tgz", + "integrity": "sha512-GkMw4D/0USd62OVO0oEgjn23TM+YJa2U2Wu5zz9xsQB1MxWKDOlrnykPxnMsN0tnJllfLPinHTka61u0QhaxBA==", "dev": true, "requires": { - "@jest/console": "^28.1.1", - "@jest/environment": "^28.1.2", - "@jest/test-result": "^28.1.1", - "@jest/transform": "^28.1.2", - "@jest/types": "^28.1.1", + "@jest/console": "^28.1.3", + "@jest/environment": "^28.1.3", + "@jest/test-result": "^28.1.3", + "@jest/transform": "^28.1.3", + "@jest/types": "^28.1.3", "@types/node": "*", "chalk": "^4.0.0", "emittery": "^0.10.2", "graceful-fs": "^4.2.9", "jest-docblock": "^28.1.1", - "jest-environment-node": "^28.1.2", - "jest-haste-map": "^28.1.1", - "jest-leak-detector": "^28.1.1", - "jest-message-util": "^28.1.1", - "jest-resolve": "^28.1.1", - "jest-runtime": "^28.1.2", - "jest-util": "^28.1.1", - "jest-watcher": "^28.1.1", - "jest-worker": "^28.1.1", - "source-map-support": "0.5.13", - "throat": "^6.0.1" + "jest-environment-node": "^28.1.3", + "jest-haste-map": "^28.1.3", + "jest-leak-detector": "^28.1.3", + "jest-message-util": "^28.1.3", + "jest-resolve": "^28.1.3", + "jest-runtime": "^28.1.3", + "jest-util": "^28.1.3", + "jest-watcher": "^28.1.3", + "jest-worker": "^28.1.3", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "dependencies": { + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + } } }, "jest-runtime": { @@ -8882,18 +8928,18 @@ } }, "jest-watcher": { - "version": "28.1.1", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-28.1.1.tgz", - "integrity": "sha512-RQIpeZ8EIJMxbQrXpJQYIIlubBnB9imEHsxxE41f54ZwcqWLysL/A0ZcdMirf+XsMn3xfphVQVV4EW0/p7i7Ug==", + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-28.1.3.tgz", + "integrity": "sha512-t4qcqj9hze+jviFPUN3YAtAEeFnr/azITXQEMARf5cMwKY2SMBRnCQTXLixTl20OR6mLh9KLMrgVJgJISym+1g==", "dev": true, "requires": { - "@jest/test-result": "^28.1.1", - "@jest/types": "^28.1.1", + "@jest/test-result": "^28.1.3", + "@jest/types": "^28.1.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "emittery": "^0.10.2", - "jest-util": "^28.1.1", + "jest-util": "^28.1.3", "string-length": "^4.0.1" } }, @@ -9706,12 +9752,6 @@ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, - "throat": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz", - "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==", - "dev": true - }, "tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", diff --git a/package.json b/package.json index 3457f7c9..33af8cdb 100644 --- a/package.json +++ b/package.json @@ -8,20 +8,20 @@ "@actions/http-client": "^2.0.1", "@actions/tool-cache": "^2.0.1", "@octopusdeploy/api-client": "^1.3.1", - "@octopusdeploy/message-contracts": "^1.3.0" + "@octopusdeploy/message-contracts": "^1.3.1" }, "description": "GitHub Action to Run a Runbook in Octopus Deploy", "devDependencies": { - "@types/jest": "^28.1.4", + "@types/jest": "^28.1.6", "@types/node": "^18.0.6", "@types/tmp": "^0.2.3", "@typescript-eslint/eslint-plugin": "^5.30.6", "@typescript-eslint/parser": "^5.30.6", "@vercel/ncc": "^0.34.0", - "eslint": "^8.19.0", + "eslint": "^8.20.0", "eslint-plugin-github": "^4.3.6", "eslint-plugin-jest": "^26.6.0", - "jest": "^28.1.2", + "jest": "^28.1.3", "jest-circus": "^28.1.3", "jest-junit": "^14.0.0", "js-yaml": "^4.1.0", diff --git a/src/cli-util.ts b/src/cli-util.ts new file mode 100644 index 00000000..a0625d9a --- /dev/null +++ b/src/cli-util.ts @@ -0,0 +1,65 @@ +// Things in this file are shared across more than one of our github actions; set up +// for easy copy-paste transfer across repos + +export interface CliOutput { + info: (message: string) => void + warn: (message: string) => void +} + +// When launching the Octopus CLI, we use a combination of environment variables and command line +// arguments. This interface carries them +export interface CliLaunchConfiguration { + args: string[] + env: { [key: string]: string } +} + +// environment variables can either be a NodeJS.ProcessEnv or a plain old object with string keys/values for testing +export type EnvVars = { [key: string]: string } | NodeJS.ProcessEnv + +// Picks up a config value from GHA Input or environment, supports mapping +// of an obsolete env var to a newer one (e.g. OCTOPUS_CLI_SERVER vs OCTOPUS_HOST) +export function pickupConfigurationValueExtended( + output: CliOutput, + env: EnvVars, + valueFromInputParameters: string, + inputObsoleteEnvKey: string, + inputNewEnvKey: string, + valueHandler: (value: string) => void +): void { + // we always want to log the warning for a deprecated environment variable, even if the parameter comes in via inputParameter + let result: string | undefined + + const deprecatedValue = env[inputObsoleteEnvKey] + if (deprecatedValue && deprecatedValue.length > 0) { + output.warn(`Detected Deprecated ${inputObsoleteEnvKey} environment variable. Prefer ${inputNewEnvKey}`) + result = deprecatedValue + } + const value = env[inputNewEnvKey] + // deliberately not 'else if' because if both OCTOPUS_CLI_API_KEY and OCTOPUS_API_KEY are set we want the latter to win + if (value && value.length > 0) { + result = value + } + if (valueFromInputParameters.length > 0) { + result = valueFromInputParameters + } + if (result) { + valueHandler(result) + } +} + +// Picks up a config value from GHA Input or environment +export function pickupConfigurationValue( + env: EnvVars, + valueFromInputParameters: string, + inputNewEnvKey: string, + valueHandler: (value: string) => void +): void { + if (valueFromInputParameters.length > 0) { + valueHandler(valueFromInputParameters) + } else { + const value = env[inputNewEnvKey] + if (value && value.length > 0) { + valueHandler(value) + } + } +} diff --git a/src/input-parameters.ts b/src/input-parameters.ts index 879a580e..61fd1205 100644 --- a/src/input-parameters.ts +++ b/src/input-parameters.ts @@ -1,4 +1,4 @@ -import {getInput, getMultilineInput} from '@actions/core' +import { getInput, getMultilineInput } from '@actions/core' export interface InputParameters { project: string @@ -28,9 +28,7 @@ export function getInputParameters(): InputParameters { } } -export function makeInputParameters( - override?: Partial -): InputParameters { +export function makeInputParameters(override?: Partial): InputParameters { const template: InputParameters = { project: '', runbook: '', diff --git a/src/main.ts b/src/main.ts index 5ff12aff..0427d5cd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,16 +1,15 @@ -import {getInputParameters} from './input-parameters' -import {info, setFailed, warning} from '@actions/core' -import {OctopusCliWrapper} from './octopus-cli-wrapper' +import { getInputParameters } from './input-parameters' +import { info, setFailed, warning } from '@actions/core' +import { CliInputs, runRunbook } from './octopus-cli-wrapper' +import { CliOutput } from './cli-util' +// GitHub actions entrypoint async function run(): Promise { try { - const wrapper = new OctopusCliWrapper( - getInputParameters(), - process.env, - msg => info(msg), - msg => warning(msg) - ) - await wrapper.runRunbook() + const inputs: CliInputs = { parameters: getInputParameters(), env: process.env } + const outputs: CliOutput = { info: s => info(s), warn: s => warning(s) } + + await runRunbook(inputs, outputs, 'octo') } catch (e: unknown) { if (e instanceof Error) { setFailed(e) diff --git a/src/octopus-cli-wrapper.ts b/src/octopus-cli-wrapper.ts index 60a7ed33..fbbe75c7 100644 --- a/src/octopus-cli-wrapper.ts +++ b/src/octopus-cli-wrapper.ts @@ -1,202 +1,154 @@ -import {exec, ExecOptions} from '@actions/exec' -import {InputParameters} from './input-parameters' +import { exec, ExecOptions } from '@actions/exec' +import { + CliOutput, + CliLaunchConfiguration, + EnvVars, + pickupConfigurationValue, + pickupConfigurationValueExtended +} from './cli-util' +import { InputParameters } from './input-parameters' + +// Things in this file are specific to create-release-action and not shared with other actions + +export interface CliInputs { + parameters: InputParameters + env: EnvVars +} -// environment variables can either be a NodeJS.ProcessEnv or a plain old object with string keys/values for testing -type EnvVars = {[key: string]: string} | NodeJS.ProcessEnv +// Converts incoming environment and inputParameters into a set of commandline args + env vars to run the Octopus CLI +export function generateLaunchConfig(inputs: CliInputs, output: CliOutput): CliLaunchConfiguration { + const launchArgs: string[] = ['run-runbook'] + const launchEnv: { [key: string]: string } = {} -export class OctopusCliWrapper { - inputParameters: InputParameters - // environment variables at the time the wrapper was created - env: EnvVars - logInfo: (message: string) => void - logWarn: (message: string) => void - - constructor( - parameters: InputParameters, - env: EnvVars, - logInfo: (message: string) => void, - logWarn: (message: string) => void - ) { - this.inputParameters = parameters - this.env = env - this.logInfo = logInfo - this.logWarn = logWarn + const parameters = inputs.parameters + + pickupConfigurationValueExtended( + output, + inputs.env, + parameters.apiKey, + 'OCTOPUS_CLI_API_KEY', + 'OCTOPUS_API_KEY', + value => (launchEnv['OCTOPUS_CLI_API_KEY'] = value) + ) + + pickupConfigurationValueExtended( + output, + inputs.env, + parameters.server, + 'OCTOPUS_CLI_SERVER', + 'OCTOPUS_HOST', + value => (launchEnv['OCTOPUS_CLI_SERVER'] = value) + ) + + pickupConfigurationValue(inputs.env, parameters.proxy, 'OCTOPUS_PROXY', value => launchArgs.push(`--proxy=${value}`)) + + pickupConfigurationValue(inputs.env, parameters.proxyUsername, 'OCTOPUS_PROXY_USERNAME', value => + launchArgs.push(`--proxyUser=${value}`) + ) + pickupConfigurationValue(inputs.env, parameters.proxyPassword, 'OCTOPUS_PROXY_PASSWORD', value => + launchArgs.push(`--proxyPass=${value}`) + ) + + pickupConfigurationValue(inputs.env, parameters.space, 'OCTOPUS_SPACE', value => launchArgs.push(`--space=${value}`)) + if (parameters.project.length > 0) launchArgs.push(`--project=${parameters.project}`) + if (parameters.runbook.length > 0) launchArgs.push(`--runbook=${parameters.runbook}`) + + if (parameters.environments.length > 0) { + for (const iterator of parameters.environments.split(',')) { + if (iterator.length > 0) { + launchArgs.push(`--environment=${iterator}`) + } + } + } + + for (const variable of parameters.variables) { + variable.split(',').map(v => launchArgs.push(`--variable=${v}`)) } - // When the Octopus CLI writes to stdout, we capture the text via this function + return { args: launchArgs, env: launchEnv } +} + +// consumes stdline and errline from the child process +// and transforms/buffers output as needed +export class OctopusCliOutputHandler { + readonly output: CliOutput + + constructor(output: CliOutput) { + this.output = output + } + + // public: attach this to the process errline + errline(line: string): void { + if (line.length === 0) { + return + } + this.output.warn(line) + } + + // public: attach this to the process stdline stdline(line: string): void { if (line.length === 0) return if (line.includes('Octopus Deploy Command Line Tool')) { const version = line.split('version ')[1] - this.logInfo(`🐙 Using Octopus Deploy CLI ${version}...`) + this.output.info(`🐙 Using Octopus Deploy CLI ${version}...`) return } if (line.includes('Handshaking with Octopus Server')) { - this.logInfo(`🤝 Handshaking with Octopus Deploy`) + this.output.info(`🤝 Handshaking with Octopus Deploy`) return } if (line.includes('Authenticated as:')) { - this.logInfo(`✅ Authenticated`) + this.output.info(`✅ Authenticated`) return } if (line === 'Done!') { - this.logInfo(`🎉 Runbook complete!`) + this.output.info(`🎉 Runbook complete!`) return } - this.logInfo(line) + this.output.info(line) } +} - // Picks up a config value from GHA Input or environment, supports mapping - // of an obsolete env var to a newer one (e.g. OCTOPUS_CLI_SERVER vs OCTOPUS_HOST) - pickupConfigurationValueExtended( - inputParameter: string, - inputObsoleteEnvKey: string, - inputNewEnvKey: string, - valueHandler: (value: string) => void - ): void { - // we always want to log the warning for a deprecated environment variable, even if the parameter comes in via inputParameter - let result: string | undefined - - const deprecatedValue = this.env[inputObsoleteEnvKey] - if (deprecatedValue && deprecatedValue.length > 0) { - this.logWarn( - `Detected Deprecated ${inputObsoleteEnvKey} environment variable. Prefer ${inputNewEnvKey}` - ) - result = deprecatedValue - } - const value = this.env[inputNewEnvKey] - // deliberately not 'else if' because if both OCTOPUS_CLI_API_KEY and OCTOPUS_API_KEY are set we want the latter to win - if (value && value.length > 0) { - result = value - } - if (inputParameter.length > 0) { - result = inputParameter - } - if (result) { - valueHandler(result) - } - } - - // Picks up a config value from GHA Input or environment - pickupConfigurationValue( - inputParameter: string, - inputNewEnvKey: string, - valueHandler: (value: string) => void - ): void { - if (inputParameter.length > 0) { - valueHandler(inputParameter) - } else { - const value = this.env[inputNewEnvKey] - if (value && value.length > 0) { - valueHandler(value) - } - } - } - - // Converts incoming environment and inputParameters into a set of commandline args + env vars to run the Octopus CLI - generateLaunchConfig(): CliLaunchConfiguration { - // Note: this is specialised to only work for run-runbook, but feels like it wants to be more generic and reusable? - // Given we have multiple github actions and each lives in its own repo, what's our strategy for sharing here? - const launchArgs: string[] = ['run-runbook'] - const launchEnv: {[key: string]: string} = {} - - const parameters = this.inputParameters - - this.pickupConfigurationValueExtended( - parameters.apiKey, - 'OCTOPUS_CLI_API_KEY', - 'OCTOPUS_API_KEY', - value => (launchEnv['OCTOPUS_CLI_API_KEY'] = value) - ) - - this.pickupConfigurationValueExtended( - parameters.server, - 'OCTOPUS_CLI_SERVER', - 'OCTOPUS_HOST', - value => (launchEnv['OCTOPUS_CLI_SERVER'] = value) - ) - - this.pickupConfigurationValue(parameters.proxy, 'OCTOPUS_PROXY', value => - launchArgs.push(`--proxy=${value}`) - ) - - this.pickupConfigurationValue( - parameters.proxyUsername, - 'OCTOPUS_PROXY_USERNAME', - value => launchArgs.push(`--proxyUser=${value}`) - ) - this.pickupConfigurationValue( - parameters.proxyPassword, - 'OCTOPUS_PROXY_PASSWORD', - value => launchArgs.push(`--proxyPass=${value}`) - ) - - this.pickupConfigurationValue(parameters.space, 'OCTOPUS_SPACE', value => - launchArgs.push(`--space=${value}`) - ) - if (parameters.project.length > 0) - launchArgs.push(`--project=${parameters.project}`) - if (parameters.runbook.length > 0) - launchArgs.push(`--runbook=${parameters.runbook}`) - - if (parameters.environments.length > 0) { - for (const iterator of parameters.environments.split(',')) { - if (iterator.length > 0) { - launchArgs.push(`--environment=${iterator}`) - } - } - } - - for (const variable of parameters.variables) { - variable.split(',').map(v => launchArgs.push(`--variable=${v}`)) - } - - return {args: launchArgs, env: launchEnv} +// This invokes the CLI to do the work. +// Returns the release number assigned by the octopus server +// This shells out to 'octo' and expects to be running in GHA, so you can't unit test it; integration tests only. +export async function runRunbook(inputs: CliInputs, output: CliOutput, octoExecutable: string): Promise { + const outputHandler = new OctopusCliOutputHandler(output) + + const cliLaunchConfiguration = generateLaunchConfig(inputs, output) + + // the launch config will only have the specific few env vars that the script wants to set. + // Need to merge with the rest of the environment variables, otherwise we will pass a + // stripped environment through to the CLI and it won't have meaningful things like HOME and PATH + const envCopy = { ...(process.env as { [key: string]: string }) } + Object.assign(envCopy, cliLaunchConfiguration.env) + + const options: ExecOptions = { + listeners: { + stdline: input => outputHandler.stdline(input), + errline: input => outputHandler.errline(input) + }, + env: envCopy, + silent: true } - // This invokes the CLI to do the work. - // Returns the release number assigned by the octopus server - // This shells out to 'octo' and expects to be running in GHA, so you can't unit test it; integration tests only. - async runRunbook(octoExecutable = 'octo'): Promise { - this.logInfo('🔣 Parsing inputs...') - const cliLaunchConfiguration = this.generateLaunchConfig() - - const options: ExecOptions = { - listeners: { - stdline: input => this.stdline(input) - }, - env: cliLaunchConfiguration.env, - silent: true - } - - try { - await exec(octoExecutable, cliLaunchConfiguration.args, options) - } catch (e: unknown) { - if (e instanceof Error) { - if (e.message.includes('Unable to locate executable file')) { - throw new Error( - 'Octopus CLI executable missing. Please ensure you have added the `OctopusDeploy/install-octopus-cli-action@v1` step to your GitHub actions script before this.' - ) - } - if (e.message.includes('failed with exit code')) { - throw new Error( - 'Octopus CLI returned an error code. Please check your GitHub actions log for more detail' - ) - } + try { + await exec(octoExecutable, cliLaunchConfiguration.args, options) + } catch (e: unknown) { + if (e instanceof Error) { + // catch some particular messages and rethrow more convenient ones + if (e.message.includes('Unable to locate executable file')) { + throw new Error( + `Octopus CLI executable missing. Ensure you have added the 'OctopusDeploy/install-octopus-cli-action@v1' step to your GitHub actions workflow.\nError: ${e.message}` + ) } - throw e } + // rethrow, so our Promise is rejected. The GHA shim in index.ts will catch this and call setFailed + throw e } } - -// When launching the Octopus CLI, we use a combination of environment variables and command line -// arguments. This interface carries them -export interface CliLaunchConfiguration { - args: string[] - env: {[key: string]: string} -} diff --git a/tsconfig.json b/tsconfig.json index d7588966..7030f81b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,5 @@ "noImplicitAny": true, "esModuleInterop": true }, - "include": [ - "src" - ] + "include": ["src"] }