mirror of https://github.com/actions/toolkit
Add save-state and set-output file commands (#1178)
parent
4df45177e4
commit
b00a9fd033
|
@ -41,7 +41,9 @@ const testEnvVars = {
|
||||||
|
|
||||||
// File Commands
|
// File Commands
|
||||||
GITHUB_PATH: '',
|
GITHUB_PATH: '',
|
||||||
GITHUB_ENV: ''
|
GITHUB_ENV: '',
|
||||||
|
GITHUB_OUTPUT: '',
|
||||||
|
GITHUB_STATE: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const UUID = '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'
|
const UUID = '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'
|
||||||
|
@ -283,7 +285,7 @@ describe('@actions/core', () => {
|
||||||
).toEqual([' val1 ', ' val2 ', ' '])
|
).toEqual([' val1 ', ' val2 ', ' '])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('setOutput produces the correct command', () => {
|
it('legacy setOutput produces the correct command', () => {
|
||||||
core.setOutput('some output', 'some value')
|
core.setOutput('some output', 'some value')
|
||||||
assertWriteCalls([
|
assertWriteCalls([
|
||||||
os.EOL,
|
os.EOL,
|
||||||
|
@ -291,16 +293,74 @@ describe('@actions/core', () => {
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('setOutput handles bools', () => {
|
it('legacy setOutput handles bools', () => {
|
||||||
core.setOutput('some output', false)
|
core.setOutput('some output', false)
|
||||||
assertWriteCalls([os.EOL, `::set-output name=some output::false${os.EOL}`])
|
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)
|
core.setOutput('some output', 1.01)
|
||||||
assertWriteCalls([os.EOL, `::set-output name=some output::1.01${os.EOL}`])
|
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', () => {
|
it('setFailed sets the correct exit code and failure message', () => {
|
||||||
core.setFailed('Failure message')
|
core.setFailed('Failure message')
|
||||||
expect(process.exitCode).toBe(core.ExitCode.Failure)
|
expect(process.exitCode).toBe(core.ExitCode.Failure)
|
||||||
|
@ -466,21 +526,79 @@ describe('@actions/core', () => {
|
||||||
assertWriteCalls([`::debug::%0D%0Adebug%0A${os.EOL}`])
|
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')
|
core.saveState('state_1', 'some value')
|
||||||
assertWriteCalls([`::save-state name=state_1::some value${os.EOL}`])
|
assertWriteCalls([`::save-state name=state_1::some value${os.EOL}`])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('saveState handles numbers', () => {
|
it('legacy saveState handles numbers', () => {
|
||||||
core.saveState('state_1', 1)
|
core.saveState('state_1', 1)
|
||||||
assertWriteCalls([`::save-state name=state_1::1${os.EOL}`])
|
assertWriteCalls([`::save-state name=state_1::1${os.EOL}`])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('saveState handles bools', () => {
|
it('legacy saveState handles bools', () => {
|
||||||
core.saveState('state_1', true)
|
core.saveState('state_1', true)
|
||||||
assertWriteCalls([`::save-state name=state_1::true${os.EOL}`])
|
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', () => {
|
it('getState gets wrapper action state', () => {
|
||||||
expect(core.getState('TEST_1')).toBe('state_val')
|
expect(core.getState('TEST_1')).toBe('state_val')
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import {issue, issueCommand} from './command'
|
import {issue, issueCommand} from './command'
|
||||||
import {issueCommand as issueFileCommand} from './file-command'
|
import {issueFileCommand, prepareKeyValueMessage} from './file-command'
|
||||||
import {toCommandProperties, toCommandValue} from './utils'
|
import {toCommandProperties, toCommandValue} from './utils'
|
||||||
|
|
||||||
import * as os from 'os'
|
import * as os from 'os'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import {v4 as uuidv4} from 'uuid'
|
|
||||||
|
|
||||||
import {OidcClient} from './oidc-utils'
|
import {OidcClient} from './oidc-utils'
|
||||||
|
|
||||||
|
@ -87,26 +86,10 @@ export function exportVariable(name: string, val: any): void {
|
||||||
|
|
||||||
const filePath = process.env['GITHUB_ENV'] || ''
|
const filePath = process.env['GITHUB_ENV'] || ''
|
||||||
if (filePath) {
|
if (filePath) {
|
||||||
const delimiter = `ghadelimiter_${uuidv4()}`
|
return issueFileCommand('ENV', prepareKeyValueMessage(name, val))
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export function setOutput(name: string, value: any): void {
|
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)
|
process.stdout.write(os.EOL)
|
||||||
issueCommand('set-output', {name}, value)
|
issueCommand('set-output', {name}, toCommandValue(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -362,7 +350,12 @@ export async function group<T>(name: string, fn: () => Promise<T>): Promise<T> {
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export function saveState(name: string, value: any): void {
|
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))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -5,9 +5,10 @@
|
||||||
|
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import * as os from 'os'
|
import * as os from 'os'
|
||||||
|
import {v4 as uuidv4} from 'uuid'
|
||||||
import {toCommandValue} from './utils'
|
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}`]
|
const filePath = process.env[`GITHUB_${command}`]
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
@ -22,3 +23,25 @@ export function issueCommand(command: string, message: any): void {
|
||||||
encoding: 'utf8'
|
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}`
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue