Skip to content

Commit

Permalink
Merge pull request actions#4 from actions/features/core
Browse files Browse the repository at this point in the history
IN PROGRESS: Features/core
  • Loading branch information
jclem authored May 21, 2019
2 parents c27c952 + b057387 commit 026ad6f
Show file tree
Hide file tree
Showing 14 changed files with 440 additions and 17 deletions.
3 changes: 3 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
packages/*/node_modules/
packages/*/lib/
5 changes: 3 additions & 2 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@
"no-unused-vars": "off",
"eslint-comments/no-use": "off",
"import/no-namespace": "off",
"@typescript-eslint/no-unused-vars": "error"
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}]
},
"env": {
"node": true,
"es6": true,
"jest/globals": true
}
}
}
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
packages/*/node_modules/
packages/*/lib/
10 changes: 8 additions & 2 deletions docs/package-specs.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,16 @@ export interface InputOptions {
export function getInput(name: string, options?: InputOptions): string | undefined
/**
* fail the action
* sets the status of the action to neutral
* @param message
*/
export function setFailure(message: string): void
export function setNeutral(message: string): void
/**
* sets the status of the action to failed
* @param message
*/
export function setFailed(message: string): void
```

### IO spec
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
"bootstrap": "lerna bootstrap",
"build": "lerna run tsc",
"check-all": "concurrently \"npm:format-check\" \"npm:lint\" \"npm:test\" \"npm:build -- -- --noEmit\"",
"format": "prettier --write packages/*/src/**/*.ts",
"format-check": "prettier --check packages/*/src/**/*.ts",
"lint": "eslint packages/*/src/**/*.ts",
"format": "prettier --write packages/**/*.ts",
"format-check": "prettier --check packages/**/*.ts",
"lint": "eslint packages/**/*.ts",
"new-package": "scripts/create-package",
"test": "jest"
},
Expand Down
7 changes: 7 additions & 0 deletions packages/core/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# `@actions/core`

> Core functions for setting results, logging, registering secrets and exporting variables across actions
## Usage

See [src/core.ts](src/core.ts).
159 changes: 159 additions & 0 deletions packages/core/__tests__/lib.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import * as os from 'os'
import * as core from '../src/core'

const testEnvVars = {
'my var': '',
'special char var \r\n];': '',
'my var2': '',
'my secret': '',
'special char secret \r\n];': '',
'my secret2': '',

// Set inputs
INPUT_MY_INPUT: 'val',
INPUT_MISSING: '',
'INPUT_SPECIAL_CHARS_\'\t"\\': '\'\t"\\ repsonse '
}

describe('@actions/core', () => {
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', () => {
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%5D%3B;]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}`])
})

it('exportSecret produces the correct commands and sets the env', () => {
core.exportSecret('my secret', 'secret val')
expect(process.env['my secret']).toBe('secret val')
assertWriteCalls([
`##[set-env name=my secret;]secret val${os.EOL}`,
`##[set-secret]secret val${os.EOL}`
])
})

it('exportSecret escapes secret names', () => {
core.exportSecret('special char secret \r\n];', 'special secret val')
expect(process.env['special char secret \r\n];']).toBe('special secret val')
assertWriteCalls([
`##[set-env name=special char secret %0D%0A%5D%3B;]special secret val${
os.EOL
}`,
`##[set-secret]special secret val${os.EOL}`
])
})

it('exportSecret escapes secret values', () => {
core.exportSecret('my secret2', 'secret val\r\n')
expect(process.env['my secret2']).toBe('secret val\r\n')
assertWriteCalls([
`##[set-env name=my secret2;]secret val%0D%0A${os.EOL}`,
`##[set-secret]secret val%0D%0A${os.EOL}`
])
})

it('getInput gets non-required input', () => {
expect(core.getInput('my input')).toBe('val')
})

it('getInput gets required input', () => {
expect(core.getInput('my input', {required: true})).toBe('val')
})

it('getInput throws on missing required input', () => {
expect(() => core.getInput('missing', {required: true})).toThrow(
'Input required and not supplied: missing'
)
})

it('getInput doesnt throw on missing non-required input', () => {
expect(core.getInput('missing', {required: false})).toBe('')
})

it('getInput is case insensitive', () => {
expect(core.getInput('My InPuT')).toBe('val')
})

it('getInput handles special characters', () => {
expect(core.getInput('special chars_\'\t"\\')).toBe('\'\t"\\ repsonse')
})

it('setNeutral sets the correct exit code', () => {
core.setFailed('Failure message')
expect(process.exitCode).toBe(1)
})

