diff --git a/coderrTask/package-lock.json b/coderrTask/package-lock.json new file mode 100644 index 0000000..075fc59 --- /dev/null +++ b/coderrTask/package-lock.json @@ -0,0 +1,137 @@ +{ + "name": "coderrtasks", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/crypto-js": { + "version": "3.1.43", + "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-3.1.43.tgz", + "integrity": "sha512-EHe/YKctU3IYNBsDmSOPX/7jLHPRlx8WaiDKSY9JCTnJ8XJeM4c0ZJvx+9Gxmr2s2ihI92R+3U/gNL1sq5oRuQ==", + "dev": true + }, + "@types/mocha": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", + "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==", + "dev": true + }, + "@types/node": { + "version": "12.6.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.6.6.tgz", + "integrity": "sha512-SMgj3x28MkJyHdWaMv/g/ca3LYDi5gR7O8mX0VKazvFOnmlDXctSEdd/8jfSqozjKFK1R9If1QZWkafX7yQTpA==", + "dev": true + }, + "@types/q": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz", + "integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==", + "dev": true + }, + "axios": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz", + "integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==", + "requires": { + "follow-redirects": "1.5.10", + "is-buffer": "^2.0.2" + } + }, + "azure-pipelines-task-lib": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/azure-pipelines-task-lib/-/azure-pipelines-task-lib-2.8.0.tgz", + "integrity": "sha512-PR8oap9z2j+o455W3PwAfB4SX1p4GdJc9OHQaQV0V+iQS1IBY6dVgcNSQMkHAXb0V1bbuLOFBLanXPe5eSgGTQ==", + "requires": { + "minimatch": "3.0.4", + "mockery": "^1.7.0", + "q": "^1.1.2", + "semver": "^5.1.0", + "shelljs": "^0.3.0", + "uuid": "^3.0.1" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "crypto-js": { + "version": "3.1.9-1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.1.9-1.tgz", + "integrity": "sha1-/aGedh/Ad+Af+/3G6f38WeiAbNg=" + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.0" + } + }, + "is-buffer": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", + "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "mockery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/mockery/-/mockery-1.7.0.tgz", + "integrity": "sha1-9O3g2HUMHJcnwnLqLGBiniyaHE8=" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" + }, + "shelljs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.3.0.tgz", + "integrity": "sha1-NZbmMHp4FUT1kfN9phg2DzHbV7E=" + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + } + } +} diff --git a/coderrTask/package.json b/coderrTask/package.json new file mode 100644 index 0000000..5b50b6f --- /dev/null +++ b/coderrTask/package.json @@ -0,0 +1,29 @@ +{ + "name": "coderrtasks", + "version": "1.0.0", + "description": "Release tasks for Coderr", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "exception-handling", + "c#", + "error-handling", + "exceptions", + "environment-tracking" + ], + "author": "1TCompany AB", + "license": "Apache-2.0", + "dependencies": { + "axios": "^0.19.0", + "azure-pipelines-task-lib": "^2.8.0", + "crypto-js": "^3.1.9-1" + }, + "devDependencies": { + "@types/crypto-js": "^3.1.43", + "@types/mocha": "^5.2.7", + "@types/node": "^12.6.6", + "@types/q": "^1.5.2" + } +} diff --git a/coderrTask/src/ApiClient.ts b/coderrTask/src/ApiClient.ts new file mode 100644 index 0000000..da5d347 --- /dev/null +++ b/coderrTask/src/ApiClient.ts @@ -0,0 +1,146 @@ +import Axios, * as http from "axios"; +import { AxiosRequestConfig, AxiosPromise, Method } from "axios"; +import * as crypto from "crypto"; +import { METHODS } from "http"; + +export interface IHttpResponse { + statusCode: number; + statusReason: string; + contentType: string | null; + body: any; + charset: string | null; +} + +export class HttpError extends Error { + message: string; + reponse: IHttpResponse; + + constructor(response: IHttpResponse) { + super(response.statusReason); + this.message = response.statusReason; + this.reponse = response; + } +} + +export class QueryString { + static parse(str: string): any { + str = str.trim().replace(/^(\?|#)/, ""); + if (!str) { + return null; + } + + const data = str + .trim() + .split("&") + .reduce((ret: any, param) => { + var parts = param.replace(/\+/g, " ").split("="); + var key = parts[0]; + var val: string | null = parts[1]; + + key = decodeURIComponent(key); + val = val === undefined ? null : decodeURIComponent(val); + if (!ret.hasOwnProperty(key)) { + ret[key] = val; + } else if (Array.isArray(ret[key])) { + ret[key].push(val); + } else { + ret[key] = [ret[key], val]; + } + + return ret; + }, {}); + + return data; + } + + static stringify(data: any): string { + return data + ? Object.keys(data) + .map(key => { + var val = data[key]; + + if (Array.isArray(val)) { + return val + .map( + val2 => + encodeURIComponent(key) + "=" + encodeURIComponent(val2) + ) + .join("&"); + } + + return encodeURIComponent(key) + "=" + encodeURIComponent(val); + }) + .join("&") + : ""; + } +} + +export class CoderrApiClient { + constructor( + private url: string, + private apiKey: string, + private sharedSecret: string + ) { + if (this.url[url.length - 1] !== "/") { + this.url += "/"; + } + this.url += "api/cqs/"; + } + + async command(message: any) { + await this.request("POST", message, null); + } + + async query(query: any): Promise { + return await this.request("GET", null, query); + } + + private createSignature(sharedSecret: string, payload: Buffer): string { + return crypto + .createHmac("sha256", payload) + .update("simple") + .digest() + .toString("base64"); + } + + private async request(method: Method, message: any, queryParameters: any): any { + var json = JSON.stringify(message); + var payload = Buffer.from(json, "utf8"); + var signature = this.createSignature(this.sharedSecret, payload); + + var config: AxiosRequestConfig = { + url: this.url, + method: method, + data: payload, + headers: { + Accept: "application/json", + Authorization: `ApiKey ${this.apiKey} ${signature}`, + "X-Api-Signature": signature, + "X-Api-Key": this.apiKey, + "X-Cqs-Name": message.constructor.TYPE_NAME + } + }; + if (queryParameters != null) { + config.url += "?" + QueryString.stringify(queryParameters); + } + + var result = await http.default.request(config); + + // no data + if (result.status == 204) { + return null; + } + + if (result.status >= 400) { + throw new HttpError({ + body: result.data, + charset: result.headers.charset, + contentType: result.request.contentType, + statusCode: result.status, + statusReason: result.statusText + }); + } + + return result.data; + } +} diff --git a/coderrTask/src/index.ts b/coderrTask/src/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/coderrTask/task.json b/coderrTask/task.json new file mode 100644 index 0000000..4734587 --- /dev/null +++ b/coderrTask/task.json @@ -0,0 +1,46 @@ +{ + "id": "b8830377-0d8a-4499-8299-778243b5a850", + "name": "coderr-tasks", + "friendlyName": "Coderr Tasks", + "description": "Tasks used to modify an application in Coderr", + "helpMarkDown": "", + "category": "Utility", + "author": "1TCompany AB", + "version": { + "Major": 0, + "Minor": 1, + "Patch": 0 + }, + "instanceNameFormat": "Echo $(samplestring)", + "inputs": [ + { + "name": "apikey", + "type": "string", + "label": "API Key", + "defaultValue": "", + "required": true, + "helpMarkDown": "API Key from system settings in Coderr" + }, + { + "name": "sharedSecret", + "type": "string", + "label": "Shared secret", + "defaultValue": "", + "required": true, + "helpMarkDown": "Shared secret from system settings in Coderr" + }, + { + "name": "coderrUrl", + "type": "string", + "label": "URL", + "defaultValue": "http://app.coderr.io", + "required": true, + "helpMarkDown": "URL to your Coderr server" + } + ], + "execution": { + "Node": { + "target": "index.js" + } + } +} \ No newline at end of file diff --git a/coderrTask/tests/_suite.ts b/coderrTask/tests/_suite.ts new file mode 100644 index 0000000..a2c8c8f --- /dev/null +++ b/coderrTask/tests/_suite.ts @@ -0,0 +1,56 @@ +import * as path from 'path'; +import * as assert from 'assert'; +import * as ttm from 'azure-pipelines-task-lib/mock-test'; + +describe('Sample task tests', function () { + + before( function() { + + }); + + after(() => { + + }); + + it('should succeed with simple inputs', function(done: MochaDone) { + // Add success test here + }); + + it('it should fail if tool returns 1', function(done: MochaDone) { + // Add failure test here + }); + + it('should succeed with simple inputs', function(done: MochaDone) { + this.timeout(1000); + + let tp = path.join(__dirname, 'success.js'); + let tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp); + + tr.run(); + console.log(tr.succeeded); + assert.equal(tr.succeeded, true, 'should have succeeded'); + assert.equal(tr.warningIssues.length, 0, "should have no warnings"); + assert.equal(tr.errorIssues.length, 0, "should have no errors"); + console.log(tr.stdout); + assert.equal(tr.stdout.indexOf('Hello human') >= 0, true, "should display Hello human"); + done(); + }); + + it('it should fail if tool returns 1', function(done: MochaDone) { + this.timeout(1000); + + let tp = path.join(__dirname, 'failure.js'); + let tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp); + + tr.run(); + console.log(tr.succeeded); + assert.equal(tr.succeeded, false, 'should have failed'); + assert.equal(tr.warningIssues, 0, "should have no warnings"); + assert.equal(tr.errorIssues.length, 1, "should have 1 error issue"); + assert.equal(tr.errorIssues[0], 'Bad input was given', 'error issue output'); + assert.equal(tr.stdout.indexOf('Hello bad'), -1, "Should not display Hello bad"); + + done(); + }); +}); + diff --git a/coderrTask/tests/failure.ts b/coderrTask/tests/failure.ts new file mode 100644 index 0000000..a0a4c53 --- /dev/null +++ b/coderrTask/tests/failure.ts @@ -0,0 +1,10 @@ +import ma = require('azure-pipelines-task-lib/mock-answer'); +import tmrm = require('azure-pipelines-task-lib/mock-run'); +import path = require('path'); + +let taskPath = path.join(__dirname, '..', 'index.js'); +let tmr: tmrm.TaskMockRunner = new tmrm.TaskMockRunner(taskPath); + +tmr.setInput('samplestring', 'bad'); + +tmr.run(); \ No newline at end of file diff --git a/coderrTask/tests/success.ts b/coderrTask/tests/success.ts new file mode 100644 index 0000000..6d81d02 --- /dev/null +++ b/coderrTask/tests/success.ts @@ -0,0 +1,10 @@ +import ma = require('azure-pipelines-task-lib/mock-answer'); +import tmrm = require('azure-pipelines-task-lib/mock-run'); +import path = require('path'); + +let taskPath = path.join(__dirname, '..', 'index.js'); +let tmr: tmrm.TaskMockRunner = new tmrm.TaskMockRunner(taskPath); + +tmr.setInput('samplestring', 'human'); + +tmr.run(); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3631133 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,63 @@ +{ + "compilerOptions": { + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + } +} diff --git a/vss-extension.json b/vss-extension.json new file mode 100644 index 0000000..d1c02b4 --- /dev/null +++ b/vss-extension.json @@ -0,0 +1,38 @@ +{ + "manifestVersion": 1, + "id": "coderr.azuredevops.tasks", + "name": "Build and Release tasks for Coderr Server", + "version": "0.0.1", + "publisher": "1TCompany AB", + "targets": [ + { + "id": "Microsoft.VisualStudio.Services" + } + ], + "description": "Tasks to clear and check errors for specific environments in Coderr Server.", + "categories": [ + "Azure Pipelines", + "Error Handling", + "Exception Handling" + ], + "icons": { + "default": "images/extension-icon.png" + }, + "files": [ + { + "path": "buildAndReleaseTask" + } + ], + "contributions": [ + { + "id": "custom-build-release-task", + "type": "ms.vss-distributed-task.task", + "targets": [ + "ms.vss-distributed-task.tasks" + ], + "properties": { + "name": "buildAndReleaseTask" + } + } + ] +} \ No newline at end of file