1
0
Fork 0

Add File Commands (#571)

* Add File Commands

* pr updates w/ feedback

* run format

* fix lint/format

* slight update with an example in the docs

* pr feedback
pull/582/head
Thomas Boop 2020-09-23 11:19:20 -04:00 committed by GitHub
parent da34bfb74d
commit 0759cdc230
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 203 additions and 61 deletions

View File

@ -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 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: 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 ### Set outputs
To set an output for the step, use `::set-output`: To set an output for the step, use `::set-output`:
@ -156,7 +125,72 @@ function setCommandEcho(enabled: boolean): void {}
The `add-mask`, `debug`, `warning` and `error` commands do not support echoing. 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 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 ```cmd
echo ::set-output name=FOO::BAR echo ::set-output name=FOO::BAR
``` ```
# Environment files
During the execution of a workflow, the runner generates temporary files that can be used 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. Multiple commands can be written to the same file, separated by newlines.
### 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 a heredoc style syntax with your choice of delimeter. In the below example, we use `EOF`
```
steps:
- name: Set the value
id: step_one
run: |
echo 'JSON_RESPONSE<<EOF' >> $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 the value of the curl response.
The expected syntax for the heredoc style is:
```
{VARIABLE_NAME}<<{DELIMETER}
{VARIABLE_VALUE}
{DELIMETER}
```
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 "/Users/test/.nvm/versions/node/v12.18.3/bin" >> $GITHUB_PATH
```
Running `$PATH` in a future step will now return `/Users/test/.nvm/versions/node/v12.18.3/bin:{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
```

View File

@ -1,3 +1,4 @@
import * as fs from 'fs'
import * as os from 'os' import * as os from 'os'
import * as path from 'path' import * as path from 'path'
import * as core from '../src/core' import * as core from '../src/core'
@ -20,27 +21,34 @@ const testEnvVars = {
INPUT_MULTIPLE_SPACES_VARIABLE: 'I have multiple spaces', INPUT_MULTIPLE_SPACES_VARIABLE: 'I have multiple spaces',
// Save inputs // Save inputs
STATE_TEST_1: 'state_val' STATE_TEST_1: 'state_val',
// File Commands
GITHUB_PATH: '',
GITHUB_ENV: ''
} }
describe('@actions/core', () => { describe('@actions/core', () => {
beforeEach(() => { beforeAll(() => {
for (const key in testEnvVars) const filePath = path.join(__dirname, `test`)
process.env[key] = testEnvVars[key as keyof typeof testEnvVars] if (!fs.existsSync(filePath)) {
fs.mkdirSync(filePath)
}
})
beforeEach(() => {
for (const key in testEnvVars) {
process.env[key] = testEnvVars[key as keyof typeof testEnvVars]
}
process.stdout.write = jest.fn() process.stdout.write = jest.fn()
}) })
afterEach(() => { it('legacy exportVariable produces the correct command and sets the env', () => {
for (const key in testEnvVars) Reflect.deleteProperty(testEnvVars, key)
})
it('exportVariable produces the correct command and sets the env', () => {
core.exportVariable('my var', 'var val') core.exportVariable('my var', 'var val')
assertWriteCalls([`::set-env name=my var::var val${os.EOL}`]) assertWriteCalls([`::set-env name=my var::var val${os.EOL}`])
}) })
it('exportVariable escapes variable names', () => { it('legacy exportVariable escapes variable names', () => {
core.exportVariable('special char var \r\n,:', 'special val') core.exportVariable('special char var \r\n,:', 'special val')
expect(process.env['special char var \r\n,:']).toBe('special val') expect(process.env['special char var \r\n,:']).toBe('special val')
assertWriteCalls([ assertWriteCalls([
@ -48,28 +56,68 @@ describe('@actions/core', () => {
]) ])
}) })
it('exportVariable escapes variable values', () => { it('legacy exportVariable escapes variable values', () => {
core.exportVariable('my var2', 'var val\r\n') core.exportVariable('my var2', 'var val\r\n')
expect(process.env['my var2']).toBe('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}`]) assertWriteCalls([`::set-env name=my var2::var val%0D%0A${os.EOL}`])
}) })
it('exportVariable handles boolean inputs', () => { it('legacy exportVariable handles boolean inputs', () => {
core.exportVariable('my var', true) core.exportVariable('my var', true)
assertWriteCalls([`::set-env name=my var::true${os.EOL}`]) assertWriteCalls([`::set-env name=my var::true${os.EOL}`])
}) })
it('exportVariable handles number inputs', () => { it('legacy exportVariable handles number inputs', () => {
core.exportVariable('my var', 5) core.exportVariable('my var', 5)
assertWriteCalls([`::set-env name=my var::5${os.EOL}`]) assertWriteCalls([`::set-env name=my var::5${os.EOL}`])
}) })
it('exportVariable produces the correct command and sets the env', () => {
const command = 'ENV'
createFileCommandFile(command)
core.exportVariable('my var', 'var val')
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)
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)
verifyFileCommand(
command,
`my var<<_GitHubActionsFileCommandDelimeter_${os.EOL}5${os.EOL}_GitHubActionsFileCommandDelimeter_${os.EOL}`
)
})
it('setSecret produces the correct command', () => { it('setSecret produces the correct command', () => {
core.setSecret('secret val') core.setSecret('secret val')
assertWriteCalls([`::add-mask::secret val${os.EOL}`]) assertWriteCalls([`::add-mask::secret val${os.EOL}`])
}) })
it('prependPath produces the correct commands and sets the env', () => { 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`
)
verifyFileCommand(command, `myPath${os.EOL}`)
})
it('legacy prependPath produces the correct commands and sets the env', () => {
core.addPath('myPath') core.addPath('myPath')
expect(process.env['PATH']).toBe( expect(process.env['PATH']).toBe(
`myPath${path.delimiter}path1${path.delimiter}path2` `myPath${path.delimiter}path1${path.delimiter}path2`
@ -259,3 +307,21 @@ function assertWriteCalls(calls: string[]): void {
expect(process.stdout.write).toHaveBeenNthCalledWith(i + 1, calls[i]) 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)
}
}

View File

@ -1,4 +1,5 @@
import * as os from 'os' import * as os from 'os'
import {toCommandValue} from './utils'
// For internal use, subject to change. // 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 { function escapeData(s: any): string {
return toCommandValue(s) return toCommandValue(s)
.replace(/%/g, '%25') .replace(/%/g, '%25')

View File

@ -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 os from 'os'
import * as path from 'path' import * as path from 'path'
@ -39,8 +41,16 @@ export enum ExitCode {
export function exportVariable(name: string, val: any): void { export function exportVariable(name: string, val: any): void {
const convertedVal = toCommandValue(val) const convertedVal = toCommandValue(val)
process.env[name] = convertedVal process.env[name] = convertedVal
const filePath = process.env['GITHUB_ENV'] || ''
if (filePath) {
const delimiter = '_GitHubActionsFileCommandDelimeter_'
const commandValue = `${name}<<${delimiter}${os.EOL}${convertedVal}${os.EOL}${delimiter}`
issueFileCommand('ENV', commandValue)
} else {
issueCommand('set-env', {name}, convertedVal) issueCommand('set-env', {name}, convertedVal)
} }
}
/** /**
* Registers a secret which will get masked from logs * Registers a secret which will get masked from logs
@ -55,7 +65,12 @@ export function setSecret(secret: string): void {
* @param inputPath * @param inputPath
*/ */
export function addPath(inputPath: string): void { export function addPath(inputPath: string): void {
const filePath = process.env['GITHUB_PATH'] || ''
if (filePath) {
issueFileCommand('PATH', inputPath)
} else {
issueCommand('add-path', {}, inputPath) issueCommand('add-path', {}, inputPath)
}
process.env['PATH'] = `${inputPath}${path.delimiter}${process.env['PATH']}` process.env['PATH'] = `${inputPath}${path.delimiter}${process.env['PATH']}`
} }

View File

@ -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'
})
}

View File

@ -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)
}