it('setFailure sets the correct exit code and failure message', () => {
core.setFailed('Failure message')
expect(process.exitCode).toBe(1)
assertWriteCalls([`##[error]Failure message${os.EOL}`])
})

it('setFailure escapes the failure message', () => {
core.setFailed('Failure \r\n\nmessage\r')
expect(process.exitCode).toBe(1)
assertWriteCalls([`##[error]Failure %0D%0A%0Amessage%0D${os.EOL}`])
})

it('error sets the correct error message', () => {
core.error('Error message')
assertWriteCalls([`##[error]Error message${os.EOL}`])
})

it('error escapes the error message', () => {
core.error('Error message\r\n\n')
assertWriteCalls([`##[error]Error message%0D%0A%0A${os.EOL}`])
})

it('warning sets the correct message', () => {
core.warning('Warning')
assertWriteCalls([`##[warning]Warning${os.EOL}`])
})

it('warning escapes the message', () => {
core.warning('\r\nwarning\n')
assertWriteCalls([`##[warning]%0D%0Awarning%0A${os.EOL}`])
})

it('debug sets the correct message', () => {
core.debug('Debug')
assertWriteCalls([`##[debug]Debug${os.EOL}`])
})

it('debug escapes the message', () => {
core.debug('\r\ndebug\n')
assertWriteCalls([`##[debug]%0D%0Adebug%0A${os.EOL}`])
})
})

// Assert that process.stdout.write calls called only with the given arguments.
function assertWriteCalls(calls: string[]) {
expect(process.stdout.write).toHaveBeenCalledTimes(calls.length)

for (let i = 0; i < calls.length; i++) {
expect(process.stdout.write).toHaveBeenNthCalledWith(i + 1, calls[i])
}
}
14 changes: 14 additions & 0 deletions packages/core/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 40 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "@actions/core",
"version": "0.1.0",
"description": "Actions core lib",
"keywords": [
"core",
"actions"
],
"author": "Bryan MacFarlane <[email protected]>",
"homepage": "https://github.com/actions/toolkit/tree/master/packages/core",
"license": "MIT",
"main": "lib/core.js",
"directories": {
"lib": "lib",
"test": "__tests__"
},
"files": [
"lib"
],
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/actions/toolkit.git"
},
"scripts": {
"test": "echo \"Error: run tests from root\" && exit 1",
"tsc": "tsc"
},
"bugs": {
"url": "https://github.com/actions/toolkit/issues"
},
"devDependencies": {
"@types/node": "^12.0.2"
},
"dependencies": {
"@actions/exit": "^0.0.0"
}
}
87 changes: 87 additions & 0 deletions packages/core/src/command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import * as os from 'os'

// For internal use, subject to change.

/**
* Commands
*
* Command Format:
* ##[name key=value;key=value]message
*
* Examples:
* ##[warning]This is the user warning message
* ##[set-secret name=mypassword]definatelyNotAPassword!
*/
export function issueCommand(
command: string,
properties: any,
message: string
) {
const cmd = new Command(command, properties, message)
process.stdout.write(cmd.toString() + os.EOL)
}

export function issue(name: string, message: string) {
issueCommand(name, {}, message)
}

const CMD_PREFIX = '##['

class Command {
constructor(
command: string,
properties: {[key: string]: string},
message: string
) {
if (!command) {
command = 'missing.command'
}

this.command = command
this.properties = properties
this.message = message
}

command: string
message: string
properties: {[key: string]: string}

toString() {
let cmdStr = CMD_PREFIX + this.command

if (this.properties && Object.keys(this.properties).length > 0) {
cmdStr += ' '
for (const key in this.properties) {
if (this.properties.hasOwnProperty(key)) {
const val = this.properties[key]
if (val) {
// safely append the val - avoid blowing up when attempting to
// call .replace() if message is not a string for some reason
cmdStr += `${key}=${escape(`${val || ''}`)};`
}
}
}
}

cmdStr += ']'

// safely append the message - avoid blowing up when attempting to
// call .replace() if message is not a string for some reason
const message: string = `${this.message || ''}`
cmdStr += escapeData(message)

return cmdStr
}
}

function escapeData(s: string): string {
return s.replace(/\r/g, '%0D').replace(/\n/g, '%0A')
}

function escape(s: string): string {
return s
.replace(/\r/g, '%0D')
.replace(/\n/g, '%0A')
.replace(/]/g, '%5D')
.replace(/;/g, '%3B')
}
Loading

0 comments on commit 026ad6f

Please sign in to comment.