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
|
||||
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`:
|
||||
|
@ -156,7 +125,72 @@ function setCommandEcho(enabled: boolean): void {}
|
|||
The `add-mask`, `debug`, `warning` and `error` commands do not support echoing.
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
|
||||
# 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 path from 'path'
|
||||
import * as core from '../src/core'
|
||||
|
@ -20,27 +21,34 @@ const testEnvVars = {
|
|||
INPUT_MULTIPLE_SPACES_VARIABLE: 'I have multiple spaces',
|
||||
|
||||
// Save inputs
|
||||
STATE_TEST_1: 'state_val'
|
||||
STATE_TEST_1: 'state_val',
|
||||
|
||||
// File Commands
|
||||
GITHUB_PATH: '',
|
||||
GITHUB_ENV: ''
|
||||
}
|
||||
|
||||
describe('@actions/core', () => {
|
||||
beforeEach(() => {
|
||||
for (const key in testEnvVars)
|
||||
process.env[key] = testEnvVars[key as keyof typeof testEnvVars]
|
||||
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]
|
||||
}
|
||||
process.stdout.write = jest.fn()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
for (const key in testEnvVars) Reflect.deleteProperty(testEnvVars, key)
|
||||
})
|
||||
|
||||
it('exportVariable produces the correct command and sets the env', () => {
|
||||
it('legacy exportVariable produces the correct command and sets the env', () => {
|
||||
core.exportVariable('my var', 'var val')
|
||||
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')
|
||||
expect(process.env['special char var \r\n,:']).toBe('special val')
|
||||
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')
|
||||
expect(process.env['my var2']).toBe('var val\r\n')
|
||||
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)
|
||||
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)
|
||||
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', () => {
|
||||
core.setSecret('secret val')
|
||||
assertWriteCalls([`::add-mask::secret val${os.EOL}`])
|
||||
})
|
||||
|
||||
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')
|
||||
expect(process.env['PATH']).toBe(
|
||||
`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])
|
||||
}
|
||||
}
|
||||
|
||||
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 {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')
|
||||
|
|
|
@ -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,8 +41,16 @@ export enum ExitCode {
|
|||
export function exportVariable(name: string, val: any): void {
|
||||
const convertedVal = toCommandValue(val)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a secret which will get masked from logs
|
||||
|
@ -55,7 +65,12 @@ export function setSecret(secret: string): void {
|
|||
* @param inputPath
|
||||
*/
|
||||
export function addPath(inputPath: string): void {
|
||||
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']}`
|
||||
}
|
||||
|
||||
|
|
|
@ -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