diff --git a/README.md b/README.md index ba465c49..43ba4fa3 100644 --- a/README.md +++ b/README.md @@ -282,6 +282,7 @@ As seen above, we have two steps. One for a noop deploy, and one for a regular d | `merge_deploy_mode` | `false` | `"false"` | Advanced configuration option for operations on merge commits. See the [merge commit docs](#merge-commit-workflow-strategy) below | | `unlock_on_merge_mode` | `false` | `"false"` | Advanced configuration option for automatically releasing locks associated with a pull request when that pull request is merged. See the [unlock on merge mode](docs/unlock-on-merge.md) documentation for more details | | `skip_completing` | `false` | `"false"` | If set to "true", skip the process of completing a deployment. You must manually create a deployment status after the deployment is complete. Default is "false" | +| `deploy_message_path` | `false` | `".github/deployment_message.md"` | The path to a markdown file which is used as a template for custom deployment messages. Example: `".github/deployment_message.md"` | ## Outputs πŸ“€ diff --git a/__tests__/functions/check-input.test.js b/__tests__/functions/check-input.test.js new file mode 100644 index 00000000..efd7fe6f --- /dev/null +++ b/__tests__/functions/check-input.test.js @@ -0,0 +1,23 @@ +import {checkInput} from '../../src/functions/check-input' + +test('checks an input an finds that it is valid', async () => { + expect(await checkInput('production')).toStrictEqual('production') +}) + +test('checks an input an finds that it is valid with true/false strings', async () => { + expect(await checkInput('true')).toStrictEqual('true') + + expect(await checkInput('false')).toStrictEqual('false') +}) + +test('checks an empty string input an finds that it is invalid', async () => { + expect(await checkInput('')).toStrictEqual(null) +}) + +test('checks a null object input an finds that it is invalid', async () => { + expect(await checkInput(null)).toStrictEqual(null) +}) + +test('checks a string of null input an finds that it is invalid', async () => { + expect(await checkInput('null')).toStrictEqual(null) +}) diff --git a/__tests__/functions/post-deploy-message.test.js b/__tests__/functions/post-deploy-message.test.js new file mode 100644 index 00000000..59ceb733 --- /dev/null +++ b/__tests__/functions/post-deploy-message.test.js @@ -0,0 +1,183 @@ +import {postDeployMessage} from '../../src/functions/post-deploy-message' +import * as core from '@actions/core' +import dedent from 'dedent-js' + +// const debugMock = jest.spyOn(core, 'debug') + +var context +var environment +var environment_url +var environment_url_simple +var status +var noop +var ref + +beforeEach(() => { + jest.clearAllMocks() + jest.spyOn(core, 'info').mockImplementation(() => {}) + jest.spyOn(core, 'debug').mockImplementation(() => {}) + + process.env.DEPLOY_MESSAGE = null + process.env.INPUT_ENVIRONMENT_URL_IN_COMMENT = 'true' + process.env.INPUT_DEPLOY_MESSAGE_PATH = '.github/deployment_message.md' + + environment = 'production' + environment_url = 'https://example.com' + environment_url_simple = 'example.com' + status = 'success' + noop = false + ref = 'test-ref' + + context = { + actor: 'monalisa', + eventName: 'issue_comment', + workflow: 'test-workflow', + repo: { + owner: 'corp', + repo: 'test' + }, + payload: { + comment: { + id: '1' + } + } + } +}) + +test('successfully constructs a post deploy message with the defaults', async () => { + expect( + await postDeployMessage( + context, // context + environment, // environment + environment_url, // environment_url + status, // status + noop, // noop + ref // ref + ) + ).toStrictEqual( + dedent(` + ### Deployment Results βœ… + + **${context.actor}** successfully deployed branch \`${ref}\` to **${environment}** + + > **Environment URL:** [${environment_url_simple}](${environment_url})`) + ) +}) + +test('successfully constructs a post deploy message with the defaults during a "noop" deploy', async () => { + expect( + await postDeployMessage( + context, // context + environment, // environment + environment_url, // environment_url + status, // status + true, // noop + ref // ref + ) + ).toStrictEqual( + dedent(` + ### Deployment Results βœ… + + **${context.actor}** successfully **noop** deployed branch \`${ref}\` to **${environment}**`) + ) +}) + +test('successfully constructs a post deploy message with the defaults during a deployment failure', async () => { + expect( + await postDeployMessage( + context, // context + environment, // environment + environment_url, // environment_url + 'failure', // status + noop, // noop + ref // ref + ) + ).toStrictEqual( + dedent(` + ### Deployment Results ❌ + + **${context.actor}** had a failure when deploying branch \`${ref}\` to **${environment}**`) + ) +}) + +test('successfully constructs a post deploy message with the defaults during a deployment with an unknown status', async () => { + expect( + await postDeployMessage( + context, // context + environment, // environment + environment_url, // environment_url + 'unknown', // status + noop, // noop + ref // ref + ) + ).toStrictEqual( + dedent(` + ### Deployment Results ⚠️ + + Warning: deployment status is unknown, please use caution`) + ) +}) + +test('successfully constructs a post deploy message with a custom env var', async () => { + process.env.DEPLOY_MESSAGE = 'Deployed 1 shiny new server' + + expect( + await postDeployMessage( + context, // context + environment, // environment + environment_url, // environment_url + status, // status + noop, // noop + ref // ref + ) + ).toStrictEqual( + dedent(` + ### Deployment Results βœ… + + **${context.actor}** successfully deployed branch \`${ref}\` to **${environment}** + +
Show Results + + Deployed 1 shiny new server + +
+ + > **Environment URL:** [${environment_url_simple}](${environment_url})`) + ) +}) + +test('successfully constructs a post deploy message with a custom markdown file', async () => { + process.env.INPUT_DEPLOY_MESSAGE_PATH = + '__tests__/templates/test_deployment_message.md' + expect( + await postDeployMessage( + context, // context + environment, // environment + environment_url, // environment_url + status, // status + noop, // noop + ref // ref + ) + ).toStrictEqual( + dedent(`### Deployment Results :rocket: + + The following variables are available to use in this template: + + - \`environment\` - The name of the environment (String) + - \`environment_url\` - The URL of the environment (String) {Optional} + - \`status\` - The status of the deployment (String) - \`success\`, \`failure\`, or \`unknown\` + - \`noop\` - Whether or not the deployment is a noop (Boolean) + - \`ref\` - The ref of the deployment (String) + - \`actor\` - The GitHub username of the actor who triggered the deployment (String) + + Here is an example: + + monalisa deployed branch \`test-ref\` to the **production** environment. This deployment was a success :rocket:. + + You can view the deployment [here](https://example.com). + + + + `) + ) +}) diff --git a/__tests__/functions/post-deploy.test.js b/__tests__/functions/post-deploy.test.js index c331e938..6db25aba 100644 --- a/__tests__/functions/post-deploy.test.js +++ b/__tests__/functions/post-deploy.test.js @@ -3,16 +3,24 @@ import * as actionStatus from '../../src/functions/action-status' import * as lock from '../../src/functions/lock' import * as unlock from '../../src/functions/unlock' import * as createDeploymentStatus from '../../src/functions/deployment' +import * as postDeployMessage from '../../src/functions/post-deploy-message' import * as core from '@actions/core' const infoMock = jest.spyOn(core, 'info') +var octokit +var context + beforeEach(() => { jest.clearAllMocks() jest.spyOn(core, 'info').mockImplementation(() => {}) + jest.spyOn(core, 'debug').mockImplementation(() => {}) jest.spyOn(actionStatus, 'actionStatus').mockImplementation(() => { return undefined }) + jest.spyOn(postDeployMessage, 'postDeployMessage').mockImplementation(() => { + return 'Updated 1 server' + }) jest.spyOn(lock, 'lock').mockImplementation(() => { return {lockData: {sticky: true}} }) @@ -21,33 +29,32 @@ beforeEach(() => { .mockImplementation(() => { return undefined }) - jest.spyOn(core, 'debug').mockImplementation(() => {}) -}) -const context = { - actor: 'monalisa', - eventName: 'issue_comment', - workflow: 'test-workflow', - repo: { - owner: 'corp', - repo: 'test' - }, - payload: { - comment: { - id: '1' + context = { + actor: 'monalisa', + eventName: 'issue_comment', + workflow: 'test-workflow', + repo: { + owner: 'corp', + repo: 'test' + }, + payload: { + comment: { + id: '1' + } } } -} -const octokit = { - rest: { - repos: { - createDeploymentStatus: jest.fn().mockReturnValue({ - data: {} - }) + octokit = { + rest: { + repos: { + createDeploymentStatus: jest.fn().mockReturnValue({ + data: {} + }) + } } } -} +}) test('successfully completes a production branch deployment', async () => { const actionStatusSpy = jest.spyOn(actionStatus, 'actionStatus') @@ -62,9 +69,8 @@ test('successfully completes a production branch deployment', async () => { 123, 12345, 'success', - 'Deployment has created 1 new server', 'test-ref', - 'false', + false, 456, 'production', '' // environment_url @@ -88,7 +94,7 @@ test('successfully completes a production branch deployment', async () => { } }, 12345, - ' ### Deployment Results βœ…\n\n **monalisa** successfully deployed branch `test-ref` to **production**\n\n
Show Results\n\n Deployment has created 1 new server\n\n
', + 'Updated 1 server', true ) expect(createDeploymentStatusSpy).toHaveBeenCalled() @@ -128,9 +134,8 @@ test('successfully completes a production branch deployment with an environment 123, 12345, 'success', - 'Deployment has created 1 new server', 'test-ref', - 'false', + false, 456, 'production', 'https://example.com', // environment_url @@ -155,7 +160,7 @@ test('successfully completes a production branch deployment with an environment } }, 12345, - ' ### Deployment Results βœ…\n\n **monalisa** successfully deployed branch `test-ref` to **production**\n\n
Show Results\n\n Deployment has created 1 new server\n\n
\n\n> **Environment URL:** [example.com](https://example.com)', + 'Updated 1 server', true ) expect(createDeploymentStatusSpy).toHaveBeenCalled() @@ -201,9 +206,8 @@ test('successfully completes a production branch deployment and removes a non-st 123, 12345, 'success', - 'Deployment has created 1 new server', 'test-ref', - 'false', + false, 456, 'production', '' // environment_url @@ -228,7 +232,7 @@ test('successfully completes a production branch deployment and removes a non-st } }, 12345, - ' ### Deployment Results βœ…\n\n **monalisa** successfully deployed branch `test-ref` to **production**\n\n
Show Results\n\n Deployment has created 1 new server\n\n
', + 'Updated 1 server', true ) expect(createDeploymentStatusSpy).toHaveBeenCalled() @@ -273,9 +277,8 @@ test('successfully completes a noop branch deployment and removes a non-sticky l 123, 12345, 'success', - 'Deployment has created 1 new server', 'test-ref', - 'true', + true, 456, 'production' ) @@ -299,7 +302,7 @@ test('successfully completes a noop branch deployment and removes a non-sticky l } }, 12345, - ' ### Deployment Results βœ…\n\n **monalisa** successfully **noop** deployed branch `test-ref` to **production**\n\n
Show Results\n\n Deployment has created 1 new server\n\n
', + 'Updated 1 server', true ) expect(infoMock).toHaveBeenCalledWith( @@ -316,9 +319,8 @@ test('successfully completes a production branch deployment with no custom messa 123, 12345, 'success', - '', 'test-ref', - 'false', + false, 456, 'production' ) @@ -340,7 +342,7 @@ test('successfully completes a production branch deployment with no custom messa } }, 12345, - ' ### Deployment Results βœ…\n\n **monalisa** successfully deployed branch `test-ref` to **production**', + 'Updated 1 server', true ) }) @@ -353,9 +355,8 @@ test('successfully completes a noop branch deployment', async () => { 123, 12345, 'success', - 'Deployment has created 1 new server', 'test-ref', - 'true', + true, 456, 'production' ) @@ -370,9 +371,8 @@ test('updates with a failure for a production branch deployment', async () => { 123, 12345, 'failure', - 'Deployment has failed to create 1 new server', 'test-ref', - 'false', + false, 456, 'production' ) @@ -387,9 +387,8 @@ test('updates with an unknown for a production branch deployment', async () => { 123, 12345, 'unknown', - 'Deployment has failed to create 1 new server', 'test-ref', - 'false', + false, 456, 'production' ) @@ -418,9 +417,9 @@ test('fails due to no ref', async () => { context, octokit, 123, + 12345, 'success', - 'Deployment has created 1 new server', - '' + null // ref ) } catch (e) { expect(e.message).toBe('no ref provided') @@ -436,9 +435,8 @@ test('fails due to no deployment_id', async () => { 123, 12345, 'success', - 'Deployment has created 1 new server', 'test-ref', - 'false', + false, '' ) } catch (e) { @@ -455,9 +453,8 @@ test('fails due to no environment', async () => { 123, 12345, 'success', - 'Deployment has created 1 new server', 'test-ref', - 'false', + false, 456, '' ) @@ -469,16 +466,7 @@ test('fails due to no environment', async () => { test('fails due to no noop', async () => { jest.resetAllMocks() try { - await postDeploy( - context, - octokit, - 123, - 12345, - 'success', - 'Deployment has created 1 new server', - 'test-ref', - '' - ) + await postDeploy(context, octokit, 123, 12345, 'success', 'test-ref', null) } catch (e) { expect(e.message).toBe('no noop value provided') } diff --git a/__tests__/schemas/action.schema.yml b/__tests__/schemas/action.schema.yml index dbb0b754..1c996994 100644 --- a/__tests__/schemas/action.schema.yml +++ b/__tests__/schemas/action.schema.yml @@ -320,6 +320,16 @@ inputs: default: type: string required: true + deploy_message_path: + description: + type: string + required: true + required: + type: boolean + required: true + default: + type: string + required: false # outputs section outputs: diff --git a/__tests__/templates/test_deployment_message.md b/__tests__/templates/test_deployment_message.md new file mode 100644 index 00000000..55694e62 --- /dev/null +++ b/__tests__/templates/test_deployment_message.md @@ -0,0 +1,18 @@ +### Deployment Results {{ ":rocket:" if status === "success" else ":cry:" }} + +The following variables are available to use in this template: + +- `environment` - The name of the environment (String) +- `environment_url` - The URL of the environment (String) {Optional} +- `status` - The status of the deployment (String) - `success`, `failure`, or `unknown` +- `noop` - Whether or not the deployment is a noop (Boolean) +- `ref` - The ref of the deployment (String) +- `actor` - The GitHub username of the actor who triggered the deployment (String) + +Here is an example: + +{{ actor }} deployed branch `{{ ref }}` to the **{{ environment }}** environment. This deployment was a {{ status }} {{ ":rocket:" if status === "success" else ":cry:" }}. + +{% if environment_url %}You can view the deployment [here]({{ environment_url }}).{% endif %} + +{% if noop %}This was a noop deployment.{% endif %} diff --git a/action.yml b/action.yml index d7828fa5..23d839a2 100644 --- a/action.yml +++ b/action.yml @@ -121,6 +121,10 @@ inputs: description: 'If set to "true", skip the process of completing a deployment. You must manually create a deployment status after the deployment is complete. Default is "false"' required: false default: "false" + deploy_message_path: + description: 'The path to a markdown file which is used as a template for custom deployment messages. Example: ".github/deployment_message.md"' + required: false + default: ".github/deployment_message.md" outputs: triggered: description: 'The string "true" if the trigger was found, otherwise the string "false"' diff --git a/dist/index.js b/dist/index.js index 71568838..45cbc7a2 100644 --- a/dist/index.js +++ b/dist/index.js @@ -4357,6 +4357,278 @@ exports.request = request; //# sourceMappingURL=index.js.map +/***/ }), + +/***/ 3980: +/***/ (function(module) { + +// MIT license (by Elan Shanker). +(function(globals) { + 'use strict'; + + var executeSync = function(){ + var args = Array.prototype.slice.call(arguments); + if (typeof args[0] === 'function'){ + args[0].apply(null, args.splice(1)); + } + }; + + var executeAsync = function(fn){ + if (typeof setImmediate === 'function') { + setImmediate(fn); + } else if (typeof process !== 'undefined' && process.nextTick) { + process.nextTick(fn); + } else { + setTimeout(fn, 0); + } + }; + + var makeIterator = function (tasks) { + var makeCallback = function (index) { + var fn = function () { + if (tasks.length) { + tasks[index].apply(null, arguments); + } + return fn.next(); + }; + fn.next = function () { + return (index < tasks.length - 1) ? makeCallback(index + 1): null; + }; + return fn; + }; + return makeCallback(0); + }; + + var _isArray = Array.isArray || function(maybeArray){ + return Object.prototype.toString.call(maybeArray) === '[object Array]'; + }; + + var waterfall = function (tasks, callback, forceAsync) { + var nextTick = forceAsync ? executeAsync : executeSync; + callback = callback || function () {}; + if (!_isArray(tasks)) { + var err = new Error('First argument to waterfall must be an array of functions'); + return callback(err); + } + if (!tasks.length) { + return callback(); + } + var wrapIterator = function (iterator) { + return function (err) { + if (err) { + callback.apply(null, arguments); + callback = function () {}; + } else { + var args = Array.prototype.slice.call(arguments, 1); + var next = iterator.next(); + if (next) { + args.push(wrapIterator(next)); + } else { + args.push(callback); + } + nextTick(function () { + iterator.apply(null, args); + }); + } + }; + }; + wrapIterator(makeIterator(tasks))(); + }; + + if (typeof define !== 'undefined' && define.amd) { + define([], function () { + return waterfall; + }); // RequireJS + } else if ( true && module.exports) { + module.exports = waterfall; // CommonJS + } else { + globals.waterfall = waterfall; //