From 407223b0f2373e53f8cf8612051cf55f82b31b16 Mon Sep 17 00:00:00 2001 From: Joseph Lust Date: Wed, 12 May 2021 09:32:22 -0400 Subject: [PATCH] v1.10 release --- lib/src/constants.js | 28 +++++++++ lib/src/entities/Environment.js | 2 + lib/src/index.js | 104 +++++++++++++++++++++----------- lib/src/mablApiClient.js | 46 +++++++++++--- lib/test/suite.test.js | 81 +++++++++++++++++++++++-- 5 files changed, 210 insertions(+), 51 deletions(-) create mode 100644 lib/src/constants.js create mode 100644 lib/src/entities/Environment.js diff --git a/lib/src/constants.js b/lib/src/constants.js new file mode 100644 index 00000000..5baf2673 --- /dev/null +++ b/lib/src/constants.js @@ -0,0 +1,28 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ActionOutputs = exports.ActionInputs = exports.USER_AGENT = void 0; +exports.USER_AGENT = 'mabl-github-run-tests-action'; +var ActionInputs; +(function (ActionInputs) { + ActionInputs["ApplicationId"] = "application-id"; + ActionInputs["BrowserTypes"] = "browser-types"; + ActionInputs["ContinueOnFailure"] = "continue-on-failure"; + ActionInputs["EnvironmentId"] = "environment-id"; + ActionInputs["EventTime"] = "event-time"; + ActionInputs["HttpHeaders"] = "http-headers"; + ActionInputs["MablBranch"] = "mabl-branch"; + ActionInputs["PlanLabels"] = "plan-labels"; + ActionInputs["RebaselineImages"] = "rebaseline-images"; + ActionInputs["SetStaticBaseline"] = "set-static-baseline"; + ActionInputs["Uri"] = "uri"; +})(ActionInputs = exports.ActionInputs || (exports.ActionInputs = {})); +var ActionOutputs; +(function (ActionOutputs) { + ActionOutputs["DeploymentId"] = "mabl-deployment-id"; + ActionOutputs["PlansRun"] = "plans_run"; + ActionOutputs["PlansPassed"] = "plans_passed"; + ActionOutputs["PlansFailed"] = "plans_failed"; + ActionOutputs["TestsRun"] = "tests_run"; + ActionOutputs["TestsPassed"] = "tests_passed"; + ActionOutputs["TestsFailed"] = "tests_failed"; +})(ActionOutputs = exports.ActionOutputs || (exports.ActionOutputs = {})); diff --git a/lib/src/entities/Environment.js b/lib/src/entities/Environment.js new file mode 100644 index 00000000..c8ad2e54 --- /dev/null +++ b/lib/src/entities/Environment.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/lib/src/index.js b/lib/src/index.js index c85d7cc8..644f777f 100644 --- a/lib/src/index.js +++ b/lib/src/index.js @@ -31,11 +31,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); +exports.run = exports.booleanInput = exports.optionalInput = exports.optionalArrayInput = void 0; const axios_1 = __importDefault(require("axios")); const mablApiClient_1 = require("./mablApiClient"); const table_1 = require("./table"); const core = __importStar(require("@actions/core")); const github = __importStar(require("@actions/github")); +const constants_1 = require("./constants"); const DEFAULT_MABL_APP_URL = 'https://app.mabl.com'; const EXECUTION_POLL_INTERVAL_MILLIS = 10000; const EXECUTION_COMPLETED_STATUSES = [ @@ -46,34 +48,56 @@ const EXECUTION_COMPLETED_STATUSES = [ 'terminated', ]; const GITHUB_BASE_URL = 'https://api.github.com'; +function optionalArrayInput(name) { + return core + .getInput(name, { + required: false, + }) + .split(/[,\n]/) + .filter((item) => item.length) + .map((item) => item.trim()); +} +exports.optionalArrayInput = optionalArrayInput; +function optionalInput(name) { + const rawValue = core.getInput(name, { + required: false, + }); + if (rawValue.length > 0) { + return rawValue; + } + return; +} +exports.optionalInput = optionalInput; +function booleanInput(name) { + return (core + .getInput(name, { + required: false, + }) + .toLowerCase() === 'true'); +} +exports.booleanInput = booleanInput; function run() { var _a, _b, _c; return __awaiter(this, void 0, void 0, function* () { try { core.startGroup('Gathering inputs'); - const applicationId = core.getInput('application-id', { - required: false, - }); - const environmentId = core.getInput('environment-id', { - required: false, - }); - const apiKey = process.env.MABL_API_KEY || ''; + const applicationId = optionalInput(constants_1.ActionInputs.ApplicationId); + const environmentId = optionalInput(constants_1.ActionInputs.EnvironmentId); + const apiKey = process.env.MABL_API_KEY; if (!apiKey) { - core.setFailed('MABL_API_KEY required'); + core.setFailed('env var MABL_API_KEY required'); + return; } - const browserTypes = core.getInput('browser-types', { - required: false, - }); - const uri = core.getInput('uri', { required: false }); - const rebaselineImages = parseBoolean(core.getInput('rebaseline-images', { - required: false, - })); - const setStaticBaseline = parseBoolean(core.getInput('set-static-baseline', { - required: false, - })); - const continueOnPlanFailure = parseBoolean(core.getInput('continue-on-failure', { required: false })); + const planLabels = optionalArrayInput(constants_1.ActionInputs.PlanLabels); + const browserTypes = optionalArrayInput(constants_1.ActionInputs.BrowserTypes); + const httpHeaders = optionalArrayInput(constants_1.ActionInputs.HttpHeaders); + const uri = optionalInput(constants_1.ActionInputs.Uri); + const mablBranch = optionalInput(constants_1.ActionInputs.MablBranch); + const rebaselineImages = booleanInput(constants_1.ActionInputs.RebaselineImages); + const setStaticBaseline = booleanInput(constants_1.ActionInputs.SetStaticBaseline); + const continueOnPlanFailure = booleanInput(constants_1.ActionInputs.ContinueOnFailure); const pullRequest = yield getRelatedPullRequest(); - const eventTimeString = core.getInput('event-time', { required: false }); + const eventTimeString = optionalInput(constants_1.ActionInputs.EventTime); const eventTime = eventTimeString ? parseInt(eventTimeString) : Date.now(); let properties = { triggering_event_name: process.env.GITHUB_EVENT_NAME, @@ -98,18 +122,28 @@ function run() { const revision = process.env.GITHUB_EVENT_NAME === 'pull_request' ? (_c = (_b = github.context.payload.pull_request) === null || _b === void 0 ? void 0 : _b.head) === null || _c === void 0 ? void 0 : _c.sha : process.env.GITHUB_SHA; + if (mablBranch) { + core.info(`Using mabl branch [${mablBranch}]`); + } core.info(`Using git revision [${revision}]`); core.endGroup(); core.startGroup('Creating deployment event'); const apiClient = new mablApiClient_1.MablApiClient(apiKey); - const deployment = yield apiClient.postDeploymentEvent(applicationId, environmentId, browserTypes, uri, rebaselineImages, setStaticBaseline, revision, eventTime, properties); - core.setOutput('mabl-deployment-id', deployment.id); - let outputLink = baseApiUrl; + const deployment = yield apiClient.postDeploymentEvent(browserTypes, planLabels, httpHeaders, rebaselineImages, setStaticBaseline, eventTime, properties, applicationId, environmentId, uri, revision, mablBranch); + core.setOutput(constants_1.ActionOutputs.DeploymentId, deployment.id); + let appOrEnv; if (applicationId) { - const application = yield apiClient.getApplication(applicationId); - outputLink = `${baseApiUrl}/workspaces/${application.organization_id}/events/${deployment.id}`; - core.info(`Deployment triggered. View output at: ${outputLink}`); + appOrEnv = yield apiClient.getApplication(applicationId); + } + else if (environmentId) { + appOrEnv = yield apiClient.getEnvironment(environmentId); } + if (!appOrEnv) { + core.setFailed('Invalid configuration. Valid "application-id" or "environment-id" must be set. No tests started.'); + return; + } + const outputLink = `${baseApiUrl}/workspaces/${appOrEnv.organization_id}/events/${deployment.id}`; + core.info(`Deployment triggered. View output at: ${outputLink}`); core.startGroup('Await completion of tests'); let executionComplete = false; while (!executionComplete) { @@ -132,12 +166,12 @@ function run() { finalExecutionResult.executions.forEach((execution) => { core.info(table_1.prettyFormatExecution(execution)); }); - core.setOutput('plans_run', '' + finalExecutionResult.plan_execution_metrics.total); - core.setOutput('plans_passed', '' + finalExecutionResult.plan_execution_metrics.passed); - core.setOutput('plans_failed', '' + finalExecutionResult.plan_execution_metrics.failed); - core.setOutput('tests_run', '' + finalExecutionResult.journey_execution_metrics.total); - core.setOutput('tests_passed', '' + finalExecutionResult.journey_execution_metrics.passed); - core.setOutput('tests_failed', '' + finalExecutionResult.journey_execution_metrics.failed); + core.setOutput(constants_1.ActionOutputs.PlansRun, '' + finalExecutionResult.plan_execution_metrics.total); + core.setOutput(constants_1.ActionOutputs.PlansPassed, '' + finalExecutionResult.plan_execution_metrics.passed); + core.setOutput(constants_1.ActionOutputs.PlansFailed, '' + finalExecutionResult.plan_execution_metrics.failed); + core.setOutput(constants_1.ActionOutputs.TestsRun, '' + finalExecutionResult.journey_execution_metrics.total); + core.setOutput(constants_1.ActionOutputs.TestsPassed, '' + finalExecutionResult.journey_execution_metrics.passed); + core.setOutput(constants_1.ActionOutputs.TestsFailed, '' + finalExecutionResult.journey_execution_metrics.failed); if (finalExecutionResult.journey_execution_metrics.failed === 0) { core.debug('Deployment plans passed'); } @@ -154,9 +188,7 @@ function run() { } }); } -function parseBoolean(toParse) { - return (toParse === null || toParse === void 0 ? void 0 : toParse.toLowerCase()) === 'true'; -} +exports.run = run; function getExecutionsStillPending(executionResult) { return executionResult.executions.filter((execution) => { return !(EXECUTION_COMPLETED_STATUSES.includes(execution.status) && @@ -176,7 +208,7 @@ function getRelatedPullRequest() { Authorization: `token ${githubToken}`, Accept: 'application/vnd.github.groot-preview+json', 'Content-Type': 'application/json', - 'User-Agent': 'mabl-action', + 'User-Agent': constants_1.USER_AGENT, }, }; const client = axios_1.default.create(config); diff --git a/lib/src/mablApiClient.js b/lib/src/mablApiClient.js index f50dc937..9ce633e8 100644 --- a/lib/src/mablApiClient.js +++ b/lib/src/mablApiClient.js @@ -15,13 +15,14 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.MablApiClient = void 0; const async_retry_1 = __importDefault(require("async-retry")); const axios_1 = __importDefault(require("axios")); +const constants_1 = require("./constants"); class MablApiClient { constructor(apiKey) { var _a; this.baseUrl = (_a = process.env.APP_URL) !== null && _a !== void 0 ? _a : 'https://api.mabl.com'; const config = { headers: { - 'User-Agent': 'github-run-tests-action', + 'User-Agent': constants_1.USER_AGENT, Accept: 'application/json', 'Content-Type': 'application/json', }, @@ -60,13 +61,23 @@ class MablApiClient { }); }); } - getApplication(applicationId) { + getApplication(id) { return __awaiter(this, void 0, void 0, function* () { try { - return yield this.makeGetRequest(`${this.baseUrl}/v1/applications/${applicationId}`); + return yield this.makeGetRequest(`${this.baseUrl}/v1/applications/${id}`); } catch (error) { - throw new Error(`failed to get mabl application ($applicationId) from the API ${error}`); + throw new Error(`failed to get mabl application (${id}) from the API ${error}`); + } + }); + } + getEnvironment(id) { + return __awaiter(this, void 0, void 0, function* () { + try { + return yield this.makeGetRequest(`${this.baseUrl}/v1/environments/${id}`); + } + catch (error) { + throw new Error(`failed to get mabl environment (${id}) from the API ${error}`); } }); } @@ -80,10 +91,10 @@ class MablApiClient { } }); } - postDeploymentEvent(applicationId, environmentId, browserTypes, uri, rebaselineImages, setStaticBaseline, revision, eventTime, properties) { + postDeploymentEvent(browserTypes, planLabels, httpHeaders, rebaselineImages, setStaticBaseline, eventTime, properties, applicationId, environmentId, uri, revision, mablBranch) { return __awaiter(this, void 0, void 0, function* () { try { - const requestBody = this.buildRequestBody(applicationId, environmentId, browserTypes, uri, rebaselineImages, setStaticBaseline, eventTime, properties, revision); + const requestBody = this.buildRequestBody(browserTypes, planLabels, httpHeaders, rebaselineImages, setStaticBaseline, eventTime, properties, applicationId, environmentId, uri, revision, mablBranch); return yield this.makePostRequest(`${this.baseUrl}/events/deployment/`, requestBody); } catch (e) { @@ -91,7 +102,7 @@ class MablApiClient { } }); } - buildRequestBody(applicationId, environmentId, browserTypes, uri, rebaselineImages, setStaticBaseline, event_time, properties, revision) { + buildRequestBody(browserTypes, planLabels, httpHeaders, rebaselineImages, setStaticBaseline, event_time, properties, applicationId, environmentId, uri, revision, mablBranch) { const requestBody = {}; if (environmentId) { requestBody.environment_id = environmentId; @@ -99,14 +110,31 @@ class MablApiClient { if (applicationId) { requestBody.application_id = applicationId; } + if (mablBranch) { + requestBody.source_control_tag = mablBranch; + } const planOverrides = {}; - if (browserTypes) { - planOverrides.browser_types = browserTypes.split(','); + if (browserTypes.length) { + planOverrides.browser_types = browserTypes; } if (uri) { planOverrides.uri = uri; } + if (httpHeaders.length) { + planOverrides.http_headers = httpHeaders.map((header) => { + const parts = header.split(':', 2); + return { + name: parts[0], + value: parts[1], + log_header_value: false, + }; + }); + planOverrides.http_headers_required = true; + } requestBody.plan_overrides = planOverrides; + if (planLabels.length) { + requestBody.plan_labels = planLabels; + } if (revision) { requestBody.revision = revision; } diff --git a/lib/test/suite.test.js b/lib/test/suite.test.js index 50f6b56d..08b53eff 100644 --- a/lib/test/suite.test.js +++ b/lib/test/suite.test.js @@ -1,13 +1,68 @@ "use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; Object.defineProperty(exports, "__esModule", { value: true }); const mablApiClient_1 = require("../src/mablApiClient"); +const src_1 = require("../src"); +const constants_1 = require("../src/constants"); describe('GitHub Action tests', () => { + function setGithubInput(name, value) { + process.env[`INPUT_${name.replace(/ /g, '_').toUpperCase()}`] = value; + } + function assertExitCode(expected) { + expect(process.exitCode).toEqual(expected); + } + it('handles invalid application/environment ids', () => __awaiter(void 0, void 0, void 0, function* () { + setGithubInput(constants_1.ActionInputs.ApplicationId, ''); + setGithubInput(constants_1.ActionInputs.EnvironmentId, ''); + yield src_1.run(); + assertExitCode(1); + })); + it('parses array inputs', () => { + setGithubInput(constants_1.ActionInputs.BrowserTypes, ''); + expect(src_1.optionalArrayInput(constants_1.ActionInputs.BrowserTypes)).toEqual([]); + setGithubInput(constants_1.ActionInputs.BrowserTypes, 'chrome '); + expect(src_1.optionalArrayInput(constants_1.ActionInputs.BrowserTypes)).toEqual(['chrome']); + setGithubInput(constants_1.ActionInputs.BrowserTypes, 'chrome, firefox '); + expect(src_1.optionalArrayInput(constants_1.ActionInputs.BrowserTypes)).toEqual(['chrome', 'firefox']); + setGithubInput(constants_1.ActionInputs.BrowserTypes, 'chrome\nfirefox\nsafari '); + expect(src_1.optionalArrayInput(constants_1.ActionInputs.BrowserTypes)).toEqual(['chrome', 'firefox', 'safari']); + }); + it('parses boolean inputs', () => { + setGithubInput(constants_1.ActionInputs.RebaselineImages, ''); + expect(src_1.booleanInput(constants_1.ActionInputs.RebaselineImages)).toEqual(false); + setGithubInput(constants_1.ActionInputs.RebaselineImages, 'false '); + expect(src_1.booleanInput(constants_1.ActionInputs.RebaselineImages)).toEqual(false); + setGithubInput(constants_1.ActionInputs.RebaselineImages, 'False '); + expect(src_1.booleanInput(constants_1.ActionInputs.RebaselineImages)).toEqual(false); + setGithubInput(constants_1.ActionInputs.RebaselineImages, 'True '); + expect(src_1.booleanInput(constants_1.ActionInputs.RebaselineImages)).toEqual(true); + setGithubInput(constants_1.ActionInputs.RebaselineImages, 'true '); + expect(src_1.booleanInput(constants_1.ActionInputs.RebaselineImages)).toEqual(true); + setGithubInput(constants_1.ActionInputs.RebaselineImages, 'TRUE'); + expect(src_1.booleanInput(constants_1.ActionInputs.RebaselineImages)).toEqual(true); + }); + it('parses optional string inputs', () => { + setGithubInput(constants_1.ActionInputs.ApplicationId, ''); + expect(src_1.optionalInput(constants_1.ActionInputs.ApplicationId)).toBeUndefined(); + setGithubInput(constants_1.ActionInputs.ApplicationId, 'baz '); + expect(src_1.optionalInput(constants_1.ActionInputs.ApplicationId)).toEqual('baz'); + setGithubInput(constants_1.ActionInputs.ApplicationId, 'BAZ'); + expect(src_1.optionalInput(constants_1.ActionInputs.ApplicationId)).toEqual('BAZ'); + }); it('builds the request correctly with all options', () => { const expected = { environment_id: 'env', application_id: 'app', plan_overrides: { - browser_types: ['firefox', ' chrome', ' internet_explorer'], + browser_types: ['firefox', 'chrome', 'internet_explorer'], uri: 'uri', }, actions: { rebaseline_images: true, set_static_baseline: true }, @@ -27,7 +82,7 @@ describe('GitHub Action tests', () => { }, }; const apiClient = new mablApiClient_1.MablApiClient('test'); - const requestBody = apiClient.buildRequestBody('app', 'env', 'firefox, chrome, internet_explorer', 'uri', true, true, 0, { + const requestBody = apiClient.buildRequestBody(['firefox', 'chrome', 'internet_explorer'], [], [], true, true, 0, { repository_branch_name: 'master', repository_commit_username: 'gcooney', repository_action: 'mabl-tests', @@ -39,13 +94,27 @@ describe('GitHub Action tests', () => { repository_pull_request_title: 'good pr', repository_pull_request_merged_at: '2019', repository_pull_request_created_at: '2019', - }, 'abcs'); + }, 'app', 'env', 'uri', 'abcs'); expect(expected).toStrictEqual(requestBody); }); it('builds the request correctly with some options', () => { const expected = { application_id: 'app', - plan_overrides: { uri: 'uri' }, + plan_labels: ['alpha', 'beta'], + plan_overrides: { + uri: 'uri', + browser_types: ['chrome', 'firefox'], + http_headers: [{ + name: 'Header-Uno', + value: 'value-uno', + log_header_value: false, + }, { + name: 'Header-Dos', + value: 'value-dos', + log_header_value: false, + }], + http_headers_required: true + }, actions: {}, revision: 'abcs', properties: { @@ -63,7 +132,7 @@ describe('GitHub Action tests', () => { }, }; const apiClient = new mablApiClient_1.MablApiClient('test'); - const requestBody = apiClient.buildRequestBody('app', '', '', 'uri', false, false, 0, { + const requestBody = apiClient.buildRequestBody(['chrome', 'firefox'], ['alpha', 'beta'], ['Header-Uno:value-uno', 'Header-Dos:value-dos'], false, false, 0, { repository_branch_name: 'master', repository_commit_username: 'gcooney', repository_action: 'mabl-tests', @@ -75,7 +144,7 @@ describe('GitHub Action tests', () => { repository_pull_request_title: 'good pr', repository_pull_request_merged_at: '2019', repository_pull_request_created_at: '2019', - }, 'abcs'); + }, 'app', '', 'uri', 'abcs'); expect(expected).toStrictEqual(requestBody); }); });