├── .dockerignore ├── .gitignore ├── .gitlab-ci.yml ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── index.js ├── lib ├── AppInstall.js ├── AppPublish.js ├── AppRollback.js ├── BatchInstall.js ├── PluginActivate.js ├── PluginRollback.js ├── SCApply.js ├── ScanInstance.js ├── ServiceNowCICDRestAPIService.js ├── TestRun.js └── envpipeline.js └── task.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .git 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - apply 3 | - publish 4 | - install 5 | - installplugin 6 | - test 7 | - rollbackapp 8 | - rollbackplugin 9 | 10 | variables: 11 | TESTSTATE: "not run" 12 | SN_AUTH_DEV: $(K8S_SECRET_SN_AUTH_DEV) 13 | SN_AUTH_PROD: $(K8S_SECRET_SN_AUTH_PROD) 14 | DOTENV_FILE: "$CI_PROJECT_DIR/build.env" 15 | 16 | image: amalyi/gltst:latest 17 | 18 | SCApply: 19 | stage: apply 20 | variables: 21 | task: SCApply 22 | NOWINSTANCE: "cicdgitlabappauthor.service-now.com" 23 | NOWAUTH: $SN_AUTH_DEV 24 | APP_SCOPE: "x_sofse_cicd_gitla" 25 | script: 26 | - task.sh 27 | when: manual 28 | AppPublish: 29 | stage: publish 30 | variables: 31 | task: AppPublish 32 | NOWINSTANCE: "cicdgitlabappauthor.service-now.com" 33 | NOWAUTH: $SN_AUTH_DEV 34 | SCOPE: "x_sofse_cicd_gitla" 35 | VERSIONFORMAT: "autodetect" 36 | DEVNOTES: "Updated version" 37 | artifacts: 38 | reports: 39 | dotenv: $DOTENV_FILE 40 | script: 41 | - task.sh 42 | AppInstall: 43 | stage: install 44 | variables: 45 | task: AppInstall 46 | NOWINSTANCE: "cicdgitlabappclient.service-now.com" 47 | NOWAUTH: $SN_AUTH_PROD 48 | SCOPE: "x_sofse_cicd_gitla" 49 | VERSION: $PUBLISHVERSION 50 | artifacts: 51 | reports: 52 | dotenv: $DOTENV_FILE 53 | script: 54 | - task.sh 55 | installPlugin: 56 | stage: installplugin 57 | variables: 58 | task: PluginActivate 59 | NOWINSTANCE: "cicdgitlabappclient.service-now.com" 60 | NOWAUTH: $SN_AUTH_PROD 61 | PLUGINID: "com.servicenow_now_calendar" 62 | script: 63 | - task.sh 64 | 65 | testsuccess: 66 | stage: test 67 | variables: 68 | task: TestRun 69 | NOWINSTANCE: "cicdgitlabappclient.service-now.com" 70 | NOWAUTH: $SN_AUTH_PROD 71 | TEST_SUITE_SYS_ID: "0a383a65532023008cd9ddeeff7b1258" 72 | script: 73 | - echo 'TESTSTATE=started' >> $DOTENV_FILE 74 | - task.sh 75 | artifacts: 76 | reports: 77 | dotenv: $DOTENV_FILE 78 | testfail: 79 | stage: test 80 | variables: 81 | task: TestRun 82 | NOWINSTANCE: "cicdgitlabappclient.service-now.com" 83 | NOWAUTH: $SN_AUTH_PROD 84 | TEST_SUITE_SYS_ID: "d24c05f01bd31c506ce4a82b234bcbf6" 85 | script: 86 | - echo 'TESTSTATE=started' >> $DOTENV_FILE 87 | - task.sh 88 | artifacts: 89 | reports: 90 | dotenv: $DOTENV_FILE 91 | 92 | PluginRollback: 93 | stage: rollbackplugin 94 | variables: 95 | task: PluginRollback 96 | NOWINSTANCE: "cicdgitlabappclient.service-now.com" 97 | NOWAUTH: $SN_AUTH_PROD 98 | PLUGINID: "com.servicenow_now_calendar" 99 | script: 100 | - "[[ \"$TESTSTATE\" == \"started\" ]] && task.sh; echo Done" 101 | when: on_failure 102 | AppRollback: 103 | stage: rollbackapp 104 | variables: 105 | task: AppRollback 106 | NOWINSTANCE: "cicdgitlabappclient.service-now.com" 107 | NOWAUTH: $SN_AUTH_PROD 108 | SCOPE: "x_sofse_cicd_gitla" 109 | ROLLBACKVERSION: $(rollbackVersion) 110 | script: 111 | - echo $TESTSTATE 112 | - echo $rollbackVersion 113 | - "[[ \"$TESTSTATE\" == \"started\" ]] && task.sh; echo Done" 114 | when: on_failure 115 | 116 | 117 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contribution Model 2 | 3 | Contributions to help improve this integration are more than welcome. As is standard workflow, please fork the project to your own repo, create a branch to commit your changes to, then submit a pull request back to this project. An open-source community maintainer will then help to review the changes and merge the pull request if the changes are approved. A new version of the integration will then be published onto the respective marketplace. In your pull request, please include the following where applicable: 4 | 5 | 1. Description of the problem solved. Include Issue link if there is one. 6 | 2. Acceptance criteria for how an approver can tell if the "story" was completed. 7 | 8 | If your changes involve code, please update the unit tests as well. 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12-alpine 2 | WORKDIR /cicd/ 3 | ENV PATH "$PATH:/cicd/" 4 | COPY . . 5 | RUN chmod +x task.sh -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 ServiceNow 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ServiceNow CI/CD Docker Image for Gitlab CI/CD 2 | 3 | ## Contents 4 | 5 | - [Intro](#intro) 6 | - [Docker](#Docker) 7 | - [Usage](#usage) 8 | - [API Docs](#api-docs) 9 | - [List of tasks](#tasks) 10 | 11 | --- 12 | 13 | ## Intro 14 | 15 | This Docker image provides build steps for configuring CI/CD pipelines with Now Platform application development. **Click on the below screenshot to see a video for how you can use this extension to get started faster.** 16 | 17 | [![Get Started with GitLab in 10 Minutes](https://gitlab.com/ServiceNow-DevX/sncicd-gitlab-pipeline/-/raw/master/README_images/youtube_link_GitLab.png)](https://www.youtube.com/watch?v=B_LSwYKE11s "Get Started with GitLab in 10 Minutes") 18 | 19 | The build steps are API wrappers for the [CI/CD APIs](https://developer.servicenow.com/dev.do#!/reference/api/paris/rest/cicd-api) first released with Orlando. They will currently work with the Orlando and Paris releases. 20 | 21 | This is intended to be used with a pipeline .yml file, such as the example provided at our [GitLab repo](https://gitlab.com/ServiceNow-DevX/sncicd-gitlab-pipeline/-/blob/master/.gitlab-ci.yml). 22 | 23 | ## Docker 24 | 25 | Before pushing the image onto Docker Hub, please make sure to link to your Docker account: `docker login` 26 | 27 | ```shell script 28 | docker build . -t "servicenowdevx/sncicd-gitlab-docker" 29 | docker push servicenowdevx/sncicd-gitlab-docker:latest 30 | ``` 31 | 32 | ## Usage 33 | 34 | 1. [Link to Source Control](https://developer.servicenow.com/dev.do#!/learn/learning-plans/paris/new_to_servicenow/app_store_learnv2_devenvironment_paris_linking_an_application_to_source_control) for an application that has been created on your instance. 35 | 2. Add a new file named **.gitlab-ci.yml** in the root directory of the Git repo. This will be your pipeline in GitLab. 36 | 3. Copy and paste the contents of a pipeline template. We provide an example in this repo with the [.gitlab-ci.yml](.gitlab-ci.yml) file. 37 | 4. Configure your CICD variables by defining keys such as **SN_AUTH_DEV** and values in the format **username:password**. 38 | 39 | ![CICD variables](https://gitlab.com/ServiceNow-DevX/sncicd-gitlab-pipeline/-/raw/master/README_images/cicdvariables.png) 40 | 41 | 5. Depending on how you have your triggers for the pipeline setup, you can now run a build on every commit, PR, etc. 42 | 43 | **Other Notes** 44 | 45 | Build steps are not independently named, and can be run as `task.sh` throughout your pipeline. To choose which build step to run, specify the `task` variable as a part of the variables section. Please note that the `task` variable must be in lowercase, while all other variables must be in UPPER_CASE. 46 | 47 | ## API docs 48 | 49 | All the API calls are made corresponding with ServiceNow [REST API documentation](https://developer.servicenow.com/dev.do#!/reference/api/orlando/rest/cicd-api). Extension covers all the endpoints mentioned there. Some of endpoints have no separate task in extension's because of helper nature of these endpoint i.e. progress API. 50 | 51 | ## Tasks 52 | 53 | ### Required parameters 54 | 55 | Every task must have defined env variables `NOWAUTH` and `NOWINSTANCE` - auth in form of login:password and ServiceNow instance domain. 56 | 57 | In order to keep sensitive data like password safe, use protected variables (see `K8_SECRET_*` for GitLab) and pass them in pipeline without copy and paste passwords itself. 58 | 59 | - Apply SourceControl Changes 60 | > Apply changes from a remote source control to a specified local application 61 | > Parameters: 62 | > - task=SCApply 63 | > - APP_SCOPE 64 | > - APP_SYS_ID 65 | > - BRANCH 66 | 67 | - Publish Application 68 | > Installs the specified application from the application repository onto the local instance 69 | > Parameters: 70 | > - task=AppPublish 71 | > - SCOPE 72 | > - SYS_ID 73 | > - DEV_NOTES 74 | > - VERSIONFORMAT=(exact|autodetect) 75 | > - VERSION 76 | 77 | - Install Application 78 | > Installs the specified application from the application repository onto the local instance 79 | > Parameters: 80 | > - task=AppInstall 81 | > - SCOPE 82 | > - SYS_ID 83 | > - VERSION 84 | 85 | - Rollback App 86 | > Initiate a rollback of a specified application to a specified version. 87 | > Parameters: 88 | > - task=AppRollback 89 | > - SCOPE 90 | > - SYS_ID 91 | > - VERSION 92 | 93 | - Add a plugin 94 | > Activate a desired plugin on ServiceNow instance 95 | > Parameters: 96 | > - task=PluginActivate 97 | > - PLUGINID 98 | 99 | - Rollback a plugin 100 | > Rollback a desired plugin on ServiceNow instance 101 | > Parameters: 102 | > - task=PluginRolback 103 | > - PLUGINID 104 | 105 | - Start Test Suite 106 | > Start a specified automated test suite. 107 | > Parameters: 108 | > - task=TestRun 109 | > - BROWSER_NAME 110 | > - BROWSER_VERSION 111 | > - OS_NAME 112 | > - OS_VERSION 113 | > - TEST_SUITE_SYS_ID 114 | > - TEST_SUITE_NAME 115 | 116 | ## Support Model 117 | 118 | ServiceNow built this integration with the intent to help customers get started faster in adopting CI/CD APIs for DevOps workflows, but __will not be providing formal support__. This integration is therefore considered "use at your own risk", and will rely on the open-source community to help drive fixes and feature enhancements via Issues. Occasionally, ServiceNow may choose to contribute to the open-source project to help address the highest priority Issues, and will do our best to keep the integrations updated with the latest API changes shipped with family releases. This is a good opportunity for our customers and community developers to step up and help drive iteration and improvement on these open-source integrations for everyone's benefit. 119 | 120 | ## Governance Model 121 | 122 | Initially, ServiceNow product management and engineering representatives will own governance of these integrations to ensure consistency with roadmap direction. In the longer term, we hope that contributors from customers and our community developers will help to guide prioritization and maintenance of these integrations. At that point, this governance model can be updated to reflect a broader pool of contributors and maintainers. 123 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const tasks = ['AppInstall', 'AppPublish', 'AppRollback', 'PluginActivate', 'PluginRollback', 'SCApply', 'TestRun', 'ScanInstance', 'BatchInstall']; 2 | try { 3 | const taskName = process.env.task || ''; 4 | const task = tasks.indexOf(taskName) === -1 ? () => Promise.reject('Task not found') : require('./lib/' + taskName), 5 | pipeline = require('./lib/envpipeline'); 6 | task(pipeline).then(res => pipeline.success(res)).catch(e => pipeline.fail(e)); 7 | } catch (e) { 8 | process.stderr.write('The error is:' + e); 9 | process.exit(1); 10 | } -------------------------------------------------------------------------------- /lib/AppInstall.js: -------------------------------------------------------------------------------- 1 | const APIService = require('./ServiceNowCICDRestAPIService'); 2 | 3 | let API; 4 | module.exports = (pipeline, transport) => { 5 | API = new APIService(pipeline.url(), pipeline.auth(), transport); 6 | let options = {}; 7 | 'scope sys_id version base_app_version auto_upgrade_base_app' 8 | .split(' ') 9 | .forEach(name => { 10 | const val = pipeline.get(name); 11 | if (val) { 12 | if (name === 'auto_upgrade_base_app') { 13 | options[name] = val === 'true' ? true : false; 14 | } else { 15 | options[name] = val; 16 | } 17 | } 18 | }); 19 | if (!options.version) { 20 | let envVersion = pipeline.getVar('ServiceNow-CICD-App-Publish.publishVersion'); 21 | if (!envVersion) { 22 | pipeline.getVar('publishVersion'); 23 | } 24 | if (envVersion) { 25 | options.version = envVersion; 26 | } 27 | } 28 | if (options.version) { 29 | console.log('Installing with version: ' + options.version); 30 | } 31 | return API 32 | .appRepoInstall(options) 33 | .then(function (version) { 34 | console.log('\x1b[32mSuccess\x1b[0m\n'); 35 | if (version) { 36 | pipeline.setVar('rollbackVersion', version); 37 | console.log('Rollback version is: ' + version); 38 | return version; 39 | } 40 | }) 41 | .catch(err => { 42 | process.stderr.write('\x1b[31mInstallation failed\x1b[0m\n'); 43 | process.stderr.write(`The error is: ${err}\n`); 44 | return Promise.reject(err); 45 | }); 46 | }; -------------------------------------------------------------------------------- /lib/AppPublish.js: -------------------------------------------------------------------------------- 1 | const APIService = require('./ServiceNowCICDRestAPIService'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | let API; 6 | 7 | /** 8 | * 9 | * @param sourceDir string 10 | * @param sysId string 11 | * @param scope string 12 | * @returns string 13 | */ 14 | function getCurrVersionFromFS(options) { 15 | const { sys_id: sysId, scope, is_app_customization: isAppCustomization } = options; 16 | const filePrefix = isAppCustomization ? 'sys_app_customization_' : 'sys_app_' ; 17 | 18 | let version; 19 | let sourceDir = options.source_dir || process.env.CI_PROJECT_DIR; 20 | if (sourceDir) { 21 | console.log('Looking in ' + sourceDir); 22 | try { 23 | let sourceDirContent = fs.readdirSync(sourceDir); 24 | if (sourceDirContent && sourceDirContent.indexOf('sn_source_control.properties')) { 25 | let snConfig = fs.readFileSync(path.join(sourceDir, '/sn_source_control.properties')).toString(); 26 | let match = snConfig.match(/^path=(.*)\s*$/m); 27 | if (match) { 28 | const projectPath = path.join(sourceDir, match[1]); 29 | console.log('Trying ' + projectPath); 30 | if (sysId) { 31 | const verMatch = fs 32 | .readFileSync(path.join(projectPath, filePrefix + sysId + '.xml')) 33 | .toString() 34 | .match(/([^<]+)<\/version>/); 35 | if (verMatch) { 36 | version = verMatch[1]; 37 | } 38 | } else { 39 | const dirContent = fs.readdirSync(projectPath); 40 | if (dirContent) { 41 | const escapedScope = scope.replace(/&/g, '&').replace(/ regex.test(f)); 44 | for (const app of apps) { 45 | console.log('Try ' + app); 46 | const fcontent = fs.readFileSync(path.join(projectPath, app)).toString(); 47 | if (fcontent.indexOf('' + escapedScope + '') > 0) { 48 | version = fcontent.match(/([^<]+)<\/version>/)[1]; 49 | break; 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } else { 56 | process.stderr.write('sn_source_control.properties not found\n') 57 | } 58 | } catch (e) { 59 | process.stderr.write(e.toString() + '\n'); 60 | } 61 | } else { 62 | process.stderr.write('BUILD_SOURCESDIRECTORY env not found\n'); 63 | } 64 | 65 | return version; 66 | } 67 | 68 | module.exports = (pipeline, transport) => { 69 | API = new APIService(pipeline.url(), pipeline.auth(), transport); 70 | let options = {}; 71 | let version; 72 | 'scope sys_id dev_notes source_dir is_app_customization increment_by' 73 | .split(' ') 74 | .forEach(name => { 75 | const val = pipeline.get(name); 76 | if (val) { 77 | if (name === 'is_app_customization') { 78 | // convert 'false' to false 79 | options[name] = val === 'true' ? true : false; 80 | } else { 81 | options[name] = val; 82 | } 83 | } 84 | }); 85 | options.version_format = pipeline.get('versionFormat'); 86 | switch (options.version_format) { 87 | case "exact": 88 | options.version = pipeline.get('version', true); 89 | break; 90 | case "template": 91 | options.version = pipeline.get('template', true) + '.' + process.env.CI_PIPELINE_IID; 92 | break; 93 | case "detect": 94 | version = getCurrVersionFromFS(options); 95 | if (+options.increment_by < 0) { 96 | return Promise.reject('Increment_by should be positive or zero.'); 97 | } else { 98 | increment = options.increment_by ? +options.increment_by : 0; 99 | } 100 | if (version) { 101 | console.log('Current version is ' + version + ', incrementing'); 102 | version = version.split('.').map(digit => parseInt(digit)); 103 | version[2]+=increment; 104 | version = version.join('.'); 105 | options.version = version; 106 | } else { 107 | return Promise.reject('Cannot detect version from file'); 108 | } 109 | break; 110 | case "autodetect": 111 | options.autodetect = true; 112 | break; 113 | default: 114 | process.stderr.write('No version format selected\n'); 115 | return Promise.reject(); 116 | } 117 | 118 | console.log('Start installation with version ' + (options.version || 'autodetect')); 119 | return API 120 | .appRepoPublish(options) 121 | .then(function (version) { 122 | pipeline.setVar('publishVersion', version); 123 | console.log('\x1b[32mSuccess\x1b[0m\n'); 124 | console.log('Publication was made with version: ' + version); 125 | return version; 126 | }) 127 | .catch(err => { 128 | process.stderr.write('\x1b[31mPublication failed\x1b[0m\n'); 129 | process.stderr.write('The error is:' + err); 130 | return Promise.reject(err); 131 | }) 132 | } 133 | -------------------------------------------------------------------------------- /lib/AppRollback.js: -------------------------------------------------------------------------------- 1 | const APIService = require('./ServiceNowCICDRestAPIService'); 2 | 3 | let API; 4 | module.exports = (pipeline, transport) => { 5 | API = new APIService(pipeline.url(), pipeline.auth(), transport); 6 | let options = {}; 7 | 'scope sys_id version' 8 | .split(' ') 9 | .forEach(name => { 10 | const val = pipeline.get(name); 11 | if (val) { 12 | options[name] = val; 13 | } 14 | }); 15 | if (!options.version) { // try to get envvar 16 | options.version = pipeline.getVar('ServiceNow-CICD-App-Install.rollbackVersion'); 17 | } 18 | if (!options.version) { // try to get envvar 19 | options.version = pipeline.getVar('rollbackVersion'); 20 | } 21 | if (options.version) { 22 | console.log(`Using version ${options.version} to rollback application.`) 23 | } 24 | const forceRollback = pipeline.get('autodetectVersion') === 'yes'; 25 | if (!options.version && forceRollback) { // 26 | console.log('Trying to detect rollback version automatically.'); 27 | options.version = '9999.9999.9999'; 28 | } 29 | return API 30 | .appRepoRollback(options, forceRollback) 31 | .then(function (version) { 32 | console.log('\x1b[32mSuccess\x1b[0m\n'); 33 | console.log('Successfully rolled back to version: ' + version) 34 | return version; 35 | }) 36 | .catch(err => { 37 | process.stderr.write('\x1b[31mRollback failed\x1b[0m\n'); 38 | process.stderr.write('The error is:' + err); 39 | return Promise.reject(err); 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /lib/BatchInstall.js: -------------------------------------------------------------------------------- 1 | const APIService = require('./ServiceNowCICDRestAPIService'); 2 | const path = require('path'); 3 | 4 | const defaultManifestFile = 'manifest.json'; 5 | 6 | /** 7 | * 8 | * @param filename string 9 | * @returns object 10 | */ 11 | function getPayloadFromFS(options) { 12 | let { source_dir: sourceDir, filename } = options; 13 | sourceDir = sourceDir || process.env.CI_PROJECT_DIR; 14 | const manifestFile = filename || defaultManifestFile; 15 | if (sourceDir) { 16 | const fullpath = path.join(sourceDir, manifestFile); 17 | console.log('Reading payload from ' + fullpath); 18 | try { 19 | const payload = require(fullpath); 20 | 21 | return payload; 22 | } catch (error) { 23 | process.stderr.write(error.toString() + '\n'); 24 | } 25 | } else { 26 | process.stderr.write('BUILD_SOURCESDIRECTORY env not found\n'); 27 | } 28 | 29 | return Promise.reject(); 30 | } 31 | 32 | module.exports = (pipeline, transport) => { 33 | let API = new APIService(pipeline.url(), pipeline.auth(), transport); 34 | 35 | let options = {}, payload = ''; 36 | 'payload_source batch_name batch_notes batch_packages batch_file source_dir' 37 | .split(' ') 38 | .forEach(name => { 39 | const val = pipeline.get(name); 40 | if (val) { 41 | options[name] = val; 42 | } 43 | }); 44 | const url = 'app/batch/install'; 45 | switch (options.payload_source) { 46 | case 'file': 47 | payload = getPayloadFromFS(options); 48 | break; 49 | case 'pipeline': 50 | payload = { 51 | name: options.batch_name, 52 | notes: options.batch_notes, 53 | packages: JSON.parse(`[${options.batch_packages}]`) 54 | }; 55 | break; 56 | default: 57 | process.stderr.write('Wrong payload source is provided.\n'); 58 | return Promise.reject(); 59 | } 60 | 61 | console.log(`Start ${payload.name} batch install`); 62 | return API 63 | .batchInstall(url, JSON.stringify(payload)) 64 | .then(function (rollbackUrl) { 65 | console.log('\x1b[32mSuccess\x1b[0m\n'); 66 | console.log('Rollback URL: ', rollbackUrl); 67 | pipeline.setVar('ServiceNow-CICD-Batch-Install.rollbackUrl', rollbackUrl); 68 | pipeline.setVar('rollbackUrl', rollbackUrl); 69 | 70 | return rollbackUrl; 71 | }) 72 | .catch(err => { 73 | process.stderr.write('\x1b[31mBatch Install failed!\x1b[0m\n'); 74 | err && process.stderr.write('The error is: ' + err); 75 | return Promise.reject(err); 76 | }); 77 | }; 78 | -------------------------------------------------------------------------------- /lib/PluginActivate.js: -------------------------------------------------------------------------------- 1 | const APIService = require('./ServiceNowCICDRestAPIService'); 2 | let API; 3 | module.exports = (pipeline, transport) => { 4 | API = new APIService(pipeline.url(), pipeline.auth(), transport); 5 | return API 6 | .activatePlugin(pipeline.get('pluginID', true)) 7 | .then(function (status) { 8 | console.log('\x1b[32mSuccess\x1b[0m\nPlugin has been activated.'); 9 | if (status) { 10 | console.log('Status is: ' + status); 11 | return status; 12 | } 13 | }) 14 | .catch(err => { 15 | process.stderr.write('\x1b[31mPlugin activation failed\x1b[0m\n'); 16 | process.stderr.write('The error is:' + err); 17 | return Promise.reject(err); 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /lib/PluginRollback.js: -------------------------------------------------------------------------------- 1 | const APIService = require('./ServiceNowCICDRestAPIService'); 2 | let API; 3 | module.exports = (pipeline, transport) => { 4 | API = new APIService(pipeline.url(), pipeline.auth(), transport); 5 | return API 6 | .deActivatePlugin(pipeline.get('pluginID', true)) 7 | .then(function (status) { 8 | console.log('\x1b[32mSuccess\x1b[0m\nPlugin has been deactivated.'); 9 | if (status) { 10 | console.log('Status is: ' + status); 11 | return status 12 | } 13 | }) 14 | .catch(err => { 15 | process.stderr.write('\x1b[31mPlugin deactivation failed\x1b[0m\n'); 16 | process.stderr.write('The error is:' + err); 17 | return Promise.reject(err); 18 | }) 19 | 20 | } -------------------------------------------------------------------------------- /lib/SCApply.js: -------------------------------------------------------------------------------- 1 | const APIService = require('./ServiceNowCICDRestAPIService'); 2 | 3 | let API; 4 | module.exports = (pipeline, transport) => { 5 | API = new APIService(pipeline.url(), pipeline.auth(), transport); 6 | let options = {}; 7 | 'app_scope app_sys_id branch' 8 | .split(' ') 9 | .forEach(name => { 10 | const val = pipeline.get(name); 11 | if (val) { 12 | options[name] = val; 13 | } 14 | }); 15 | return API 16 | .SCApplyChanges(options) 17 | .then(function (response) { 18 | process.stdout.write('\x1b[32mSuccess\x1b[0m\n'); 19 | if (response.status_message) { 20 | process.stdout.write('Status is: ' + response.status_message); 21 | return response.status_message; 22 | } 23 | }) 24 | .catch(err => { 25 | process.stderr.write('\x1b[31mInstallation failed\x1b[0m\n'); 26 | process.stderr.write('The error is:' + err); 27 | return Promise.reject(err); 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /lib/ScanInstance.js: -------------------------------------------------------------------------------- 1 | const APIService = require('./ServiceNowCICDRestAPIService'); 2 | 3 | let API; 4 | module.exports = (pipeline, transport) => { 5 | API = new APIService(pipeline.url(), pipeline.auth(), transport); 6 | let options = {}, payload = ''; 7 | 'scan_type target_table target_sys_id combo_sys_id suite_sys_id app_scope_sys_ids update_set_sys_ids' 8 | .split(' ') 9 | .forEach(name => { 10 | const val = pipeline.get(name); 11 | if (val) { 12 | options[name] = val; 13 | } 14 | }); 15 | 16 | switch (options.scan_type) { 17 | case 'full': 18 | url = 'instance_scan/full_scan'; 19 | break; 20 | case 'point': 21 | url = 'instance_scan/point_scan'; 22 | break; 23 | case 'suiteCombo': 24 | url = `instance_scan/suite_scan/combo/${options.combo_sys_id}` 25 | break; 26 | case 'suiteScoped': 27 | // requires body-payload 28 | payload = JSON.stringify({app_scope_sys_ids: options.app_scope_sys_ids.split(',')}); 29 | url = `instance_scan/suite_scan/${options.suite_sys_id}/scoped_apps` 30 | break; 31 | case 'suiteUpdate': 32 | // requires body-payload 33 | payload = JSON.stringify({update_set_sys_ids: options.update_set_sys_ids.split(',')}); 34 | url = `instance_scan/suite_scan/${options.suite_sys_id}/update_sets` 35 | break; 36 | default: 37 | process.stderr.write('Wrong ScanType provided.\n'); 38 | return Promise.reject(); 39 | } 40 | 41 | console.log(`Start ${options.scan_type} instance scan`); 42 | return API 43 | .scanInstance(url, options, payload) 44 | .then(function (response) { 45 | console.log('\x1b[32mSuccess\x1b[0m\n'); 46 | if (response.status_message) { 47 | console.log('Status message: ' + response.status_message); 48 | return response.status_message; 49 | } 50 | }) 51 | .catch(err => { 52 | process.stderr.write('\x1b[31mScan failed\x1b[0m\n'); 53 | process.stderr.write('The error is:' + err); 54 | return Promise.reject(err); 55 | }); 56 | }; 57 | -------------------------------------------------------------------------------- /lib/ServiceNowCICDRestAPIService.js: -------------------------------------------------------------------------------- 1 | const https = require('https'); 2 | const URL = require('url'); 3 | const respError = function (error, response) { 4 | this.errorMessage = error; 5 | this.response = response; 6 | }; 7 | 8 | /** 9 | * 10 | * @param instance string Domain name of instance 11 | * @param auth string username:password 12 | * @param transport default is https, and can be changed to mocks 13 | * @constructor 14 | */ 15 | function ServiceNowCICDRestAPIService(instance, auth, transport = null) { 16 | if (typeof instance !== 'string' || instance.length < 3) { 17 | throw new Error(`Instance is not valid: "${instance}"`); 18 | } 19 | if ( 20 | typeof auth !== 'string' || 21 | auth.length < 3 || 22 | auth.indexOf(':') < 1 23 | ) { 24 | throw new Error('Incorrect auth'); 25 | } 26 | instance = instance.replace(/(?:^https?:\/\/)?([^\/]+)(?:\/.*$)/, '$1'); 27 | if (!instance.length) { 28 | throw new Error('Incorrect instance'); 29 | } 30 | 31 | const config = { 32 | instance, 33 | auth, 34 | delayInProgressPolling: 3000 35 | }; 36 | const errCodeMessages = { 37 | 401: 'The user credentials are incorrect.', 38 | 403: 'Forbidden. The user is not an admin or does not have the CICD role.', 39 | 404: 'Not found. The requested item was not found.', 40 | 405: 'Invalid method. The functionality is disabled.', 41 | 409: 'Conflict. The requested item is not unique.', 42 | 500: 'Internal server error. An unexpected error occurred while processing the request.' 43 | }; 44 | 45 | this.activatePlugin = activatePlugin; 46 | this.deActivatePlugin = deactivatePlugin; 47 | // this.getTSResults = getTSResults; 48 | this.testSuiteRun = testSuiteRun; 49 | this.appRepoInstall = appRepoInstall; 50 | this.appRepoRollback = appRepoRollback; 51 | this.appRepoPublish = appRepoPublish; 52 | this.SCApplyChanges = SCApplyChanges; 53 | this.scanInstance = scanInstance; 54 | this.batchInstall = batchInstall; 55 | 56 | /** 57 | * Activate plugin by its ID 58 | * @param id string 59 | * @returns {Promise} 60 | */ 61 | function activatePlugin(id) { 62 | const URL = 'plugin/' + id + '/activate'; 63 | return request(URL, false, 'POST') 64 | .then(resp => getProgress(resp)) 65 | .catch(err => Promise.reject(err.errorMessage)) 66 | .then(resp => resp.status_message); 67 | } 68 | 69 | /** 70 | * Deactivate plugin by its ID 71 | * @param id string 72 | * @returns {Promise} 73 | */ 74 | function deactivatePlugin(id) { 75 | const URL = 'plugin/' + id + '/rollback'; 76 | return request(URL, false, 'POST') 77 | .then(resp => getProgress(resp)) 78 | .catch(err => Promise.reject(err.errorMessage)) 79 | .then(resp => resp.status_message); 80 | } 81 | 82 | /** 83 | * Get TestSuite run results by ID 84 | * @param resultID string 85 | * @returns {Promise} 86 | */ 87 | function getTSResults(resultID) { 88 | const URL = 'testsuite/results/' + resultID; 89 | return request(URL) 90 | .catch(err => Promise.reject(err.errorMessage)); 91 | } 92 | 93 | /** 94 | * Run Test Suite 95 | * available options are: 96 | * test_suite_sys_id, test_suite_name, browser_name, browser_version, os_name os_version 97 | * required options are: 98 | * test_suite_sys_id|test_suite_name 99 | * @param options object 100 | * @returns {Promise} 101 | */ 102 | 103 | function testSuiteRun(options) { 104 | if (!options || !(options.test_suite_sys_id || options.test_suite_name)) { 105 | return Promise.reject('Please specify test_suite_name or test_suite_sys_id'); 106 | } 107 | if (options.test_suite_sys_id && options.test_suite_name) { 108 | delete options.test_suite_name; 109 | } 110 | return request( 111 | 'testsuite/run', 112 | {fields: 'test_suite_sys_id test_suite_name browser_name browser_version os_name os_version', options}, 113 | 'POST') 114 | .then(resp => getProgress(resp)) 115 | .catch(err => (err.response.results && err.response.results.id) ? 116 | err.response : Promise.reject(err.errorMessage)) 117 | .then(resp => getPropertyByPath(resp, 'results.id')) 118 | .then(resultID => resultID ? getTSResults(resultID) : Promise.reject('No result ID')); 119 | } 120 | 121 | /** 122 | * Scan instance, available options are: 123 | * full, point, suiteCombo, suiteScoped, suiteUpdate 124 | * 125 | * @param url string 126 | * @param options object 127 | * @param payload string 128 | * @returns {Promise} If available, the previously installed version. If not available, null. 129 | */ 130 | function scanInstance(url, options, payload = '') { 131 | return request(url, {fields: 'target_table target_sys_id', options}, 'POST', payload) 132 | .then(resp => getProgress(resp, true)) 133 | .catch(err => Promise.reject(err.errorMessage)); 134 | } 135 | 136 | /** 137 | * Batch Install. Payload could be obtained from repo file or workflow definition. 138 | * 139 | * @param url string 140 | * @param payload string 141 | * @returns {Promise} If available, the previously installed version. If not available, null. 142 | */ 143 | function batchInstall(url, payload) { 144 | return request(url, false, 'POST', payload) 145 | .then(resp => getProgress(resp)) 146 | .then(async (resp) => { 147 | const progressUrl = resp.links.progress.url; 148 | const resultsUrl = resp.links.results.url; 149 | const rollbackUrl = resp.links.rollback.url; 150 | request(progressUrl).then(response => { 151 | if (response.status_message) { 152 | console.log('Status is: ' + response.status_message); 153 | return response.status_message; 154 | } 155 | }); 156 | await extractBatchResults(resultsUrl).then(msg => msg && console.log("Status messages:", msg)); 157 | 158 | return rollbackUrl; 159 | }) 160 | .catch(async (errObj) => { 161 | try { 162 | const resultsUrl = errObj.response.results.url; 163 | await extractBatchResults(resultsUrl).then(msg => msg && console.log("Status messages:", msg)); 164 | setTimeout(() => { console.log("World!"); }, 3000); 165 | return Promise.reject(errObj.errorMessage); 166 | } catch (error) { 167 | return Promise.reject(); 168 | } 169 | }); 170 | } 171 | 172 | /** 173 | * Extract batch install verbose messages 174 | * 175 | * @param url string 176 | * @returns string 177 | */ 178 | function extractBatchResults(url) { 179 | return url && request(url).then((resp) => { 180 | let msg = ''; 181 | resp.batch_items.forEach((item) => { 182 | msg += `\n${item.name}: ${item.state}. ${item.status_message}`; 183 | }); 184 | 185 | return msg; 186 | }).catch(err => console.log("Fetch details error: ", err)); 187 | } 188 | 189 | /** 190 | * Install the specified application from the application repository onto the local instance 191 | * available options are: 192 | * scope, sys_id, version 193 | * required options are: 194 | * scope|sys_id 195 | * @param options 196 | * @returns {Promise} If available, the previously installed version. If not available, null. 197 | */ 198 | function appRepoInstall(options) { 199 | if (!options || !(options.scope || options.sys_id)) { 200 | return Promise.reject('Please specify scope or sys_id'); 201 | } 202 | if (options.scope && options.sys_id) { 203 | delete options.scope; 204 | } 205 | return request('app_repo/install', {fields: 'sys_id scope version base_app_version auto_upgrade_base_app', options}, 'POST') 206 | .then(resp => getProgress(resp)) 207 | .catch(err => Promise.reject(err.errorMessage)) 208 | .then(resp => resp.rollback_version || (resp.results && resp.results.rollback_version)); 209 | } 210 | 211 | /** 212 | * Initiate a rollback of a specified application to a specified version 213 | * available options are: 214 | * scope, sys_id, version 215 | * required options are: 216 | * scope|sys_id, version 217 | * @param options 218 | * @returns {Promise} 219 | */ 220 | function appRepoRollback(options, forceRollback = false) { 221 | if (!options || !(options.scope || options.sys_id) || !options.version) { 222 | return Promise.reject('Please specify scope or sys_id, and version'); 223 | } 224 | if (options.scope && options.sys_id) { 225 | delete options.scope; 226 | } 227 | return request('app_repo/rollback', {fields: 'sys_id scope version', options}, 'POST') 228 | .catch(err => { 229 | if (forceRollback && err.errorMessage.indexOf('Expected rollback version does not match target: ') === 0) { 230 | options.version = err.errorMessage.substr(49); 231 | return request('app_repo/rollback', {fields: 'sys_id scope version', options}, 'POST'); 232 | } else { 233 | return Promise.reject(err); 234 | } 235 | }) 236 | .then(resp => getProgress(resp)) 237 | .then(() => options.version) 238 | .catch(err => Promise.reject(err.errorMessage)); 239 | } 240 | 241 | /** 242 | * Publish the specified application and all of its artifacts to the application repository 243 | * available options are: 244 | * scope, sys_id, version, dev_notes 245 | * required options are: 246 | * scope|sys_id 247 | * @param options 248 | * @returns {Promise} 249 | */ 250 | function appRepoPublish(options) { 251 | if (!options || !(options.scope || options.sys_id)) { 252 | return Promise.reject('Please specify scope or sys_id'); 253 | } 254 | if (['autodetect','detect'].includes(options.version_format) && 255 | options.is_app_customization === true && !options.sys_id) { 256 | return Promise.reject('Sys_id is not specified!'); 257 | } 258 | if (options.scope && options.sys_id) { 259 | delete options.scope; 260 | } 261 | 262 | let promise = Promise.resolve(); 263 | if (!options.version && options.autodetect) { 264 | let increment; 265 | if (+options.increment_by < 0) { 266 | return Promise.reject('Increment_by should be positive or zero.'); 267 | } else { 268 | increment = options.increment_by ? +options.increment_by : 0; 269 | } 270 | promise = getCurrentApplicationVersion(options).then(version=>{ 271 | if(version) { 272 | version = version.split('.'); 273 | version[2]+=increment; 274 | version = version.join('.'); 275 | options.version = version; 276 | } else { 277 | return Promise.reject('Can\'t autodetect version number.'); 278 | } 279 | }); 280 | } 281 | return promise.then(() => request('app_repo/publish', { 282 | fields: 'sys_id scope version dev_notes', 283 | options 284 | }, 'POST') 285 | .then(resp => getProgress(resp)) 286 | .catch(err => Promise.reject(err.errorMessage)) 287 | .then(() => options.version)); 288 | } 289 | 290 | /** 291 | * Start applying changes from a remote source control to a specified local application. 292 | * available options are: 293 | * app_scope, app_sys_id, branch_name 294 | * required options are: 295 | * app_scope|app_sys_id 296 | * @param options object 297 | * @returns {Promise} 298 | */ 299 | function SCApplyChanges(options) { 300 | if (!options || !(options.app_scope || options.app_sys_id)) { 301 | return Promise.reject('Please specify app_scope or app_sys_id'); 302 | } 303 | if (options.app_scope && options.app_sys_id) { 304 | delete options.app_scope; 305 | } 306 | return request('sc/apply_changes', {fields: 'app_sys_id app_scope branch_name', options}, 'POST') 307 | .then(resp => getProgress(resp)) 308 | .catch(err => Promise.reject(err.errorMessage)); 309 | } 310 | 311 | /////////////////////////////////////////////////////////////////////////////// 312 | 313 | /** 314 | * Async wait 315 | * @param ms 316 | * @returns {Promise} 317 | */ 318 | function wait(ms) { 319 | return new Promise(a => setTimeout(() => a(), ms)); 320 | } 321 | 322 | /** 323 | * Wait until progress resolves and return result or reject on error. 324 | * @param response object 325 | * @param returnProgress bool 326 | * @returns {Promise|} 327 | */ 328 | function getProgress(response, returnProgress = false) { 329 | let status = +response.status; 330 | const progressId = getPropertyByPath(response, 'links.progress.id'); 331 | if (progressId && (status === 0 || status === 1)) { 332 | const progressUrl = 'progress/' + progressId; 333 | return request(progressUrl) 334 | .then(progressBody => { 335 | status = +progressBody.status; 336 | if (status >= 2) { 337 | process.stdout.write('\n'); 338 | response.results = getPropertyByPath(progressBody, 'links.results'); 339 | if (status === 2) { 340 | return Promise.resolve(returnProgress ? progressBody : response); 341 | } else { 342 | progressBody.results = response.results || getPropertyByPath(response, 'links.results'); 343 | return Promise.reject(new respError(progressBody.error || progressBody.status_message, progressBody)); 344 | } 345 | } else { 346 | if (status === 1) { 347 | const percentage = getPropertyByPath(progressBody, 'percent_complete'); 348 | if (percentage) { 349 | process.stdout.write(`${percentage}% complete`); 350 | } 351 | } 352 | process.stdout.write('.\n'); 353 | return wait(config.delayInProgressPolling).then(() => getProgress(response, returnProgress)); 354 | } 355 | }); 356 | } else { 357 | return status === 2 ? 358 | Promise.resolve(response) : 359 | Promise.reject(new respError(response.error || response.status_message, response)); 360 | } 361 | } 362 | 363 | /** 364 | * get current app version via now/table rest api 365 | * 366 | * @param options object 367 | * @returns {Promise} 368 | */ 369 | function getCurrentApplicationVersion(options) { 370 | if (options.sys_id) { 371 | const table = options.is_app_customization ? 'sys_app_customization' : 'sys_app'; 372 | return request(`https://${config.instance}/api/now/table/${table}/${options.sys_id}?sysparm_fields=version`) 373 | .then(data => { 374 | return (data && data.version) || false; 375 | }) 376 | .catch(() => false); 377 | } else { 378 | return request(`https://${config.instance}/api/now/table/sys_app?sysparm_fields=scope,version`) 379 | .then(data => { 380 | let version = false; 381 | if (Array.isArray(data)) { 382 | data = data.filter(e => e.scope === options.scope); 383 | if (data[0]) { 384 | version = data[0].version; 385 | } 386 | } 387 | return version; 388 | }) 389 | .catch(() => false); 390 | } 391 | } 392 | 393 | /** 394 | * Make a wrapper to https request 395 | * @param url 396 | * @param data object|boolean 397 | * @param method 398 | * @returns {Promise} 399 | */ 400 | 401 | function request(url, data = false, method = 'GET', payload = '') { 402 | if (transport) { 403 | return transport(url, data, method); 404 | } 405 | if (data) { 406 | url = createURL(url, data.fields, data.options); 407 | } 408 | if (url.indexOf('https://') !== 0) { 409 | url = `https://${config.instance}/api/sn_cicd/${url}`; 410 | } 411 | let urldata = URL.parse(url); 412 | let options = {method}; 413 | options.host = urldata.host; 414 | options.path = urldata.path; 415 | options.auth = config.auth; 416 | if (!options.headers) { 417 | options.headers = {}; 418 | } 419 | options.headers.accept = 'application/json'; 420 | options.headers['User-Agent'] = 'sncicd_extint_azure'; 421 | 422 | return httpsRequest(options, payload) 423 | .catch(err => { 424 | // console.error(err); 425 | let message = err.code || ( 426 | err.statusCode && ( 427 | errCodeMessages[err.statusCode] || err.statusCode 428 | ) 429 | ); 430 | if ( 431 | err.statusCode && 432 | err.statusCode === 400 && 433 | err.body && 434 | err.body.error 435 | ) { 436 | message = err.body.error; 437 | } 438 | if ( 439 | err.statusCode && 440 | err.statusCode === 200 && 441 | err.body.indexOf('class="instance-hibernating-page"') > 0 442 | ) { 443 | message = 'Instance is hibernated'; 444 | } 445 | return Promise.reject(new respError(message, err)); 446 | }); 447 | } 448 | 449 | /** 450 | * 451 | * @param object object 452 | * @param path string 453 | * @returns value mixed 454 | */ 455 | function getPropertyByPath(object, path) { 456 | return path.split('.').reduce((xs, x) => (xs && xs[x]) ? xs[x] : null, object); 457 | } 458 | 459 | /** 460 | * Wrapper for https calls 461 | * @param params object 462 | * @param postData object 463 | * @returns {Promise} 464 | */ 465 | function httpsRequest(params, postData) { 466 | return new Promise(function (resolve, reject) { 467 | let req = https.request(params, function (res) { 468 | // reject on bad status 469 | let body = []; 470 | res.on('data', function (chunk) { 471 | body.push(chunk); 472 | }); 473 | res.on('end', function () { 474 | body = Buffer.concat(body).toString(); 475 | let errState = res.statusCode !== 200; 476 | try { 477 | body = JSON.parse(body).result; 478 | } catch (e) { 479 | errState = true; 480 | } 481 | if (errState) { 482 | reject({statusCode: res.statusCode, body}); 483 | } else { 484 | resolve(body); 485 | } 486 | }); 487 | }); 488 | req.on('error', function (err) { 489 | reject(err); 490 | }); 491 | if (params.method === 'POST' && postData) { 492 | req.write(postData); 493 | } 494 | req.end(); 495 | }); 496 | } 497 | } 498 | 499 | /** 500 | * helper for URL building 501 | * @param prefix string 502 | * @param fields string space-separated list of fields 503 | * @param options object 504 | * @returns {string} 505 | */ 506 | function createURL(prefix, fields, options) { 507 | return prefix + '?' + 508 | fields 509 | .split(' ') 510 | .filter(optName => options.hasOwnProperty(optName)) 511 | .map(optName => optName + '=' + encodeURIComponent(options[optName])) 512 | .join('&'); 513 | } 514 | 515 | module.exports = ServiceNowCICDRestAPIService; 516 | -------------------------------------------------------------------------------- /lib/TestRun.js: -------------------------------------------------------------------------------- 1 | const APIService = require('./ServiceNowCICDRestAPIService'); 2 | const messages = { 3 | 'error': 'Error message.', 4 | 'rolledup_test_error_count': 'Number of tests with errors', 5 | 'rolledup_test_failure_count': 'Number of tests that failed', 6 | 'rolledup_test_skip_count': 'Number of tests that were skipped', 7 | 'rolledup_test_success_count': 'Number of tests that ran successfully', 8 | 'status_detail': 'Additional information about the current state', 9 | 'status_message': 'Description of the current state', 10 | 'test_suite_duration': 'Amount of time that it took to execute the test suite', 11 | 'test_suite_name': 'Name of the test suite' 12 | }; 13 | let API; 14 | module.exports = (pipeline, transport) => { 15 | API = new APIService(pipeline.url(), pipeline.auth(), transport); 16 | let options = {}; 17 | 'browser_name browser_version os_name os_version test_suite_sys_id test_suite_name' 18 | .split(' ') 19 | .forEach(name => { 20 | const val = pipeline.get(name); 21 | if (val) { 22 | options[name] = val; 23 | } 24 | }); 25 | return API 26 | .testSuiteRun(options) 27 | .catch(err => { 28 | process.stderr.write('\x1b[31mTestsuite run failed\x1b[0m\n'); 29 | process.stderr.write('The error is:' + err); 30 | return Promise.reject(err); 31 | }) 32 | .then(function (response) { 33 | if (response) { 34 | if (response.status === '2') { //success 35 | console.log('\x1b[32mSuccess\x1b[0m\n'); 36 | } else { 37 | process.stderr.write('\x1b[31mTestsuite run failed\x1b[0m\n'); 38 | } 39 | 40 | if (response.links && response.links.results && response.links.results.url) { 41 | console.log('Link to results is: ' + response.links.results.url); 42 | } 43 | console.log(Object.keys(messages) 44 | .filter(name => response[name]) 45 | .map(name => messages[name] + ': ' + response[name]) 46 | .join('\n') 47 | ); 48 | if (response.status !== '2') { 49 | return Promise.reject('Testsuite failed'); 50 | } 51 | } 52 | }) 53 | 54 | } 55 | -------------------------------------------------------------------------------- /lib/envpipeline.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const dotenvFilename = process.env.DOTENV_FILE || 'build.env'; 3 | module.exports = { 4 | auth: () => process.env.NOWAUTH || '', 5 | url: () => process.env.NOWINSTANCE || '', 6 | get: (name, required = false) => { 7 | name = name.toUpperCase(); 8 | let result = process.env[name] || ''; 9 | if (!result && required) { 10 | throw new Error(`Input or variable "${name}" is missing`); 11 | } 12 | return result; 13 | }, 14 | getVar: name => process.env[name.toUpperCase().replace(/[^A-Z0-9_]/g, '_')] || '', 15 | setVar: (name, value) => { 16 | process.env[name.toUpperCase()] = value.toString(); 17 | let currContent; 18 | try { 19 | currContent = fs.readFileSync(dotenvFilename).toString(); 20 | } 21 | catch (e) { 22 | currContent = ''; 23 | } 24 | let vault = {}; 25 | currContent.split('\n').filter(s=>s.length >0 && s.indexOf('=')>0).forEach(s=>{ 26 | const key = s.substr(0, s.indexOf('=')); 27 | vault[key] = s.substr(key.length + 1); 28 | }); 29 | vault[name.toUpperCase().replace(/[^A-Z0-9_]/g, '_')] = value; 30 | fs.writeFileSync(dotenvFilename, Object.keys(vault).map(key=>`${key}=${vault[key]}`).join('\n')); 31 | }, 32 | success: message => { 33 | if(message) { 34 | console.log(message); 35 | } 36 | process.exit(0); 37 | }, 38 | fail: message => { 39 | console.error(message); 40 | process.exit(1); 41 | }, 42 | }; -------------------------------------------------------------------------------- /task.sh: -------------------------------------------------------------------------------- 1 | cd /cicd/ 2 | /usr/local/bin/node ./index.js 3 | --------------------------------------------------------------------------------