Skip to content

Commit

Permalink
Add File Commands
Browse files Browse the repository at this point in the history
  • Loading branch information
thboop committed Sep 11, 2020
1 parent c9819f7 commit 8d1441e
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 66 deletions.
115 changes: 83 additions & 32 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:
Expand Down Expand Up @@ -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<<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:
```
{
"slideshow": {
"author": "Yours Truly",
"date": "date of publication",
"slides": [
{
"title": "Wake up to WonderWidgets!",
"type": "all"
},
{
"items": [
"Why <em>WonderWidgets</em> are great",
"Who <em>buys</em> 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
```
65 changes: 47 additions & 18 deletions packages/core/__tests__/core.test.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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]
Expand All @@ -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', () => {
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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)
}
}
14 changes: 1 addition & 13 deletions packages/core/src/command.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as os from 'os'
import {toCommandValue} from './utils'

// For internal use, subject to change.

Expand Down Expand Up @@ -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')
Expand Down
10 changes: 7 additions & 3 deletions packages/core/src/core.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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)
}

/**
Expand All @@ -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']}`
}

Expand Down
24 changes: 24 additions & 0 deletions packages/core/src/file-command.ts
Original file line number Diff line number Diff line change
@@ -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'
})
}
15 changes: 15 additions & 0 deletions packages/core/src/utils.ts
Original file line number Diff line number Diff line change
@@ -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)
}

0 comments on commit 8d1441e

Please sign in to comment.