From b00a9fd033f4b30f2355acd212f531ecbbb9b38f Mon Sep 17 00:00:00 2001 From: Francesco Renzi Date: Thu, 29 Sep 2022 14:45:02 +0100 Subject: [PATCH] Add save-state and set-output file commands (#1178) --- packages/core/__tests__/core.test.ts | 132 +++++++++++++++++++++++++-- packages/core/src/core.ts | 39 ++++---- packages/core/src/file-command.ts | 25 ++++- 3 files changed, 165 insertions(+), 31 deletions(-) diff --git a/packages/core/__tests__/core.test.ts b/packages/core/__tests__/core.test.ts index d00374fd35..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' @@ -283,7 +285,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 +293,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 +526,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..bd98e4b598 100644 --- a/packages/core/src/core.ts +++ b/packages/core/src/core.ts @@ -1,10 +1,9 @@ import {issue, issueCommand} from './command' -import {issueCommand as issueFileCommand} from './file-command' +import {issueFileCommand, prepareKeyValueMessage} 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 +86,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', prepareKeyValueMessage(name, val)) } + + issueCommand('set-env', {name}, convertedVal) } /** @@ -207,8 +190,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', prepareKeyValueMessage(name, value)) + } + process.stdout.write(os.EOL) - issueCommand('set-output', {name}, value) + issueCommand('set-output', {name}, toCommandValue(value)) } /** @@ -362,7 +350,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', 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 978d8ff94b..832c2f0e61 100644 --- a/packages/core/src/file-command.ts +++ b/packages/core/src/file-command.ts @@ -5,9 +5,10 @@ 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 { +export function issueFileCommand(command: string, message: any): void { const filePath = process.env[`GITHUB_${command}`] if (!filePath) { throw new Error( @@ -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}` +}