Skip to content

Commit b00a9fd

Browse files
authored
Add save-state and set-output file commands (#1178)
1 parent 4df4517 commit b00a9fd

File tree

3 files changed

+165
-31
lines changed

3 files changed

+165
-31
lines changed

packages/core/__tests__/core.test.ts

Lines changed: 125 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ const testEnvVars = {
4141

4242
// File Commands
4343
GITHUB_PATH: '',
44-
GITHUB_ENV: ''
44+
GITHUB_ENV: '',
45+
GITHUB_OUTPUT: '',
46+
GITHUB_STATE: ''
4547
}
4648

4749
const UUID = '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'
@@ -283,24 +285,82 @@ describe('@actions/core', () => {
283285
).toEqual([' val1 ', ' val2 ', ' '])
284286
})
285287

286-
it('setOutput produces the correct command', () => {
288+
it('legacy setOutput produces the correct command', () => {
287289
core.setOutput('some output', 'some value')
288290
assertWriteCalls([
289291
os.EOL,
290292
`::set-output name=some output::some value${os.EOL}`
291293
])
292294
})
293295

294-
it('setOutput handles bools', () => {
296+
it('legacy setOutput handles bools', () => {
295297
core.setOutput('some output', false)
296298
assertWriteCalls([os.EOL, `::set-output name=some output::false${os.EOL}`])
297299
})
298300

299-
it('setOutput handles numbers', () => {
301+
it('legacy setOutput handles numbers', () => {
300302
core.setOutput('some output', 1.01)
301303
assertWriteCalls([os.EOL, `::set-output name=some output::1.01${os.EOL}`])
302304
})
303305

306+
it('setOutput produces the correct command and sets the output', () => {
307+
const command = 'OUTPUT'
308+
createFileCommandFile(command)
309+
core.setOutput('my out', 'out val')
310+
verifyFileCommand(
311+
command,
312+
`my out<<${DELIMITER}${os.EOL}out val${os.EOL}${DELIMITER}${os.EOL}`
313+
)
314+
})
315+
316+
it('setOutput handles boolean inputs', () => {
317+
const command = 'OUTPUT'
318+
createFileCommandFile(command)
319+
core.setOutput('my out', true)
320+
verifyFileCommand(
321+
command,
322+
`my out<<${DELIMITER}${os.EOL}true${os.EOL}${DELIMITER}${os.EOL}`
323+
)
324+
})
325+
326+
it('setOutput handles number inputs', () => {
327+
const command = 'OUTPUT'
328+
createFileCommandFile(command)
329+
core.setOutput('my out', 5)
330+
verifyFileCommand(
331+
command,
332+
`my out<<${DELIMITER}${os.EOL}5${os.EOL}${DELIMITER}${os.EOL}`
333+
)
334+
})
335+
336+
it('setOutput does not allow delimiter as value', () => {
337+
const command = 'OUTPUT'
338+
createFileCommandFile(command)
339+
340+
expect(() => {
341+
core.setOutput('my out', `good stuff ${DELIMITER} bad stuff`)
342+
}).toThrow(
343+
`Unexpected input: value should not contain the delimiter "${DELIMITER}"`
344+
)
345+
346+
const filePath = path.join(__dirname, `test/${command}`)
347+
fs.unlinkSync(filePath)
348+
})
349+
350+
it('setOutput does not allow delimiter as name', () => {
351+
const command = 'OUTPUT'
352+
createFileCommandFile(command)
353+
354+
expect(() => {
355+
core.setOutput(`good stuff ${DELIMITER} bad stuff`, 'test')
356+
}).toThrow(
357+
`Unexpected input: name should not contain the delimiter "${DELIMITER}"`
358+
)
359+
360+
const filePath = path.join(__dirname, `test/${command}`)
361+
fs.unlinkSync(filePath)
362+
})
363+
304364
it('setFailed sets the correct exit code and failure message', () => {
305365
core.setFailed('Failure message')
306366
expect(process.exitCode).toBe(core.ExitCode.Failure)
@@ -466,21 +526,79 @@ describe('@actions/core', () => {
466526
assertWriteCalls([`::debug::%0D%0Adebug%0A${os.EOL}`])
467527
})
468528

469-
it('saveState produces the correct command', () => {
529+
it('legacy saveState produces the correct command', () => {
470530
core.saveState('state_1', 'some value')
471531
assertWriteCalls([`::save-state name=state_1::some value${os.EOL}`])
472532
})
473533

474-
it('saveState handles numbers', () => {
534+
it('legacy saveState handles numbers', () => {
475535
core.saveState('state_1', 1)
476536
assertWriteCalls([`::save-state name=state_1::1${os.EOL}`])
477537
})
478538

479-
it('saveState handles bools', () => {
539+
it('legacy saveState handles bools', () => {
480540
core.saveState('state_1', true)
481541
assertWriteCalls([`::save-state name=state_1::true${os.EOL}`])
482542
})
483543

544+
it('saveState produces the correct command and saves the state', () => {
545+
const command = 'STATE'
546+
createFileCommandFile(command)
547+
core.saveState('my state', 'out val')
548+
verifyFileCommand(
549+
command,
550+
`my state<<${DELIMITER}${os.EOL}out val${os.EOL}${DELIMITER}${os.EOL}`
551+
)
552+
})
553+
554+
it('saveState handles boolean inputs', () => {
555+
const command = 'STATE'
556+
createFileCommandFile(command)
557+
core.saveState('my state', true)
558+
verifyFileCommand(
559+
command,
560+
`my state<<${DELIMITER}${os.EOL}true${os.EOL}${DELIMITER}${os.EOL}`
561+
)
562+
})
563+
564+
it('saveState handles number inputs', () => {
565+
const command = 'STATE'
566+
createFileCommandFile(command)
567+
core.saveState('my state', 5)
568+
verifyFileCommand(
569+
command,
570+
`my state<<${DELIMITER}${os.EOL}5${os.EOL}${DELIMITER}${os.EOL}`
571+
)
572+
})
573+
574+
it('saveState does not allow delimiter as value', () => {
575+
const command = 'STATE'
576+
createFileCommandFile(command)
577+
578+
expect(() => {
579+
core.saveState('my state', `good stuff ${DELIMITER} bad stuff`)
580+
}).toThrow(
581+
`Unexpected input: value should not contain the delimiter "${DELIMITER}"`
582+
)
583+
584+
const filePath = path.join(__dirname, `test/${command}`)
585+
fs.unlinkSync(filePath)
586+
})
587+
588+
it('saveState does not allow delimiter as name', () => {
589+
const command = 'STATE'
590+
createFileCommandFile(command)
591+
592+
expect(() => {
593+
core.saveState(`good stuff ${DELIMITER} bad stuff`, 'test')
594+
}).toThrow(
595+
`Unexpected input: name should not contain the delimiter "${DELIMITER}"`
596+
)
597+
598+
const filePath = path.join(__dirname, `test/${command}`)
599+
fs.unlinkSync(filePath)
600+
})
601+
484602
it('getState gets wrapper action state', () => {
485603
expect(core.getState('TEST_1')).toBe('state_val')
486604
})

packages/core/src/core.ts

Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import {issue, issueCommand} from './command'
2-
import {issueCommand as issueFileCommand} from './file-command'
2+
import {issueFileCommand, prepareKeyValueMessage} from './file-command'
33
import {toCommandProperties, toCommandValue} from './utils'
44

55
import * as os from 'os'
66
import * as path from 'path'
7-
import {v4 as uuidv4} from 'uuid'
87

98
import {OidcClient} from './oidc-utils'
109

@@ -87,26 +86,10 @@ export function exportVariable(name: string, val: any): void {
8786

8887
const filePath = process.env['GITHUB_ENV'] || ''
8988
if (filePath) {
90-
const delimiter = `ghadelimiter_${uuidv4()}`
91-
92-
// 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.
93-
if (name.includes(delimiter)) {
94-
throw new Error(
95-
`Unexpected input: name should not contain the delimiter "${delimiter}"`
96-
)
97-
}
98-
99-
if (convertedVal.includes(delimiter)) {
100-
throw new Error(
101-
`Unexpected input: value should not contain the delimiter "${delimiter}"`
102-
)
103-
}
104-
105-
const commandValue = `${name}<<${delimiter}${os.EOL}${convertedVal}${os.EOL}${delimiter}`
106-
issueFileCommand('ENV', commandValue)
107-
} else {
108-
issueCommand('set-env', {name}, convertedVal)
89+
return issueFileCommand('ENV', prepareKeyValueMessage(name, val))
10990
}
91+
92+
issueCommand('set-env', {name}, convertedVal)
11093
}
11194

11295
/**
@@ -207,8 +190,13 @@ export function getBooleanInput(name: string, options?: InputOptions): boolean {
207190
*/
208191
// eslint-disable-next-line @typescript-eslint/no-explicit-any
209192
export function setOutput(name: string, value: any): void {
193+
const filePath = process.env['GITHUB_OUTPUT'] || ''
194+
if (filePath) {
195+
return issueFileCommand('OUTPUT', prepareKeyValueMessage(name, value))
196+
}
197+
210198
process.stdout.write(os.EOL)
211-
issueCommand('set-output', {name}, value)
199+
issueCommand('set-output', {name}, toCommandValue(value))
212200
}
213201

214202
/**
@@ -362,7 +350,12 @@ export async function group<T>(name: string, fn: () => Promise<T>): Promise<T> {
362350
*/
363351
// eslint-disable-next-line @typescript-eslint/no-explicit-any
364352
export function saveState(name: string, value: any): void {
365-
issueCommand('save-state', {name}, value)
353+
const filePath = process.env['GITHUB_STATE'] || ''
354+
if (filePath) {
355+
return issueFileCommand('STATE', prepareKeyValueMessage(name, value))
356+
}
357+
358+
issueCommand('save-state', {name}, toCommandValue(value))
366359
}
367360

368361
/**

packages/core/src/file-command.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55

66
import * as fs from 'fs'
77
import * as os from 'os'
8+
import {v4 as uuidv4} from 'uuid'
89
import {toCommandValue} from './utils'
910

10-
export function issueCommand(command: string, message: any): void {
11+
export function issueFileCommand(command: string, message: any): void {
1112
const filePath = process.env[`GITHUB_${command}`]
1213
if (!filePath) {
1314
throw new Error(
@@ -22,3 +23,25 @@ export function issueCommand(command: string, message: any): void {
2223
encoding: 'utf8'
2324
})
2425
}
26+
27+
export function prepareKeyValueMessage(key: string, value: any): string {
28+
const delimiter = `ghadelimiter_${uuidv4()}`
29+
const convertedValue = toCommandValue(value)
30+
31+
// These should realistically never happen, but just in case someone finds a
32+
// way to exploit uuid generation let's not allow keys or values that contain
33+
// the delimiter.
34+
if (key.includes(delimiter)) {
35+
throw new Error(
36+
`Unexpected input: name should not contain the delimiter "${delimiter}"`
37+
)
38+
}
39+
40+
if (convertedValue.includes(delimiter)) {
41+
throw new Error(
42+
`Unexpected input: value should not contain the delimiter "${delimiter}"`
43+
)
44+
}
45+
46+
return `${key}<<${delimiter}${os.EOL}${convertedValue}${os.EOL}${delimiter}`
47+
}

0 commit comments

Comments
 (0)