From 5f66339fde9cd037162ae60032e7782a0b90cc77 Mon Sep 17 00:00:00 2001 From: Danny McCormick Date: Thu, 16 May 2019 16:40:21 -0400 Subject: [PATCH] Add Bryan's core code --- packages/core/README.md | 11 ++ packages/core/__tests__/lib.test.ts | 29 ++++++ packages/core/package.json | 34 +++++++ packages/core/src/interfaces.ts | 31 ++++++ packages/core/src/internal.ts | 149 ++++++++++++++++++++++++++++ packages/core/src/lib.ts | 56 +++++++++++ packages/core/tsconfig.json | 11 ++ 7 files changed, 321 insertions(+) create mode 100644 packages/core/README.md create mode 100644 packages/core/__tests__/lib.test.ts create mode 100644 packages/core/package.json create mode 100644 packages/core/src/interfaces.ts create mode 100644 packages/core/src/internal.ts create mode 100644 packages/core/src/lib.ts create mode 100644 packages/core/tsconfig.json diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 00000000..9193971e --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,11 @@ +# `@actions/core` + +> TODO: description + +## Usage + +``` +const core = require('@actions/core'); + +// TODO: DEMONSTRATE API +``` diff --git a/packages/core/__tests__/lib.test.ts b/packages/core/__tests__/lib.test.ts new file mode 100644 index 00000000..3917550c --- /dev/null +++ b/packages/core/__tests__/lib.test.ts @@ -0,0 +1,29 @@ +'use strict'; + +import * as core from '../src/lib' + +describe('@actions/core', () => { + it('needs tests', () => { + + }); +}); + + +// it('exits successfully', () => { +// jest.spyOn(process, 'exit').mockImplementation() +// core.fail('testing fail'); +// exit.success() +// expect(process.exit).toHaveBeenCalledWith(0) +// }) + +// it('exits as a failure', () => { +// jest.spyOn(process, 'exit').mockImplementation() +// exit.failure() +// expect(process.exit).toHaveBeenCalledWith(1) +// }) + +// it('exits neutrally', () => { +// jest.spyOn(process, 'exit').mockImplementation() +// exit.neutral() +// expect(process.exit).toHaveBeenCalledWith(78) +// }) diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 00000000..1a922ff2 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,34 @@ +{ + "name": "@actions/core", + "version": "1.0.0", + "description": "Actions core lib", + "keywords": [ + "core", + "actions" + ], + "author": "Bryan MacFarlane ", + "homepage": "https://github.com/actions/toolkit/tree/master/packages/io", + "license": "MIT", + "main": "lib/lib.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" + } +} diff --git a/packages/core/src/interfaces.ts b/packages/core/src/interfaces.ts new file mode 100644 index 00000000..164ca103 --- /dev/null +++ b/packages/core/src/interfaces.ts @@ -0,0 +1,31 @@ +export interface FileDetails { + /** + * Full path to the file causing the issue. + * Note: the agent will translate to the proper repo when posting + * the issue back to the timeline + */ + File: string, + Line: number, + Column: number +} + +/** + * The code to exit an action + * Spec: https://github.com/github/dreamlifter/blob/master/docs/actions-model.md#exit-codes + */ +export enum ExitCode { + /** + * A code indicating that the action was successful + */ + Success = 0, + + /** + * A code indicating that the action was a failure + */ + Failure = 1, + + /** + * A code indicating that the action is complete, but neither succeeded nor failed + */ + Neutral = 78 +} \ No newline at end of file diff --git a/packages/core/src/internal.ts b/packages/core/src/internal.ts new file mode 100644 index 00000000..1d2e867c --- /dev/null +++ b/packages/core/src/internal.ts @@ -0,0 +1,149 @@ +import os = require('os'); + + +/** + * Commands + * Spec: https://github.com/github/dreamlifter/blob/master/docs/actions-model.md#logging-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: {[key: string]: string}, message: string) { + var cmd = new _Command(command, properties, message); + _writeLine(cmd.toString()); +} + +export function _issue(name: string, message: string) { + _issueCommand(name, {}, message); +} + +let CMD_PREFIX = '##['; + +export class _Command { + constructor(command: string, properties: any, message: string) { + if (!command) { + command = 'missing.command'; + } + + this.command = command; + this.properties = properties; + this.message = message; + } + + public command: string; + public message: string; + public properties: any; + + public toString() { + var cmdStr = CMD_PREFIX + this.command; + + if (this.properties && Object.keys(this.properties).length > 0) { + cmdStr += ' '; + for (var key in this.properties) { + if (this.properties.hasOwnProperty(key)) { + var 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 + let message: string = '' + (this.message || ''); + cmdStr += escapedata(message); + + return cmdStr; + } +} + +export function _commandFromString(commandLine: string) { + var preLen = CMD_PREFIX.length; + var lbPos = commandLine.indexOf('['); + var rbPos = commandLine.indexOf(']'); + if (lbPos == -1 || rbPos == -1 || rbPos - lbPos < 3) { + throw new Error('Invalid command brackets'); + } + var cmdInfo = commandLine.substring(lbPos + 1, rbPos); + var spaceIdx = cmdInfo.indexOf(' '); + + var command = cmdInfo; + var properties: {[key: string]: string} = {}; + + if (spaceIdx > 0) { + command = cmdInfo.trim().substring(0, spaceIdx); + var propSection = cmdInfo.trim().substring(spaceIdx+1); + + var propLines: string[] = propSection.split(';'); + propLines.forEach(function (propLine: string) { + propLine = propLine.trim(); + if (propLine.length > 0) { + var eqIndex = propLine.indexOf('='); + if (eqIndex == -1){ + throw new Error('Invalid property: ' + propLine); + } + + var key: string = propLine.substring(0, eqIndex); + var val: string = propLine.substring(eqIndex+1); + + properties[key] = unescape(val); + } + }); + } + + let msg: string = unescapedata(commandLine.substring(rbPos + 1)); + var cmd = new _Command(command, properties, msg); + return cmd; +} + +function escapedata(s: string) : string { + return s.replace(/\r/g, '%0D') + .replace(/\n/g, '%0A'); +} + +function unescapedata(s: string) : string { + return s.replace(/%0D/g, '\r') + .replace(/%0A/g, '\n'); +} + +function escape(s: string) : string { + return s.replace(/\r/g, '%0D') + .replace(/\n/g, '%0A') + .replace(/]/g, '%5D') + .replace(/;/g, '%3B'); +} + +function unescape(s: string) : string { + return s.replace(/%0D/g, '\r') + .replace(/%0A/g, '\n') + .replace(/%5D/g, ']') + .replace(/%3B/g, ';'); +} + +//----------------------------------------------------- +// Streams: allow to override the stream +//----------------------------------------------------- + +let _outStream = process.stdout; +let _errStream = process.stderr; + +export function _writeLine(str: string): void { + _outStream.write(str + os.EOL); +} + +export function _setStdStream(stdStream: NodeJS.WriteStream): void { + _outStream = stdStream; +} + +export function _setErrStream(errStream: NodeJS.WriteStream): void { + _errStream = errStream; +} \ No newline at end of file diff --git a/packages/core/src/lib.ts b/packages/core/src/lib.ts new file mode 100644 index 00000000..6b662560 --- /dev/null +++ b/packages/core/src/lib.ts @@ -0,0 +1,56 @@ +import im = require('./interfaces'); +import intm = require('./internal'); +import process = require('process'); + +/** + * 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 setVariable(name: string, val: string) { + process.env[name] = val; + intm._issueCommand('set-variable', {'name': name}, val); +} + +/** + * sets a variable which will get masked from logs + * @param name name of the secret variable + * @param val value of the secret variable + */ +export function setSecret(name: string, val: string) { + intm._issueCommand('set-secret', {'name': name}, val); + setVariable(name, val); +} + +//----------------------------------------------------------------------- +// Results +//----------------------------------------------------------------------- +/** + * fail the action + * @param message + */ +export function fail(message: string) { + process.exitCode = im.ExitCode.Failure; + error(message); +} + +//----------------------------------------------------------------------- +// Logging Commands +// https://github.com/github/dreamlifter/blob/master/docs/actions-model.md#logging-commands +// +// error and warning issues do not take FileDetails because while possible, +// that's typically reserved for the agent and the problem matchers. +// +//----------------------------------------------------------------------- + +export function addPath(path: string) { + intm._issueCommand('add-path', {}, path); +} + +export function error(message: string) { + intm._issue('error', message); +} + +export function warning(message: string) { + intm._issue('warning', message); +} \ No newline at end of file diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 00000000..a8b812a6 --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./lib", + "rootDir": "./src" + }, + "include": [ + "./src" + ] +} \ No newline at end of file