From 44f5f95877dc276020122609a8863fd0eccf8e7d Mon Sep 17 00:00:00 2001 From: Francesco Renzi Date: Tue, 13 Sep 2022 13:29:49 +0100 Subject: [PATCH 1/3] Add save-state and set-output file commands --- packages/core/__tests__/core.test.ts | 128 +++++++++++++++++++++++++-- packages/core/src/core.ts | 42 ++++----- packages/core/src/file-command.ts | 23 +++++ 3 files changed, 164 insertions(+), 29 deletions(-) diff --git a/packages/core/__tests__/core.test.ts b/packages/core/__tests__/core.test.ts index d00374fd35..42e62c9afd 100644 --- a/packages/core/__tests__/core.test.ts +++ b/packages/core/__tests__/core.test.ts @@ -283,7 +283,7 @@ describe('@actions/core', () => { ).toEqual([' val1 ', ' val2 ', ' ']) }) - it('setOutput produces the correct command', () => { + it('legacy setOutput produces the correct command', () => { core.setOutput('some output', 'some value') assertWriteCalls([ os.EOL, @@ -291,16 +291,74 @@ describe('@actions/core', () => { ]) }) - it('setOutput handles bools', () => { + it('legacy setOutput handles bools', () => { core.setOutput('some output', false) assertWriteCalls([os.EOL, `::set-output name=some output::false${os.EOL}`]) }) - it('setOutput handles numbers', () => { + it('legacy setOutput handles numbers', () => { core.setOutput('some output', 1.01) assertWriteCalls([os.EOL, `::set-output name=some output::1.01${os.EOL}`]) }) + it('setOutput produces the correct command and sets the output', () => { + const command = 'OUTPUT' + createFileCommandFile(command) + core.setOutput('my out', 'out val') + verifyFileCommand( + command, + `my out<<${DELIMITER}${os.EOL}out val${os.EOL}${DELIMITER}${os.EOL}` + ) + }) + + it('setOutput handles boolean inputs', () => { + const command = 'OUTPUT' + createFileCommandFile(command) + core.setOutput('my out', true) + verifyFileCommand( + command, + `my out<<${DELIMITER}${os.EOL}true${os.EOL}${DELIMITER}${os.EOL}` + ) + }) + + it('setOutput handles number inputs', () => { + const command = 'OUTPUT' + createFileCommandFile(command) + core.setOutput('my out', 5) + verifyFileCommand( + command, + `my out<<${DELIMITER}${os.EOL}5${os.EOL}${DELIMITER}${os.EOL}` + ) + }) + + it('setOutput does not allow delimiter as value', () => { + const command = 'OUTPUT' + createFileCommandFile(command) + + expect(() => { + core.setOutput('my out', `good stuff ${DELIMITER} bad stuff`) + }).toThrow( + `Unexpected input: value should not contain the delimiter "${DELIMITER}"` + ) + + const filePath = path.join(__dirname, `test/${command}`) + fs.unlinkSync(filePath) + }) + + it('setOutput does not allow delimiter as name', () => { + const command = 'OUTPUT' + createFileCommandFile(command) + + expect(() => { + core.setOutput(`good stuff ${DELIMITER} bad stuff`, 'test') + }).toThrow( + `Unexpected input: name should not contain the delimiter "${DELIMITER}"` + ) + + const filePath = path.join(__dirname, `test/${command}`) + fs.unlinkSync(filePath) + }) + it('setFailed sets the correct exit code and failure message', () => { core.setFailed('Failure message') expect(process.exitCode).toBe(core.ExitCode.Failure) @@ -466,21 +524,79 @@ describe('@actions/core', () => { assertWriteCalls([`::debug::%0D%0Adebug%0A${os.EOL}`]) }) - it('saveState produces the correct command', () => { + it('legacy saveState produces the correct command', () => { core.saveState('state_1', 'some value') assertWriteCalls([`::save-state name=state_1::some value${os.EOL}`]) }) - it('saveState handles numbers', () => { + it('legacy saveState handles numbers', () => { core.saveState('state_1', 1) assertWriteCalls([`::save-state name=state_1::1${os.EOL}`]) }) - it('saveState handles bools', () => { + it('legacy saveState handles bools', () => { core.saveState('state_1', true) assertWriteCalls([`::save-state name=state_1::true${os.EOL}`]) }) + it('saveState produces the correct command and saves the state', () => { + const command = 'STATE' + createFileCommandFile(command) + core.saveState('my state', 'out val') + verifyFileCommand( + command, + `my state<<${DELIMITER}${os.EOL}out val${os.EOL}${DELIMITER}${os.EOL}` + ) + }) + + it('saveState handles boolean inputs', () => { + const command = 'STATE' + createFileCommandFile(command) + core.saveState('my state', true) + verifyFileCommand( + command, + `my state<<${DELIMITER}${os.EOL}true${os.EOL}${DELIMITER}${os.EOL}` + ) + }) + + it('saveState handles number inputs', () => { + const command = 'STATE' + createFileCommandFile(command) + core.saveState('my state', 5) + verifyFileCommand( + command, + `my state<<${DELIMITER}${os.EOL}5${os.EOL}${DELIMITER}${os.EOL}` + ) + }) + + it('saveState does not allow delimiter as value', () => { + const command = 'STATE' + createFileCommandFile(command) + + expect(() => { + core.saveState('my state', `good stuff ${DELIMITER} bad stuff`) + }).toThrow( + `Unexpected input: value should not contain the delimiter "${DELIMITER}"` + ) + + const filePath = path.join(__dirname, `test/${command}`) + fs.unlinkSync(filePath) + }) + + it('saveState does not allow delimiter as name', () => { + const command = 'STATE' + createFileCommandFile(command) + + expect(() => { + core.saveState(`good stuff ${DELIMITER} bad stuff`, 'test') + }).toThrow( + `Unexpected input: name should not contain the delimiter "${DELIMITER}"` + ) + + const filePath = path.join(__dirname, `test/${command}`) + fs.unlinkSync(filePath) + }) + it('getState gets wrapper action state', () => { expect(core.getState('TEST_1')).toBe('state_val') }) diff --git a/packages/core/src/core.ts b/packages/core/src/core.ts index 36f8f433ab..179424a362 100644 --- a/packages/core/src/core.ts +++ b/packages/core/src/core.ts @@ -1,10 +1,12 @@ import {issue, issueCommand} from './command' -import {issueCommand as issueFileCommand} from './file-command' +import { + issueCommand as issueFileCommand, + prepareKeyValueMessage as prepareCommandValue +} from './file-command' import {toCommandProperties, toCommandValue} from './utils' import * as os from 'os' import * as path from 'path' -import {v4 as uuidv4} from 'uuid' import {OidcClient} from './oidc-utils' @@ -87,26 +89,10 @@ export function exportVariable(name: string, val: any): void { const filePath = process.env['GITHUB_ENV'] || '' if (filePath) { - const delimiter = `ghadelimiter_${uuidv4()}` - - // These should realistically never happen, but just in case someone finds a way to exploit uuid generation let's not allow keys or values that contain the delimiter. - if (name.includes(delimiter)) { - throw new Error( - `Unexpected input: name should not contain the delimiter "${delimiter}"` - ) - } - - if (convertedVal.includes(delimiter)) { - throw new Error( - `Unexpected input: value should not contain the delimiter "${delimiter}"` - ) - } - - const commandValue = `${name}<<${delimiter}${os.EOL}${convertedVal}${os.EOL}${delimiter}` - issueFileCommand('ENV', commandValue) - } else { - issueCommand('set-env', {name}, convertedVal) + return issueFileCommand('ENV', prepareCommandValue(name, val)) } + + issueCommand('set-env', {name}, convertedVal) } /** @@ -207,8 +193,13 @@ export function getBooleanInput(name: string, options?: InputOptions): boolean { */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function setOutput(name: string, value: any): void { + const filePath = process.env['GITHUB_OUTPUT'] || '' + if (filePath) { + return issueFileCommand('OUTPUT', prepareCommandValue(name, value)) + } + process.stdout.write(os.EOL) - issueCommand('set-output', {name}, value) + issueCommand('set-output', {name}, toCommandValue(value)) } /** @@ -362,7 +353,12 @@ export async function group(name: string, fn: () => Promise): Promise { */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function saveState(name: string, value: any): void { - issueCommand('save-state', {name}, value) + const filePath = process.env['GITHUB_STATE'] || '' + if (filePath) { + return issueFileCommand('STATE', prepareCommandValue(name, value)) + } + + issueCommand('save-state', {name}, toCommandValue(value)) } /** diff --git a/packages/core/src/file-command.ts b/packages/core/src/file-command.ts index 978d8ff94b..283770e648 100644 --- a/packages/core/src/file-command.ts +++ b/packages/core/src/file-command.ts @@ -5,6 +5,7 @@ import * as fs from 'fs' import * as os from 'os' +import {v4 as uuidv4} from 'uuid' import {toCommandValue} from './utils' export function issueCommand(command: string, message: any): void { @@ -22,3 +23,25 @@ export function issueCommand(command: string, message: any): void { encoding: 'utf8' }) } + +export function prepareKeyValueMessage(key: string, value: any): string { + const delimiter = `ghadelimiter_${uuidv4()}` + const convertedValue = toCommandValue(value) + + // These should realistically never happen, but just in case someone finds a + // way to exploit uuid generation let's not allow keys or values that contain + // the delimiter. + if (key.includes(delimiter)) { + throw new Error( + `Unexpected input: name should not contain the delimiter "${delimiter}"` + ) + } + + if (convertedValue.includes(delimiter)) { + throw new Error( + `Unexpected input: value should not contain the delimiter "${delimiter}"` + ) + } + + return `${key}<<${delimiter}${os.EOL}${convertedValue}${os.EOL}${delimiter}` +} From 28f9431d3c2a0f934f7414b3c3df248922c8cf09 Mon Sep 17 00:00:00 2001 From: Francesco Renzi Date: Fri, 16 Sep 2022 13:38:27 +0100 Subject: [PATCH 2/3] Address review comments --- packages/core/src/core.ts | 11 ++++------- packages/core/src/file-command.ts | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/core/src/core.ts b/packages/core/src/core.ts index 179424a362..bd98e4b598 100644 --- a/packages/core/src/core.ts +++ b/packages/core/src/core.ts @@ -1,8 +1,5 @@ import {issue, issueCommand} from './command' -import { - issueCommand as issueFileCommand, - prepareKeyValueMessage as prepareCommandValue -} from './file-command' +import {issueFileCommand, prepareKeyValueMessage} from './file-command' import {toCommandProperties, toCommandValue} from './utils' import * as os from 'os' @@ -89,7 +86,7 @@ export function exportVariable(name: string, val: any): void { const filePath = process.env['GITHUB_ENV'] || '' if (filePath) { - return issueFileCommand('ENV', prepareCommandValue(name, val)) + return issueFileCommand('ENV', prepareKeyValueMessage(name, val)) } issueCommand('set-env', {name}, convertedVal) @@ -195,7 +192,7 @@ export function getBooleanInput(name: string, options?: InputOptions): boolean { export function setOutput(name: string, value: any): void { const filePath = process.env['GITHUB_OUTPUT'] || '' if (filePath) { - return issueFileCommand('OUTPUT', prepareCommandValue(name, value)) + return issueFileCommand('OUTPUT', prepareKeyValueMessage(name, value)) } process.stdout.write(os.EOL) @@ -355,7 +352,7 @@ export async function group(name: string, fn: () => Promise): Promise { export function saveState(name: string, value: any): void { const filePath = process.env['GITHUB_STATE'] || '' if (filePath) { - return issueFileCommand('STATE', prepareCommandValue(name, value)) + return issueFileCommand('STATE', prepareKeyValueMessage(name, value)) } issueCommand('save-state', {name}, toCommandValue(value)) diff --git a/packages/core/src/file-command.ts b/packages/core/src/file-command.ts index 283770e648..832c2f0e61 100644 --- a/packages/core/src/file-command.ts +++ b/packages/core/src/file-command.ts @@ -8,7 +8,7 @@ import * as os from 'os' import {v4 as uuidv4} from 'uuid' import {toCommandValue} from './utils' -export function issueCommand(command: string, message: any): void { +export function issueFileCommand(command: string, message: any): void { const filePath = process.env[`GITHUB_${command}`] if (!filePath) { throw new Error( From 3a95e19ebd9de276354a7b4f7a0b98dd8580590b Mon Sep 17 00:00:00 2001 From: Francesco Renzi Date: Thu, 29 Sep 2022 09:46:47 +0000 Subject: [PATCH 3/3] default new env vars to empty --- packages/core/__tests__/core.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core/__tests__/core.test.ts b/packages/core/__tests__/core.test.ts index 42e62c9afd..5011fcc8bf 100644 --- a/packages/core/__tests__/core.test.ts +++ b/packages/core/__tests__/core.test.ts @@ -41,7 +41,9 @@ const testEnvVars = { // File Commands GITHUB_PATH: '', - GITHUB_ENV: '' + GITHUB_ENV: '', + GITHUB_OUTPUT: '', + GITHUB_STATE: '' } const UUID = '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'