diff --git a/docs/commands.md b/docs/commands.md index 30d58d9437..97eaa8b1fa 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -7,37 +7,6 @@ these things in a script or other tool. To allow this, we provide a special `::` syntax which, if logged to `stdout` on a new line, will allow the runner to perform special behavior on your commands. The following commands are all supported: -### Set an environment variable - -To set an environment variable for future out of process steps, use `::set-env`: - -```sh -echo "::set-env name=FOO::BAR" -``` - -Running `$FOO` in a future step will now return `BAR` - -This is wrapped by the core exportVariable method which sets for future steps but also updates the variable for this step - -```javascript -export function exportVariable(name: string, val: string): void {} -``` - -### PATH Manipulation - -To prepend a string to PATH, use `::addPath`: - -```sh -echo "::add-path::BAR" -``` - -Running `$PATH` in a future step will now return `BAR:{Previous Path}`; - -This is wrapped by the core addPath method: -```javascript -export function addPath(inputPath: string): void {} -``` - ### Set outputs To set an output for the step, use `::set-output`: @@ -155,8 +124,90 @@ function setCommandEcho(enabled: boolean): void {} The `add-mask`, `debug`, `warning` and `error` commands do not support echoing. -### Command Prompt +### Command Prompt + CMD processes the `"` character differently from other shells when echoing. In CMD, the above snippets should have the `"` characters removed in order to correctly process. For example, the set output command would be: ```cmd echo ::set-output name=FOO::BAR ``` + + +# File Commands + +During the execution of a workflow, the runner generates temporary files that you can write to to perform certain actions. The path to these files are exposed via environment variables. You will need to use the `utf-8` encoding when writing to these files to ensure proper processing of the commands. + +### Set an environment variable + +To set an environment variable for future out of process steps, write to the file located at `GITHUB_ENV` or use the equivalent `actions/core` function + +```sh +echo "FOO=BAR" >> $GITHUB_ENV +``` + +Running `$FOO` in a future step will now return `BAR` + +For multiline strings, you may use the [heredoc syntax](https://tldp.org/LDP/abs/html/here-docs.html) with your choice of delimeter. In the below example, we use `EOF` +``` +steps: + - name: Set the value + id: step_one + run: | + echo 'JSON_RESPONSE<> $GITHUB_ENV + curl https://httpbin.org/json >> $GITHUB_ENV + echo 'EOF' >> $GITHUB_ENV +``` + +This would set the value of the `JSON_RESPONSE` env variable to: + +``` +{ + "slideshow": { + "author": "Yours Truly", + "date": "date of publication", + "slides": [ + { + "title": "Wake up to WonderWidgets!", + "type": "all" + }, + { + "items": [ + "Why WonderWidgets are great", + "Who buys WonderWidgets" + ], + "title": "Overview", + "type": "all" + } + ], + "title": "Sample Slide Show" + } +} +``` + +This is wrapped by the core `exportVariable` method which sets for future steps but also updates the variable for this step. + +```javascript +export function exportVariable(name: string, val: string): void {} +``` + +### PATH Manipulation + +To prepend a string to PATH write to the file located at `GITHUB_PATH` or use the equivalent `actions/core` function + +```sh +echo "foo=bar" >> $GITHUB_PATH +``` + +Running `$PATH` in a future step will now return `BAR:{Previous Path}`; + +This is wrapped by the core addPath method: +```javascript +export function addPath(inputPath: string): void {} +``` + +### Powershell + +Powershell does not use UTF8 by default. You will want to make sure you write in the correct encoding. For example, to set the path: +``` +steps: + - run: echo "mypath" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 +``` \ No newline at end of file diff --git a/packages/core/__tests__/core.test.ts b/packages/core/__tests__/core.test.ts index 2901065262..1cdb7bffd1 100644 --- a/packages/core/__tests__/core.test.ts +++ b/packages/core/__tests__/core.test.ts @@ -1,3 +1,4 @@ +import * as fs from 'fs' import * as os from 'os' import * as path from 'path' import * as core from '../src/core' @@ -24,6 +25,13 @@ const testEnvVars = { } describe('@actions/core', () => { + beforeAll(() => { + const filePath = path.join(__dirname, `test`) + if (!fs.existsSync(filePath)) { + fs.mkdirSync(filePath) + } + }) + beforeEach(() => { for (const key in testEnvVars) process.env[key] = testEnvVars[key as keyof typeof testEnvVars] @@ -36,32 +44,33 @@ describe('@actions/core', () => { }) it('exportVariable produces the correct command and sets the env', () => { + const command = 'ENV' + createFileCommandFile(command) core.exportVariable('my var', 'var val') - assertWriteCalls([`::set-env name=my var::var val${os.EOL}`]) - }) - - it('exportVariable escapes variable names', () => { - core.exportVariable('special char var \r\n,:', 'special val') - expect(process.env['special char var \r\n,:']).toBe('special val') - assertWriteCalls([ - `::set-env name=special char var %0D%0A%2C%3A::special val${os.EOL}` - ]) - }) - - it('exportVariable escapes variable values', () => { - core.exportVariable('my var2', 'var val\r\n') - expect(process.env['my var2']).toBe('var val\r\n') - assertWriteCalls([`::set-env name=my var2::var val%0D%0A${os.EOL}`]) + verifyFileCommand( + command, + `my var<<_GitHubActionsFileCommandDelimeter_${os.EOL}var val${os.EOL}_GitHubActionsFileCommandDelimeter_${os.EOL}` + ) }) it('exportVariable handles boolean inputs', () => { + const command = 'ENV' + createFileCommandFile(command) core.exportVariable('my var', true) - assertWriteCalls([`::set-env name=my var::true${os.EOL}`]) + verifyFileCommand( + command, + `my var<<_GitHubActionsFileCommandDelimeter_${os.EOL}true${os.EOL}_GitHubActionsFileCommandDelimeter_${os.EOL}` + ) }) it('exportVariable handles number inputs', () => { + const command = 'ENV' + createFileCommandFile(command) core.exportVariable('my var', 5) - assertWriteCalls([`::set-env name=my var::5${os.EOL}`]) + verifyFileCommand( + command, + `my var<<_GitHubActionsFileCommandDelimeter_${os.EOL}5${os.EOL}_GitHubActionsFileCommandDelimeter_${os.EOL}` + ) }) it('setSecret produces the correct command', () => { @@ -70,11 +79,13 @@ describe('@actions/core', () => { }) it('prependPath produces the correct commands and sets the env', () => { + const command = 'PATH' + createFileCommandFile(command) core.addPath('myPath') expect(process.env['PATH']).toBe( `myPath${path.delimiter}path1${path.delimiter}path2` ) - assertWriteCalls([`::add-path::myPath${os.EOL}`]) + verifyFileCommand(command, `myPath${os.EOL}`) }) it('getInput gets non-required input', () => { @@ -259,3 +270,21 @@ function assertWriteCalls(calls: string[]): void { expect(process.stdout.write).toHaveBeenNthCalledWith(i + 1, calls[i]) } } + +function createFileCommandFile(command: string): void { + const filePath = path.join(__dirname, `test/${command}`) + process.env[`GITHUB_${command}`] = filePath + fs.appendFileSync(filePath, '', { + encoding: 'utf8' + }) +} + +function verifyFileCommand(command: string, expectedContents: string): void { + const filePath = path.join(__dirname, `test/${command}`) + const contents = fs.readFileSync(filePath, 'utf8') + try { + expect(contents).toEqual(expectedContents) + } finally { + fs.unlinkSync(filePath) + } +} diff --git a/packages/core/src/command.ts b/packages/core/src/command.ts index acdf9def37..63b5bca642 100644 --- a/packages/core/src/command.ts +++ b/packages/core/src/command.ts @@ -1,4 +1,5 @@ import * as os from 'os' +import {toCommandValue} from './utils' // For internal use, subject to change. @@ -76,19 +77,6 @@ class Command { } } -/** - * Sanitizes an input into a string so it can be passed into issueCommand safely - * @param input input to sanitize into a string - */ -export function toCommandValue(input: any): string { - if (input === null || input === undefined) { - return '' - } else if (typeof input === 'string' || input instanceof String) { - return input as string - } - return JSON.stringify(input) -} - function escapeData(s: any): string { return toCommandValue(s) .replace(/%/g, '%25') diff --git a/packages/core/src/core.ts b/packages/core/src/core.ts index 0cf9f04ca7..d5cfdbcada 100644 --- a/packages/core/src/core.ts +++ b/packages/core/src/core.ts @@ -1,4 +1,6 @@ -import {issue, issueCommand, toCommandValue} from './command' +import {issue, issueCommand} from './command' +import {issueCommand as issueFileCommand} from './file-command' +import {toCommandValue} from './utils' import * as os from 'os' import * as path from 'path' @@ -39,7 +41,9 @@ export enum ExitCode { export function exportVariable(name: string, val: any): void { const convertedVal = toCommandValue(val) process.env[name] = convertedVal - issueCommand('set-env', {name}, convertedVal) + const delimiter = '_GitHubActionsFileCommandDelimeter_' + const commandValue = `${name}<<${delimiter}${os.EOL}${convertedVal}${os.EOL}${delimiter}` + issueFileCommand('ENV', commandValue) } /** @@ -55,7 +59,7 @@ export function setSecret(secret: string): void { * @param inputPath */ export function addPath(inputPath: string): void { - issueCommand('add-path', {}, inputPath) + issueFileCommand('PATH', inputPath) process.env['PATH'] = `${inputPath}${path.delimiter}${process.env['PATH']}` } diff --git a/packages/core/src/file-command.ts b/packages/core/src/file-command.ts new file mode 100644 index 0000000000..978d8ff94b --- /dev/null +++ b/packages/core/src/file-command.ts @@ -0,0 +1,24 @@ +// For internal use, subject to change. + +// We use any as a valid input type +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import * as fs from 'fs' +import * as os from 'os' +import {toCommandValue} from './utils' + +export function issueCommand(command: string, message: any): void { + const filePath = process.env[`GITHUB_${command}`] + if (!filePath) { + throw new Error( + `Unable to find environment variable for file command ${command}` + ) + } + if (!fs.existsSync(filePath)) { + throw new Error(`Missing file at path: ${filePath}`) + } + + fs.appendFileSync(filePath, `${toCommandValue(message)}${os.EOL}`, { + encoding: 'utf8' + }) +} diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts new file mode 100644 index 0000000000..2f1d60ceb3 --- /dev/null +++ b/packages/core/src/utils.ts @@ -0,0 +1,15 @@ +// We use any as a valid input type +/* eslint-disable @typescript-eslint/no-explicit-any */ + +/** + * Sanitizes an input into a string so it can be passed into issueCommand safely + * @param input input to sanitize into a string + */ +export function toCommandValue(input: any): string { + if (input === null || input === undefined) { + return '' + } else if (typeof input === 'string' || input instanceof String) { + return input as string + } + return JSON.stringify(input) +}