mirror of https://github.com/actions/toolkit
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 feedbackpull/582/head
parent
da34bfb74d
commit
0759cdc230
|
@ -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
|
||||||
|
```
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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,7 +41,15 @@ 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
|
||||||
issueCommand('set-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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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 {
|
||||||
issueCommand('add-path', {}, inputPath)
|
const filePath = process.env['GITHUB_PATH'] || ''
|
||||||
|
if (filePath) {
|
||||||
|
issueFileCommand('PATH', inputPath)
|
||||||
|
} else {
|
||||||
|
issueCommand('add-path', {}, inputPath)
|
||||||
|
}
|
||||||
process.env['PATH'] = `${inputPath}${path.delimiter}${process.env['PATH']}`
|
process.env['PATH'] = `${inputPath}${path.delimiter}${process.env['PATH']}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
|
})
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
Loading…
Reference in New Issue