1
0
Fork 0

Merge pull request #4 from actions/features/core

IN PROGRESS: Features/core
pull/11/head
Jonathan Clem 2019-05-21 15:29:18 -04:00 committed by GitHub
commit 026ad6f559
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 440 additions and 17 deletions

3
.eslintignore Normal file
View File

@ -0,0 +1,3 @@
node_modules/
packages/*/node_modules/
packages/*/lib/

View File

@ -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
.prettierignore Normal file
View File

@ -0,0 +1,3 @@
node_modules/
packages/*/node_modules/
packages/*/lib/

View File

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

View File

@ -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"
},

7
packages/core/README.md Normal file
View File

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

View File

@ -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
packages/core/package-lock.json generated Normal file
View File

@ -0,0 +1,14 @@
{
"name": "@actions/core",
"version": "0.1.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@types/node": {
"version": "12.0.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.2.tgz",
"integrity": "sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA==",
"dev": true
}
}
}

View File

@ -0,0 +1,40 @@
{
"name": "@actions/core",
"version": "0.1.0",
"description": "Actions core lib",
"keywords": [
"core",
"actions"
],
"author": "Bryan MacFarlane <bryanmac@microsoft.com>",
"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"
}
}

View File

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

100
packages/core/src/core.ts Normal file
View File

@ -0,0 +1,100 @@
import {ExitCode} from '@actions/exit'
import {issue, issueCommand} from './command'
/**
* Interface for getInput options
*/
export interface InputOptions {
/** Optional. Whether the input is required. If required and not present, will throw. Defaults to false */
required?: boolean
}
//-----------------------------------------------------------------------
// Variables
//-----------------------------------------------------------------------
/**
* sets env variable for this action and future actions in the job
* @param name the name of the variable to set
* @param val the value of the variable
*/
export function exportVariable(name: string, val: string) {
process.env[name] = val
issueCommand('set-env', {name}, val)
}
/**
* exports the variable and registers a secret which will get masked from logs
* @param name the name of the variable to set
* @param val value of the secret
*/
export function exportSecret(name: string, val: string) {
exportVariable(name, val)
issueCommand('set-secret', {}, val)
}
/**
* Gets the value of an input. The value is also trimmed.
*
* @param name name of the input to get
* @param options optional. See InputOptions.
* @returns string
*/
export function getInput(name: string, options?: InputOptions): string {
const val: string =
process.env[`INPUT_${name.replace(' ', '_').toUpperCase()}`] || ''
if (options && options.required && !val) {
throw new Error(`Input required and not supplied: ${name}`)
}
return val.trim()
}
//-----------------------------------------------------------------------
// Results
//-----------------------------------------------------------------------
/**
* Sets the action status to neutral
*/
export function setNeutral() {
process.exitCode = ExitCode.Neutral
}
/**
* Sets the action status to failed.
* When the action exits it will be with an exit code of 1
* @param message add error issue message
*/
export function setFailed(message: string) {
process.exitCode = ExitCode.Failure
error(message)
}
//-----------------------------------------------------------------------
// Logging Commands
//-----------------------------------------------------------------------
/**
* Writes debug message to user log
* @param message debug message
*/
export function debug(message: string) {
issueCommand('debug', {}, message)
}
/**
* Adds an error issue
* @param message error issue message
*/
export function error(message: string) {
issue('error', message)
}
/**
* Adds an warning issue
* @param message warning issue message
*/
export function warning(message: string) {
issue('warning', message)
}

View File

@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": "./",
"outDir": "./lib",
"rootDir": "./src"
},
"include": [
"./src"
]
}

View File

@ -4,8 +4,4 @@
## Usage
```
const exit = require('@actions/exit');
// TODO: DEMONSTRATE API
```
See [src/exit.ts](src/exit.ts).

View File

@ -4,8 +4,4 @@
## Usage
```
const github = require('@actions/toolkit');
// TODO: DEMONSTRATE API
```
See [src/toolkit.ts](src/toolkit.ts).