├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── app.json ├── docker-compose.yml ├── icon-url-map.json ├── jest.config.js ├── package-lock.json ├── package.json ├── screenshot.png └── src ├── GitlabEvent.js ├── GitlabEventParser.js ├── GitlabEventParser.test.js ├── JiraRemoteLinkGenerator.js ├── JiraRemoteLinkGenerator.test.js ├── JiraTicketExtractor.js ├── JiraTicketExtractor.test.js └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.idea 3 | /data 4 | /.env 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10.16.3-alpine 2 | 3 | RUN mkdir -p /usr/src/app && chown node:node /usr/src/app 4 | USER node 5 | 6 | ARG APP_VERSION 7 | WORKDIR /usr/src/app 8 | 9 | ENV APP_VERSION $APP_VERSION 10 | 11 | ADD ./src /usr/src/app/src 12 | ADD ./package.json /usr/src/app/ 13 | ADD ./package-lock.json /usr/src/app/ 14 | ADD ./icon-url-map.json /usr/src/app/ 15 | RUN npm install 16 | EXPOSE 3000 17 | 18 | CMD ["node", "./src/server"] 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Gitlab Jira Webhook is licensed under the terms of MIT License. 2 | 3 | Copyright (c) 2018 by DracoBlue (JanS@DracoBlue.de) 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitLab Jira Webhook [![Build Status](https://travis-ci.com/DracoBlue/gitlab-jira-webhook.svg?branch=master)](https://travis-ci.com/DracoBlue/gitlab-jira-webhook) 2 | 3 | This little webhook pushes GitLab events directly to Jira, so they are displayed in Jira tickets as remote links to the GitLab Merge Request. 4 | 5 | 6 | In Jira it looks like this: 7 | 8 | ![Screenshot](./screenshot.png) 9 | 10 | ## Setup 11 | 12 | ### 1. Create a .env-File 13 | 14 | ### 2. a) Jira with Username + Password 15 | 16 | .env: 17 | ``` 18 | GITLAB_PERSONAL_ACCESS_TOKEN=jashdjsahdsjadsas 19 | GITLAB_BASE_URL=https://git.example.org 20 | GITLAB_WEBHOOK_TOKEN=TheToken 21 | JIRA_BASE_URL=https://jira.example.org 22 | JIRA_USERNAME=jirauser 23 | JIRA_PASSWORD=thepassword 24 | ``` 25 | 26 | ### 2. b) Jira with P12/PFX 27 | 28 | .env: 29 | ``` 30 | GITLAB_PERSONAL_ACCESS_TOKEN=jashdjsahdsjadsas 31 | GITLAB_BASE_URL=https://git.example.org 32 | GITLAB_WEBHOOK_TOKEN=TheToken 33 | JIRA_PFX_PATH=./user_key.p12 34 | JIRA_PFX_PASSWORD=ThePassword 35 | JIRA_BASE_URL=https://jira.example.org 36 | ``` 37 | 38 | If you need to setup a https proxy, use: 39 | ``` 40 | JIRA_HTTPS_PROXY=http://username:password@proxy.example.org:3128 41 | ``` 42 | 43 | ### 3. a) Run with Nodejs + Run the Service 44 | 45 | ```console 46 | $ npm install 47 | $ npm start 48 | ``` 49 | 50 | 51 | ### 3. b) Run with Docker 52 | 53 | ```console 54 | $ docker run --rm --env-file .env -p80:3000 dracoblue/gitlab-jira-webhook 55 | ``` 56 | 57 | ### Setup a Webhook in Gitlab 58 | 59 | 1. Set URL to http://example.org/events if your Webhook runs on http://example.org. 60 | 2. Set the token to something, which you will configured in `GITLAB_WEBHOOK_TOKEN`. 61 | 62 | ### That's it! 63 | 64 | Now your merge requests should be visible whenever your reference a ticket with TEST-1234 at the 65 | ticket. 66 | 67 | ## Deploy to Google 68 | 69 | [![Run on Google Cloud](https://deploy.cloud.run/button.svg)](https://deploy.cloud.run) 70 | 71 | ## Private Icon Url Map 72 | 73 | By default all icons will be loaded from [`https://raw.githubusercontent.com/webdog/octicons-png/master/black/*`](https://github.com/webdog/octicons-png/tree/master/black). 74 | 75 | If you want to override the file, configure it with the `ICON_URL_PATH` and store the file 76 | [`icon-url-map.json`](icon-url-map.json) 77 | of this repository at a different place next to the storage. 78 | 79 | ## Tricky Implementation Details 80 | 81 | - Jira: Jira shows only the last 5 (remote) links for an issue 82 | - Thus you shouldn't add too many merge requests to one Ticket 83 | - GitLab: If on first push the merge is conflicted: the merge event does not appear! 84 | - Thus we fetch the merge request again as soon as a pipeline finishes. 85 | 86 | ## License 87 | 88 | This work is copyright by DracoBlue () and licensed under the terms of MIT License. 89 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitlab-jira-webhook", 3 | "env": { 4 | 5 | "GITLAB_BASE_URL": { 6 | "description": "Configure the base url for gitlab (without trailing slash).", 7 | "required": true, 8 | "order": 1 9 | }, 10 | "GITLAB_PERSONAL_ACCESS_TOKEN": { 11 | "description": "Configure the access token to access gitlab.", 12 | "required": true, 13 | "order": 2 14 | }, 15 | "JIRA_BASE_URL": { 16 | "description": "Configure the base url for jira (without trailing slash).", 17 | "required": true, 18 | "order": 3 19 | }, 20 | "JIRA_USERNAME": { 21 | "description": "Configure the username of the jira account.", 22 | "required": true, 23 | "order": 4 24 | }, 25 | "JIRA_PASSWORD": { 26 | "description": "Configure the password of the jira account.", 27 | "required": true, 28 | "order": 5 29 | }, 30 | "GITLAB_WEBHOOK_TOKEN": { 31 | "description": "The secret token, which will be configured on gitlab to make the connection possible.", 32 | "generator": "secret", 33 | "order": 7 34 | } 35 | }, 36 | "options": { 37 | "allow-unauthenticated": true, 38 | "max-instances": 1 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | node-cli: 4 | image: node:10.10.0-slim 5 | command: bash 6 | working_dir: /usr/src/app 7 | volumes: 8 | - './:/usr/src/app' 9 | nodemon: 10 | image: node:10.10.0-slim 11 | command: node_modules/.bin/nodemon src/server.js -w "src/" 12 | working_dir: /usr/src/app 13 | environment: 14 | - 'DEBUG=bridge:*' 15 | volumes: 16 | - './:/usr/src/app' 17 | ports: 18 | - "80:3000" 19 | ngrok: 20 | image: wernight/ngrok 21 | command: ngrok http nodemon:3000 22 | depends_on: 23 | - nodemon 24 | ports: 25 | - "4040" 26 | -------------------------------------------------------------------------------- /icon-url-map.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": "https://raw.githubusercontent.com/webdog/octicons-png/master/black/git-pull-request.png", 3 | "closed": "https://raw.githubusercontent.com/webdog/octicons-png/master/black/circle-slash.png", 4 | "locked": "https://raw.githubusercontent.com/webdog/octicons-png/master/black/lock.png", 5 | "merged": "https://raw.githubusercontent.com/webdog/octicons-png/master/black/git-merge.png", 6 | "unchecked": "https://raw.githubusercontent.com/webdog/octicons-png/master/black/clock.png", 7 | "can_be_merged": "https://raw.githubusercontent.com/webdog/octicons-png/master/black/checklist.png", 8 | "cannot_be_merged": "https://raw.githubusercontent.com/webdog/octicons-png/master/black/alert.png", 9 | "pending": "https://raw.githubusercontent.com/webdog/octicons-png/master/black/clock.png", 10 | "running": "https://raw.githubusercontent.com/webdog/octicons-png/master/black/server.png", 11 | "failed": "https://raw.githubusercontent.com/webdog/octicons-png/master/black/stop.png" 12 | } -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after the first failure 9 | // bail: false, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/tmp/jest_0", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | // clearMocks: false, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | // collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: null, 25 | 26 | // The directory where Jest should output its coverage files 27 | // coverageDirectory: null, 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "/node_modules/" 32 | // ], 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: null, 44 | 45 | // Make calling deprecated APIs throw helpful error messages 46 | // errorOnDeprecated: false, 47 | 48 | // Force coverage collection from ignored files usin a array of glob patterns 49 | // forceCoverageMatch: [], 50 | 51 | // A path to a module which exports an async function that is triggered once before all test suites 52 | // globalSetup: null, 53 | 54 | // A path to a module which exports an async function that is triggered once after all test suites 55 | // globalTeardown: null, 56 | 57 | // A set of global variables that need to be available in all test environments 58 | // globals: {}, 59 | 60 | // An array of directory names to be searched recursively up from the requiring module's location 61 | // moduleDirectories: [ 62 | // "node_modules" 63 | // ], 64 | 65 | // An array of file extensions your modules use 66 | // moduleFileExtensions: [ 67 | // "js", 68 | // "json", 69 | // "jsx", 70 | // "node" 71 | // ], 72 | 73 | // A map from regular expressions to module names that allow to stub out resources with a single module 74 | // moduleNameMapper: {}, 75 | 76 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 77 | // modulePathIgnorePatterns: [], 78 | 79 | // Activates notifications for test results 80 | // notify: false, 81 | 82 | // An enum that specifies notification mode. Requires { notify: true } 83 | // notifyMode: "always", 84 | 85 | // A preset that is used as a base for Jest's configuration 86 | // preset: null, 87 | 88 | // Run tests from one or more projects 89 | // projects: null, 90 | 91 | // Use this configuration option to add custom reporters to Jest 92 | // reporters: undefined, 93 | 94 | // Automatically reset mock state between every test 95 | // resetMocks: false, 96 | 97 | // Reset the module registry before running each individual test 98 | // resetModules: false, 99 | 100 | // A path to a custom resolver 101 | // resolver: null, 102 | 103 | // Automatically restore mock state between every test 104 | // restoreMocks: false, 105 | 106 | // The root directory that Jest should scan for tests and modules within 107 | // rootDir: null, 108 | 109 | // A list of paths to directories that Jest should use to search for files in 110 | // roots: [ 111 | // "" 112 | // ], 113 | 114 | // Allows you to use a custom runner instead of Jest's default test runner 115 | // runner: "jest-runner", 116 | 117 | // The paths to modules that run some code to configure or set up the testing environment before each test 118 | // setupFiles: [], 119 | 120 | // The path to a module that runs some code to configure or set up the testing framework before each test 121 | // setupTestFrameworkScriptFile: null, 122 | 123 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 124 | // snapshotSerializers: [], 125 | 126 | // The test environment that will be used for testing 127 | testEnvironment: "node", 128 | 129 | // Options that will be passed to the testEnvironment 130 | // testEnvironmentOptions: {}, 131 | 132 | // Adds a location field to test results 133 | // testLocationInResults: false, 134 | 135 | // The glob patterns Jest uses to detect test files 136 | // testMatch: [ 137 | // "**/__tests__/**/*.js?(x)", 138 | // "**/?(*.)+(spec|test).js?(x)" 139 | // ], 140 | 141 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 142 | // testPathIgnorePatterns: [ 143 | // "/node_modules/" 144 | // ], 145 | 146 | // The regexp pattern Jest uses to detect test files 147 | // testRegex: "", 148 | 149 | // This option allows the use of a custom results processor 150 | // testResultsProcessor: null, 151 | 152 | // This option allows use of a custom test runner 153 | // testRunner: "jasmine2", 154 | 155 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 156 | // testURL: "http://localhost", 157 | 158 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 159 | // timers: "real", 160 | 161 | // A map from regular expressions to paths to transformers 162 | // transform: null, 163 | 164 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 165 | // transformIgnorePatterns: [ 166 | // "/node_modules/" 167 | // ], 168 | 169 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 170 | // unmockedModulePathPatterns: undefined, 171 | 172 | // Indicates whether each individual test should be reported during the run 173 | // verbose: null, 174 | 175 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 176 | // watchPathIgnorePatterns: [], 177 | 178 | // Whether to use watchman for file crawling 179 | // watchman: true, 180 | }; 181 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitlab-jira-webhook", 3 | "private": true, 4 | "description": "", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "start": "node src/server.js" 9 | }, 10 | "engines": { 11 | "node": "~10.10" 12 | }, 13 | "author": "DracoBlue ", 14 | "license": "MIT", 15 | "dependencies": { 16 | "body-parser": "^1.18.3", 17 | "date-fns": "^1.29.0", 18 | "debug": "^4.0.1", 19 | "dotenv": "^6.0.0", 20 | "express": "^4.16.3", 21 | "got": "^9.2.1", 22 | "nodemon": "^1.18.4", 23 | "tunnel": "0.0.6" 24 | }, 25 | "devDependencies": { 26 | "jest": "^24.9.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DracoBlue/gitlab-jira-webhook/1e8b37726b479a89c312ec8a1d0eabd1f08866f9/screenshot.png -------------------------------------------------------------------------------- /src/GitlabEvent.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('bridge:GitlabEvent'); 2 | 3 | 4 | class GitlabEvent { 5 | 6 | constructor () { 7 | debug('created') 8 | } 9 | 10 | parseEvent (rawEvent) { 11 | debug('print') 12 | } 13 | 14 | } 15 | 16 | module.exports = GitlabEvent; -------------------------------------------------------------------------------- /src/GitlabEventParser.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('bridge:GitlabEventParser'); 2 | var GitlabEvent = require('./GitlabEvent'); 3 | 4 | class GitlabEventParser { 5 | 6 | constructor () { 7 | debug('created') 8 | } 9 | 10 | parseEvent (rawEvent) { 11 | debug('parse ', rawEvent.object_kind); 12 | 13 | let gitlabEvent = new GitlabEvent(); 14 | 15 | gitlabEvent.type = rawEvent.object_kind; 16 | gitlabEvent.targetProjectId = rawEvent.object_attributes.target.id; 17 | gitlabEvent.targetPath = rawEvent.object_attributes.target.path_with_namespace; 18 | gitlabEvent.targetUrl = rawEvent.object_attributes.target.web_url; 19 | gitlabEvent.targetBranch = rawEvent.object_attributes.target_branch; 20 | gitlabEvent.sourceBranch = rawEvent.object_attributes.source_branch; 21 | gitlabEvent.mergeTitle = rawEvent.object_attributes.title; 22 | gitlabEvent.mergeDescription = rawEvent.object_attributes.description; 23 | gitlabEvent.mergeStatus = rawEvent.object_attributes.merge_status; 24 | gitlabEvent.mergeId = rawEvent.object_attributes.id; 25 | gitlabEvent.mergeState = rawEvent.object_attributes.state; 26 | gitlabEvent.updatedAt = rawEvent.object_attributes.updated_at; 27 | gitlabEvent.mergeUrl = rawEvent.object_attributes.url; 28 | gitlabEvent.pipelineId = rawEvent.object_attributes.head_pipeline_id; 29 | gitlabEvent.pipelineBadgeUrl = rawEvent.object_attributes.source.web_url + "/badges/" + gitlabEvent.sourceBranch + "/pipeline.svg"; 30 | gitlabEvent.shortcut = "!" + rawEvent.object_attributes.iid; 31 | gitlabEvent.isWorkInProgress = rawEvent.object_attributes.work_in_progress; 32 | 33 | return gitlabEvent; 34 | } 35 | 36 | parseMergeRequestAndTargetProject (rawMergeRequest, rawTargetProject) { 37 | let gitlabEvent = new GitlabEvent(); 38 | 39 | gitlabEvent.type = "merge_request"; 40 | gitlabEvent.targetProjectId = rawMergeRequest.project_id; 41 | gitlabEvent.targetPath = rawTargetProject.path_with_namespace; 42 | gitlabEvent.targetUrl = rawTargetProject.web_url; 43 | gitlabEvent.targetBranch = rawMergeRequest.target_branch; 44 | gitlabEvent.sourceBranch = rawMergeRequest.source_branch; 45 | gitlabEvent.mergeTitle = rawMergeRequest.title; 46 | gitlabEvent.mergeDescription = rawMergeRequest.description; 47 | gitlabEvent.mergeStatus = rawMergeRequest.merge_status; 48 | gitlabEvent.mergeId = rawMergeRequest.id; 49 | gitlabEvent.mergeState = rawMergeRequest.state; 50 | gitlabEvent.updatedAt = rawMergeRequest.updated_at; 51 | gitlabEvent.mergeUrl = rawMergeRequest.web_url; 52 | if (rawMergeRequest.pipeline) { 53 | gitlabEvent.pipelineId = rawMergeRequest.pipeline.id; 54 | gitlabEvent.pipelineStatus = rawMergeRequest.pipeline.status; 55 | gitlabEvent.pipelineUrl = rawTargetProject.web_url + "/pipelines/" + gitlabEvent.pipelineId; 56 | } 57 | gitlabEvent.shortcut = "!" + rawMergeRequest.iid; 58 | gitlabEvent.isWorkInProgress = rawMergeRequest.work_in_progress; 59 | 60 | return gitlabEvent; 61 | } 62 | 63 | } 64 | 65 | module.exports = GitlabEventParser; -------------------------------------------------------------------------------- /src/GitlabEventParser.test.js: -------------------------------------------------------------------------------- 1 | var GitlabEventParser = require('./GitlabEventParser'); 2 | var parser = new GitlabEventParser(); 3 | 4 | test('parseEvent merged mergeRequest', () => { 5 | var rawData = { 6 | "object_kind": "merge_request", 7 | "event_type": "merge_request", 8 | "user": { 9 | "name": "Mister Example", 10 | "username": "example", 11 | "avatar_url": "https://secure.gravatar.com/avatar/d733ac42482dc89c17449f4ea15d758c?s=80&d=identicon" 12 | }, 13 | "project": { 14 | "id": 1422, 15 | "name": "jira-test", 16 | "description": "", 17 | "web_url": "https://git.example.org/example/jira-test", 18 | "avatar_url": null, 19 | "git_ssh_url": "git@git.example.org:example/jira-test.git", 20 | "git_http_url": "https://git.example.org/example/jira-test.git", 21 | "namespace": "example", 22 | "visibility_level": 0, 23 | "path_with_namespace": "example/jira-test", 24 | "default_branch": "master", 25 | "ci_config_path": null, 26 | "homepage": "https://git.example.org/example/jira-test", 27 | "url": "git@git.example.org:example/jira-test.git", 28 | "ssh_url": "git@git.example.org:example/jira-test.git", 29 | "http_url": "https://git.example.org/example/jira-test.git" 30 | }, 31 | "object_attributes": { 32 | "assignee_id": null, 33 | "author_id": 6, 34 | "created_at": "2018-09-16 07:21:27 UTC", 35 | "description": "Closes TEST-12345", 36 | "head_pipeline_id": 38545, 37 | "id": 10842, 38 | "iid": 4, 39 | "last_edited_at": null, 40 | "last_edited_by_id": null, 41 | "merge_commit_sha": "0843bcb100df0f3bf48ea451d009ba98c5c620d5", 42 | "merge_error": null, 43 | "merge_params": { 44 | "force_remove_source_branch": "1", 45 | "should_remove_source_branch": true, 46 | "commit_message": "Merge branch 'feature/TEST-12345-gitlab-ci-test' into 'master'\n\nUpdated\n\nCloses TEST-12345\n\nSee merge request example/jira-test!4", 47 | "squash": false 48 | }, 49 | "merge_status": "can_be_merged", 50 | "merge_user_id": 6, 51 | "merge_when_pipeline_succeeds": true, 52 | "milestone_id": null, 53 | "source_branch": "feature/TEST-12345-gitlab-ci-test", 54 | "source_project_id": 1422, 55 | "state": "merged", 56 | "target_branch": "master", 57 | "target_project_id": 1422, 58 | "time_estimate": 0, 59 | "title": "Updated", 60 | "updated_at": "2018-09-16 07:21:38 UTC", 61 | "updated_by_id": null, 62 | "url": "https://git.example.org/example/jira-test/merge_requests/4", 63 | "source": { 64 | "id": 1422, 65 | "name": "jira-test", 66 | "description": "", 67 | "web_url": "https://git.example.org/example/jira-test", 68 | "avatar_url": null, 69 | "git_ssh_url": "git@git.example.org:example/jira-test.git", 70 | "git_http_url": "https://git.example.org/example/jira-test.git", 71 | "namespace": "example", 72 | "visibility_level": 0, 73 | "path_with_namespace": "example/jira-test", 74 | "default_branch": "master", 75 | "ci_config_path": null, 76 | "homepage": "https://git.example.org/example/jira-test", 77 | "url": "git@git.example.org:example/jira-test.git", 78 | "ssh_url": "git@git.example.org:example/jira-test.git", 79 | "http_url": "https://git.example.org/example/jira-test.git" 80 | }, 81 | "target": { 82 | "id": 1422, 83 | "name": "jira-test", 84 | "description": "", 85 | "web_url": "https://git.example.org/example/jira-test", 86 | "avatar_url": null, 87 | "git_ssh_url": "git@git.example.org:example/jira-test.git", 88 | "git_http_url": "https://git.example.org/example/jira-test.git", 89 | "namespace": "example", 90 | "visibility_level": 0, 91 | "path_with_namespace": "example/jira-test", 92 | "default_branch": "master", 93 | "ci_config_path": null, 94 | "homepage": "https://git.example.org/example/jira-test", 95 | "url": "git@git.example.org:example/jira-test.git", 96 | "ssh_url": "git@git.example.org:example/jira-test.git", 97 | "http_url": "https://git.example.org/example/jira-test.git" 98 | }, 99 | "last_commit": { 100 | "id": "7987c294759a41363680fd7bc75d0a1b0d818e66", 101 | "message": "Updated\n", 102 | "timestamp": "2018-09-16T07:25:21Z", 103 | "url": "https://git.example.org/example/jira-test/commit/7987c294759a41363680fd7bc75d0a1b0d818e66", 104 | "author": { 105 | "name": "Mister Example", 106 | "email": "example@example.org" 107 | } 108 | }, 109 | "work_in_progress": false, 110 | "total_time_spent": 0, 111 | "human_total_time_spent": null, 112 | "human_time_estimate": null, 113 | "action": "merge" 114 | }, 115 | "labels": [], 116 | "changes": { 117 | "state": { 118 | "previous": "locked", 119 | "current": "merged" 120 | }, 121 | "updated_at": { 122 | "previous": "2018-09-16 07:21:38 UTC", 123 | "current": "2018-09-16 07:21:38 UTC" 124 | }, 125 | "total_time_spent": { 126 | "previous": null, 127 | "current": 0 128 | } 129 | }, 130 | "repository": { 131 | "name": "jira-test", 132 | "url": "git@git.example.org:example/jira-test.git", 133 | "description": "", 134 | "homepage": "https://git.example.org/example/jira-test" 135 | } 136 | }; 137 | 138 | const gitlabEvent = parser.parseEvent(rawData); 139 | 140 | let rawResponse = { 141 | "type": "merge_request", 142 | "shortcut": "!4", 143 | "targetProjectId": 1422, 144 | "targetPath": "example/jira-test", 145 | "targetUrl": "https://git.example.org/example/jira-test", 146 | "targetBranch": "master", 147 | "sourceBranch": "feature/TEST-12345-gitlab-ci-test", 148 | "mergeTitle": "Updated", 149 | "mergeDescription": "Closes TEST-12345", 150 | "mergeStatus": "can_be_merged", 151 | "mergeId": 10842, 152 | "mergeState": "merged", 153 | "isWorkInProgress": false, 154 | "updatedAt": "2018-09-16 07:21:38 UTC", 155 | "mergeUrl": "https://git.example.org/example/jira-test/merge_requests/4", 156 | "pipelineBadgeUrl": "https://git.example.org/example/jira-test/badges/feature/TEST-12345-gitlab-ci-test/pipeline.svg", 157 | "pipelineId": 38545, 158 | }; 159 | 160 | expect(gitlabEvent).toEqual(rawResponse); 161 | }); 162 | 163 | test('parseEvent closed mergeRequest', () => { 164 | var rawData = { 165 | "object_kind": "merge_request", 166 | "event_type": "merge_request", 167 | "user": { 168 | "name": "Mister Example", 169 | "username": "example", 170 | "avatar_url": "https://secure.gravatar.com/avatar/d733ac42482dc89c17449f4ea15d758c?s=80&d=identicon" 171 | }, 172 | "project": { 173 | "id": 1422, 174 | "name": "jira-test", 175 | "description": "", 176 | "web_url": "https://git.example.org/example/jira-test", 177 | "avatar_url": null, 178 | "git_ssh_url": "git@git.example.org:example/jira-test.git", 179 | "git_http_url": "https://git.example.org/example/jira-test.git", 180 | "namespace": "example", 181 | "visibility_level": 0, 182 | "path_with_namespace": "example/jira-test", 183 | "default_branch": "master", 184 | "ci_config_path": null, 185 | "homepage": "https://git.example.org/example/jira-test", 186 | "url": "git@git.example.org:example/jira-test.git", 187 | "ssh_url": "git@git.example.org:example/jira-test.git", 188 | "http_url": "https://git.example.org/example/jira-test.git" 189 | }, 190 | "object_attributes": { 191 | "assignee_id": null, 192 | "author_id": 6, 193 | "created_at": "2018-09-16 06:27:21 UTC", 194 | "description": "Closes TEST-12345", 195 | "head_pipeline_id": 38532, 196 | "id": 10840, 197 | "iid": 2, 198 | "last_edited_at": null, 199 | "last_edited_by_id": null, 200 | "merge_commit_sha": null, 201 | "merge_error": null, 202 | "merge_params": { 203 | "force_remove_source_branch": "1" 204 | }, 205 | "merge_status": "can_be_merged", 206 | "merge_user_id": null, 207 | "merge_when_pipeline_succeeds": false, 208 | "milestone_id": null, 209 | "source_branch": "feature/TEST-12345-canceled-test", 210 | "source_project_id": 1422, 211 | "state": "closed", 212 | "target_branch": "master", 213 | "target_project_id": 1422, 214 | "time_estimate": 0, 215 | "title": "Canceled test", 216 | "updated_at": "2018-09-16 06:33:17 UTC", 217 | "updated_by_id": null, 218 | "url": "https://git.example.org/example/jira-test/merge_requests/2", 219 | "source": { 220 | "id": 1422, 221 | "name": "jira-test", 222 | "description": "", 223 | "web_url": "https://git.example.org/example/jira-test", 224 | "avatar_url": null, 225 | "git_ssh_url": "git@git.example.org:example/jira-test.git", 226 | "git_http_url": "https://git.example.org/example/jira-test.git", 227 | "namespace": "example", 228 | "visibility_level": 0, 229 | "path_with_namespace": "example/jira-test", 230 | "default_branch": "master", 231 | "ci_config_path": null, 232 | "homepage": "https://git.example.org/example/jira-test", 233 | "url": "git@git.example.org:example/jira-test.git", 234 | "ssh_url": "git@git.example.org:example/jira-test.git", 235 | "http_url": "https://git.example.org/example/jira-test.git" 236 | }, 237 | "target": { 238 | "id": 1422, 239 | "name": "jira-test", 240 | "description": "", 241 | "web_url": "https://git.example.org/example/jira-test", 242 | "avatar_url": null, 243 | "git_ssh_url": "git@git.example.org:example/jira-test.git", 244 | "git_http_url": "https://git.example.org/example/jira-test.git", 245 | "namespace": "example", 246 | "visibility_level": 0, 247 | "path_with_namespace": "example/jira-test", 248 | "default_branch": "master", 249 | "ci_config_path": null, 250 | "homepage": "https://git.example.org/example/jira-test", 251 | "url": "git@git.example.org:example/jira-test.git", 252 | "ssh_url": "git@git.example.org:example/jira-test.git", 253 | "http_url": "https://git.example.org/example/jira-test.git" 254 | }, 255 | "last_commit": { 256 | "id": "f7c147ce8d7c02fe4070af052296ac39543855dc", 257 | "message": "Canceled test\n", 258 | "timestamp": "2018-09-16T06:30:30Z", 259 | "url": "https://git.example.org/example/jira-test/commit/f7c147ce8d7c02fe4070af052296ac39543855dc", 260 | "author": { 261 | "name": "Mister Example", 262 | "email": "example@example.org" 263 | } 264 | }, 265 | "work_in_progress": false, 266 | "total_time_spent": 0, 267 | "human_total_time_spent": null, 268 | "human_time_estimate": null, 269 | "action": "close" 270 | }, 271 | "labels": [], 272 | "changes": { 273 | "state": { 274 | "previous": "opened", 275 | "current": "closed" 276 | }, 277 | "updated_at": { 278 | "previous": "2018-09-16 06:27:21 UTC", 279 | "current": "2018-09-16 06:33:17 UTC" 280 | }, 281 | "total_time_spent": { 282 | "previous": null, 283 | "current": 0 284 | } 285 | }, 286 | "repository": { 287 | "name": "jira-test", 288 | "url": "git@git.example.org:example/jira-test.git", 289 | "description": "", 290 | "homepage": "https://git.example.org/example/jira-test" 291 | } 292 | }; 293 | 294 | const gitlabEvent = parser.parseEvent(rawData); 295 | 296 | let rawResponse = { 297 | "type": "merge_request", 298 | "shortcut": "!2", 299 | "targetProjectId": 1422, 300 | "targetPath": "example/jira-test", 301 | "targetUrl": "https://git.example.org/example/jira-test", 302 | "targetBranch": "master", 303 | "sourceBranch": "feature/TEST-12345-canceled-test", 304 | "mergeTitle": "Canceled test", 305 | "mergeDescription": "Closes TEST-12345", 306 | "mergeStatus": "can_be_merged", 307 | "mergeId": 10840, 308 | "mergeState": "closed", 309 | "isWorkInProgress": false, 310 | "updatedAt": "2018-09-16 06:33:17 UTC", 311 | "mergeUrl": "https://git.example.org/example/jira-test/merge_requests/2", 312 | "pipelineBadgeUrl": "https://git.example.org/example/jira-test/badges/feature/TEST-12345-canceled-test/pipeline.svg", 313 | "pipelineId": 38532, 314 | }; 315 | 316 | expect(gitlabEvent).toEqual(rawResponse); 317 | }); 318 | 319 | test('parseEvent open mergeRequest', () => { 320 | var rawData = { 321 | "object_kind": "merge_request", 322 | "event_type": "merge_request", 323 | "user": { 324 | "name": "Mister Example", 325 | "username": "example", 326 | "avatar_url": "https://secure.gravatar.com/avatar/d733ac42482dc89c17449f4ea15d758c?s=80&d=identicon" 327 | }, 328 | "project": { 329 | "id": 1422, 330 | "name": "jira-test", 331 | "description": "", 332 | "web_url": "https://git.example.org/example/jira-test", 333 | "avatar_url": null, 334 | "git_ssh_url": "git@git.example.org:example/jira-test.git", 335 | "git_http_url": "https://git.example.org/example/jira-test.git", 336 | "namespace": "example", 337 | "visibility_level": 0, 338 | "path_with_namespace": "example/jira-test", 339 | "default_branch": "master", 340 | "ci_config_path": null, 341 | "homepage": "https://git.example.org/example/jira-test", 342 | "url": "git@git.example.org:example/jira-test.git", 343 | "ssh_url": "git@git.example.org:example/jira-test.git", 344 | "http_url": "https://git.example.org/example/jira-test.git" 345 | }, 346 | "object_attributes": { 347 | "assignee_id": null, 348 | "author_id": 6, 349 | "created_at": "2018-09-16 06:50:00 UTC", 350 | "description": "Closes TEST-12345", 351 | "head_pipeline_id": 38537, 352 | "id": 10841, 353 | "iid": 3, 354 | "last_edited_at": null, 355 | "last_edited_by_id": null, 356 | "merge_commit_sha": null, 357 | "merge_error": null, 358 | "merge_params": { 359 | "force_remove_source_branch": "1" 360 | }, 361 | "merge_status": "unchecked", 362 | "merge_user_id": null, 363 | "merge_when_pipeline_succeeds": false, 364 | "milestone_id": null, 365 | "source_branch": "feature/TEST-12345-open-test", 366 | "source_project_id": 1422, 367 | "state": "opened", 368 | "target_branch": "master", 369 | "target_project_id": 1422, 370 | "time_estimate": 0, 371 | "title": "Open MR", 372 | "updated_at": "2018-09-16 06:50:00 UTC", 373 | "updated_by_id": null, 374 | "url": "https://git.example.org/example/jira-test/merge_requests/3", 375 | "source": { 376 | "id": 1422, 377 | "name": "jira-test", 378 | "description": "", 379 | "web_url": "https://git.example.org/example/jira-test", 380 | "avatar_url": null, 381 | "git_ssh_url": "git@git.example.org:example/jira-test.git", 382 | "git_http_url": "https://git.example.org/example/jira-test.git", 383 | "namespace": "example", 384 | "visibility_level": 0, 385 | "path_with_namespace": "example/jira-test", 386 | "default_branch": "master", 387 | "ci_config_path": null, 388 | "homepage": "https://git.example.org/example/jira-test", 389 | "url": "git@git.example.org:example/jira-test.git", 390 | "ssh_url": "git@git.example.org:example/jira-test.git", 391 | "http_url": "https://git.example.org/example/jira-test.git" 392 | }, 393 | "target": { 394 | "id": 1422, 395 | "name": "jira-test", 396 | "description": "", 397 | "web_url": "https://git.example.org/example/jira-test", 398 | "avatar_url": null, 399 | "git_ssh_url": "git@git.example.org:example/jira-test.git", 400 | "git_http_url": "https://git.example.org/example/jira-test.git", 401 | "namespace": "example", 402 | "visibility_level": 0, 403 | "path_with_namespace": "example/jira-test", 404 | "default_branch": "master", 405 | "ci_config_path": null, 406 | "homepage": "https://git.example.org/example/jira-test", 407 | "url": "git@git.example.org:example/jira-test.git", 408 | "ssh_url": "git@git.example.org:example/jira-test.git", 409 | "http_url": "https://git.example.org/example/jira-test.git" 410 | }, 411 | "last_commit": { 412 | "id": "493821d87c1ec75224c0a732020955fa69ac75bd", 413 | "message": "Open MR\n", 414 | "timestamp": "2018-09-16T06:53:52Z", 415 | "url": "https://git.example.org/example/jira-test/commit/493821d87c1ec75224c0a732020955fa69ac75bd", 416 | "author": { 417 | "name": "Mister Example", 418 | "email": "example@example.org" 419 | } 420 | }, 421 | "work_in_progress": false, 422 | "total_time_spent": 0, 423 | "human_total_time_spent": null, 424 | "human_time_estimate": null, 425 | "action": "open" 426 | }, 427 | "labels": [], 428 | "changes": { 429 | "head_pipeline_id": { 430 | "previous": null, 431 | "current": 38537 432 | }, 433 | "updated_at": { 434 | "previous": "2018-09-16 06:50:00 UTC", 435 | "current": "2018-09-16 06:50:00 UTC" 436 | }, 437 | "total_time_spent": { 438 | "previous": null, 439 | "current": 0 440 | } 441 | }, 442 | "repository": { 443 | "name": "jira-test", 444 | "url": "git@git.example.org:example/jira-test.git", 445 | "description": "", 446 | "homepage": "https://git.example.org/example/jira-test" 447 | } 448 | }; 449 | 450 | const gitlabEvent = parser.parseEvent(rawData); 451 | 452 | let rawResponse = { 453 | "type": "merge_request", 454 | "shortcut": "!3", 455 | "targetProjectId": 1422, 456 | "targetPath": "example/jira-test", 457 | "targetUrl": "https://git.example.org/example/jira-test", 458 | "targetBranch": "master", 459 | "sourceBranch": "feature/TEST-12345-open-test", 460 | "mergeTitle": "Open MR", 461 | "mergeDescription": "Closes TEST-12345", 462 | "mergeStatus": "unchecked", 463 | "mergeId": 10841, 464 | "mergeState": "opened", 465 | "isWorkInProgress": false, 466 | "updatedAt": "2018-09-16 06:50:00 UTC", 467 | "mergeUrl": "https://git.example.org/example/jira-test/merge_requests/3", 468 | "pipelineBadgeUrl": "https://git.example.org/example/jira-test/badges/feature/TEST-12345-open-test/pipeline.svg", 469 | "pipelineId": 38537, 470 | }; 471 | 472 | expect(gitlabEvent).toEqual(rawResponse); 473 | }); 474 | 475 | test('parseEvent conflict mergeRequest', () => { 476 | var rawData = { 477 | "object_kind": "merge_request", 478 | "event_type": "merge_request", 479 | "user": { 480 | "name": "Mister Example", 481 | "username": "example", 482 | "avatar_url": "https://secure.gravatar.com/avatar/d733ac42482dc89c17449f4ea15d758c?s=80&d=identicon" 483 | }, 484 | "project": { 485 | "id": 1422, 486 | "name": "jira-test", 487 | "description": "", 488 | "web_url": "https://git.example.org/example/jira-test", 489 | "avatar_url": null, 490 | "git_ssh_url": "git@git.example.org:example/jira-test.git", 491 | "git_http_url": "https://git.example.org/example/jira-test.git", 492 | "namespace": "example", 493 | "visibility_level": 0, 494 | "path_with_namespace": "example/jira-test", 495 | "default_branch": "master", 496 | "ci_config_path": null, 497 | "homepage": "https://git.example.org/example/jira-test", 498 | "url": "git@git.example.org:example/jira-test.git", 499 | "ssh_url": "git@git.example.org:example/jira-test.git", 500 | "http_url": "https://git.example.org/example/jira-test.git" 501 | }, 502 | "object_attributes": { 503 | "assignee_id": null, 504 | "author_id": 6, 505 | "created_at": "2018-09-16 07:39:33 UTC", 506 | "description": "Closes TEST-12345", 507 | "head_pipeline_id": 38550, 508 | "id": 10843, 509 | "iid": 5, 510 | "last_edited_at": "2018-09-16 07:40:45 UTC", 511 | "last_edited_by_id": 6, 512 | "merge_commit_sha": null, 513 | "merge_error": null, 514 | "merge_params": { 515 | "force_remove_source_branch": "1" 516 | }, 517 | "merge_status": "cannot_be_merged", 518 | "merge_user_id": null, 519 | "merge_when_pipeline_succeeds": false, 520 | "milestone_id": null, 521 | "source_branch": "feature/TEST-12345-conflicting-merge-request", 522 | "source_project_id": 1422, 523 | "state": "opened", 524 | "target_branch": "master", 525 | "target_project_id": 1422, 526 | "time_estimate": 0, 527 | "title": "Added conflict !", 528 | "updated_at": "2018-09-16 07:40:45 UTC", 529 | "updated_by_id": 6, 530 | "url": "https://git.example.org/example/jira-test/merge_requests/5", 531 | "source": { 532 | "id": 1422, 533 | "name": "jira-test", 534 | "description": "", 535 | "web_url": "https://git.example.org/example/jira-test", 536 | "avatar_url": null, 537 | "git_ssh_url": "git@git.example.org:example/jira-test.git", 538 | "git_http_url": "https://git.example.org/example/jira-test.git", 539 | "namespace": "example", 540 | "visibility_level": 0, 541 | "path_with_namespace": "example/jira-test", 542 | "default_branch": "master", 543 | "ci_config_path": null, 544 | "homepage": "https://git.example.org/example/jira-test", 545 | "url": "git@git.example.org:example/jira-test.git", 546 | "ssh_url": "git@git.example.org:example/jira-test.git", 547 | "http_url": "https://git.example.org/example/jira-test.git" 548 | }, 549 | "target": { 550 | "id": 1422, 551 | "name": "jira-test", 552 | "description": "", 553 | "web_url": "https://git.example.org/example/jira-test", 554 | "avatar_url": null, 555 | "git_ssh_url": "git@git.example.org:example/jira-test.git", 556 | "git_http_url": "https://git.example.org/example/jira-test.git", 557 | "namespace": "example", 558 | "visibility_level": 0, 559 | "path_with_namespace": "example/jira-test", 560 | "default_branch": "master", 561 | "ci_config_path": null, 562 | "homepage": "https://git.example.org/example/jira-test", 563 | "url": "git@git.example.org:example/jira-test.git", 564 | "ssh_url": "git@git.example.org:example/jira-test.git", 565 | "http_url": "https://git.example.org/example/jira-test.git" 566 | }, 567 | "last_commit": { 568 | "id": "23d4af0fe64d1c630d48d7f63ab66e74d6b5feb1", 569 | "message": "Added conflict\n", 570 | "timestamp": "2018-09-16T07:43:24Z", 571 | "url": "https://git.example.org/example/jira-test/commit/23d4af0fe64d1c630d48d7f63ab66e74d6b5feb1", 572 | "author": { 573 | "name": "Mister Example", 574 | "email": "example@example.org" 575 | } 576 | }, 577 | "work_in_progress": false, 578 | "total_time_spent": 0, 579 | "human_total_time_spent": null, 580 | "human_time_estimate": null, 581 | "action": "update" 582 | }, 583 | "labels": [], 584 | "changes": { 585 | "last_edited_at": { 586 | "previous": null, 587 | "current": "2018-09-16 07:40:45 UTC" 588 | }, 589 | "last_edited_by_id": { 590 | "previous": null, 591 | "current": 6 592 | }, 593 | "title": { 594 | "previous": "Added conflict", 595 | "current": "Added conflict !" 596 | }, 597 | "updated_at": { 598 | "previous": "2018-09-16 07:40:34 UTC", 599 | "current": "2018-09-16 07:40:45 UTC" 600 | }, 601 | "updated_by_id": { 602 | "previous": null, 603 | "current": 6 604 | } 605 | }, 606 | "repository": { 607 | "name": "jira-test", 608 | "url": "git@git.example.org:example/jira-test.git", 609 | "description": "", 610 | "homepage": "https://git.example.org/example/jira-test" 611 | } 612 | }; 613 | 614 | const gitlabEvent = parser.parseEvent(rawData); 615 | 616 | let rawResponse = { 617 | "type": "merge_request", 618 | "shortcut": "!5", 619 | "targetProjectId": 1422, 620 | "targetPath": "example/jira-test", 621 | "targetUrl": "https://git.example.org/example/jira-test", 622 | "targetBranch": "master", 623 | "sourceBranch": "feature/TEST-12345-conflicting-merge-request", 624 | "mergeTitle": "Added conflict !", 625 | "mergeDescription": "Closes TEST-12345", 626 | "mergeStatus": "cannot_be_merged", 627 | "mergeId": 10843, 628 | "mergeState": "opened", 629 | "isWorkInProgress": false, 630 | "updatedAt": "2018-09-16 07:40:45 UTC", 631 | "mergeUrl": "https://git.example.org/example/jira-test/merge_requests/5", 632 | "pipelineBadgeUrl": "https://git.example.org/example/jira-test/badges/feature/TEST-12345-conflicting-merge-request/pipeline.svg", 633 | "pipelineId": 38550, 634 | }; 635 | 636 | expect(gitlabEvent).toEqual(rawResponse); 637 | }); 638 | 639 | test('parseMergeRequestAndTargetProject', () => { 640 | var rawMergeRequestData = { 641 | "id": 10844, 642 | "iid": 6, 643 | "project_id": 1422, 644 | "title": "server test again", 645 | "description": "Closes TEST-12345", 646 | "state": "closed", 647 | "created_at": "2018-09-16T14:23:46.617Z", 648 | "updated_at": "2018-09-16T14:25:48.937Z", 649 | "target_branch": "master", 650 | "source_branch": "feature/TEST-12345-added-other-test", 651 | "upvotes": 0, 652 | "downvotes": 0, 653 | "author": { 654 | "id": 6, 655 | "name": "Mister Example", 656 | "username": "example", 657 | "state": "active", 658 | "avatar_url": "https://secure.gravatar.com/avatar/d733ac42482dc89c17449f4ea15d758c?s=80&d=identicon", 659 | "web_url": "https://git.example.org/example" 660 | }, 661 | "assignee": null, 662 | "source_project_id": 1422, 663 | "target_project_id": 1422, 664 | "labels": [ 665 | 666 | ], 667 | "work_in_progress": false, 668 | "milestone": null, 669 | "merge_when_pipeline_succeeds": false, 670 | "merge_status": "can_be_merged", 671 | "sha": "db65b6547005d154c23bdb42152577c2c6b8e10f", 672 | "merge_commit_sha": null, 673 | "user_notes_count": 0, 674 | "discussion_locked": null, 675 | "should_remove_source_branch": null, 676 | "force_remove_source_branch": true, 677 | "web_url": "https://git.example.org/example/jira-test/merge_requests/6", 678 | "time_stats": { 679 | "time_estimate": 0, 680 | "total_time_spent": 0, 681 | "human_time_estimate": null, 682 | "human_total_time_spent": null 683 | }, 684 | "squash": false 685 | }; 686 | 687 | var rawProjectData = { 688 | "id": 1422, 689 | "description": "", 690 | "name": "jira-test", 691 | "name_with_namespace": "Mister Example / jira-test", 692 | "path": "jira-test", 693 | "path_with_namespace": "example/jira-test", 694 | "created_at": "2018-09-14T09:04:08.368Z", 695 | "default_branch": "master", 696 | "tag_list": [ 697 | 698 | ], 699 | "ssh_url_to_repo": "git@git.example.org:example/jira-test.git", 700 | "http_url_to_repo": "https://git.example.org/example/jira-test.git", 701 | "web_url": "https://git.example.org/example/jira-test", 702 | "readme_url": "https://git.example.org/example/jira-test/blob/master/README.md", 703 | "avatar_url": null, 704 | "star_count": 0, 705 | "forks_count": 0, 706 | "last_activity_at": "2018-09-16T14:23:39.543Z", 707 | "_links": { 708 | "self": "https://git.example.org/api/v4/projects/1422", 709 | "issues": "https://git.example.org/api/v4/projects/1422/issues", 710 | "merge_requests": "https://git.example.org/api/v4/projects/1422/merge_requests", 711 | "repo_branches": "https://git.example.org/api/v4/projects/1422/repository/branches", 712 | "labels": "https://git.example.org/api/v4/projects/1422/labels", 713 | "events": "https://git.example.org/api/v4/projects/1422/events", 714 | "members": "https://git.example.org/api/v4/projects/1422/members" 715 | }, 716 | "archived": false, 717 | "visibility": "private", 718 | "owner": { 719 | "id": 6, 720 | "name": "Mister Example", 721 | "username": "example", 722 | "state": "active", 723 | "avatar_url": "https://secure.gravatar.com/avatar/d733ac42482dc89c17449f4ea15d758c?s=80&d=identicon", 724 | "web_url": "https://git.example.org/example" 725 | }, 726 | "resolve_outdated_diff_discussions": false, 727 | "container_registry_enabled": true, 728 | "issues_enabled": true, 729 | "merge_requests_enabled": true, 730 | "wiki_enabled": true, 731 | "jobs_enabled": true, 732 | "snippets_enabled": true, 733 | "shared_runners_enabled": true, 734 | "lfs_enabled": true, 735 | "creator_id": 6, 736 | "namespace": { 737 | "id": 6, 738 | "name": "example", 739 | "path": "example", 740 | "kind": "user", 741 | "full_path": "example", 742 | "parent_id": null 743 | }, 744 | "import_status": "none", 745 | "import_error": null, 746 | "open_issues_count": 0, 747 | "runners_token": "Ci_2tydXfDaJ5HKDt9q1", 748 | "public_jobs": true, 749 | "ci_config_path": null, 750 | "shared_with_groups": [ 751 | 752 | ], 753 | "only_allow_merge_if_pipeline_succeeds": false, 754 | "request_access_enabled": false, 755 | "only_allow_merge_if_all_discussions_are_resolved": false, 756 | "printing_merge_request_link_enabled": true, 757 | "merge_method": "merge", 758 | "permissions": { 759 | "project_access": { 760 | "access_level": 40, 761 | "notification_level": 3 762 | }, 763 | "group_access": null 764 | } 765 | }; 766 | 767 | const gitlabEvent = parser.parseMergeRequestAndTargetProject(rawMergeRequestData, rawProjectData); 768 | 769 | let rawResponse = { 770 | "type": "merge_request", 771 | "shortcut": "!6", 772 | "targetProjectId": 1422, 773 | "targetPath": "example/jira-test", 774 | "targetUrl": "https://git.example.org/example/jira-test", 775 | "targetBranch": "master", 776 | "sourceBranch": "feature/TEST-12345-added-other-test", 777 | "mergeTitle": "server test again", 778 | "mergeDescription": "Closes TEST-12345", 779 | "mergeStatus": "can_be_merged", 780 | "mergeId": 10844, 781 | "mergeState": "closed", 782 | "isWorkInProgress": false, 783 | "updatedAt": "2018-09-16T14:25:48.937Z", 784 | "mergeUrl": "https://git.example.org/example/jira-test/merge_requests/6" 785 | }; 786 | 787 | expect(gitlabEvent).toEqual(rawResponse); 788 | }); 789 | -------------------------------------------------------------------------------- /src/JiraRemoteLinkGenerator.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('bridge:JiraRemoteLinkGenerator'); 2 | var url = require('url'); 3 | var formatDate = require('date-fns').format; 4 | var fs = require('fs'); 5 | var iconUrlMap = JSON.parse(fs.readFileSync((process.env.ICON_URL_PATH || './') + 'icon-url-map.json')); 6 | 7 | class JiraRemoteLinkGenerator { 8 | 9 | constructor (newMode = false) { 10 | debug('created') 11 | this.newMode = newMode; 12 | } 13 | 14 | generateRemoteLinkFromEvent (event) { 15 | debug('generateRemoteLinkFromEvent') 16 | 17 | var urlParts = new URL(event.mergeUrl); 18 | urlParts.pathname = ''; 19 | urlParts.search = ''; 20 | 21 | var objectIconUrl = iconUrlMap["default"]; 22 | var objectTitle = "Open Merge Request"; 23 | 24 | if (event.mergeState === 'closed') { 25 | objectTitle = "Canceled Merge Request"; 26 | objectIconUrl = iconUrlMap['closed']; 27 | } 28 | 29 | if (event.mergeState === 'locked') { 30 | objectTitle = "Locked Merge Request"; 31 | objectIconUrl = iconUrlMap['locked']; 32 | } 33 | 34 | if (event.mergeState === 'merged') { 35 | objectIconUrl = iconUrlMap['merged']; 36 | objectTitle = "Merged Merge Request"; 37 | } 38 | 39 | var statusIconUrl = iconUrlMap['unchecked']; 40 | var statusTitle = 'Unchecked'; 41 | var statusUrl = event.mergeUrl; 42 | 43 | if (event.mergeStatus === 'can_be_merged') { 44 | statusIconUrl = iconUrlMap['can_be_merged']; 45 | statusTitle = 'Can be Merged'; 46 | } 47 | 48 | if (event.mergeStatus === 'cannot_be_merged') { 49 | statusIconUrl = iconUrlMap['cannot_be_merged']; 50 | statusTitle = 'Cannot be Merged'; 51 | } 52 | 53 | if (event.pipelineStatus && event.pipelineUrl) { 54 | if (event.pipelineStatus === "pending") { 55 | statusIconUrl = iconUrlMap['pending']; 56 | statusTitle = 'Pipeline pending'; 57 | statusUrl = event.pipelineUrl; 58 | } 59 | if (event.pipelineStatus === "running") { 60 | statusIconUrl = iconUrlMap['running']; 61 | statusTitle = 'Pipeline running'; 62 | statusUrl = event.pipelineUrl; 63 | } 64 | if (event.pipelineStatus === "failed") { 65 | statusIconUrl = iconUrlMap['failed']; 66 | statusTitle = 'Pipeline failed'; 67 | statusUrl = event.pipelineUrl; 68 | } 69 | } 70 | 71 | var title = event.targetPath + " " + event.shortcut + " " + event.mergeTitle; 72 | var summary = ""; 73 | var relationship = "Gitlab Merge Request"; 74 | 75 | if (this.newMode) { 76 | title = event.shortcut + " " + event.mergeTitle; 77 | summary = formatDate(new Date(event.updatedAt), 'YYYY/MM/DD HH:mm'); 78 | relationship = event.targetPath; 79 | } 80 | 81 | const jiraRemoteLink = { 82 | "globalId": "gitlabUrl=" + encodeURIComponent(urlParts.toString()) + "&projectId=" + event.targetProjectId + "&mergeId=" + event.mergeId, 83 | "application": { 84 | "type":"com.gitlab", 85 | "name":"Gitlab" 86 | }, 87 | "relationship": relationship, 88 | "object": { 89 | "url": event.mergeUrl, 90 | "title": title, 91 | "summary": summary, 92 | "icon": { 93 | "url16x16": objectIconUrl, 94 | "title":objectTitle 95 | }, 96 | "status": { 97 | "resolved": ['opened', 'locked'].indexOf(event.mergeState) === -1, 98 | "icon": { 99 | "url16x16": statusIconUrl, 100 | "title": statusTitle, 101 | "link": statusUrl 102 | } 103 | } 104 | } 105 | }; 106 | 107 | return jiraRemoteLink; 108 | } 109 | 110 | } 111 | 112 | module.exports = JiraRemoteLinkGenerator; -------------------------------------------------------------------------------- /src/JiraRemoteLinkGenerator.test.js: -------------------------------------------------------------------------------- 1 | var JiraRemoteLinkGenerator = require('./JiraRemoteLinkGenerator'); 2 | var jiraRemoteLinkGenerator = new JiraRemoteLinkGenerator(); 3 | 4 | test('generate merged mergeRequest', () => { 5 | 6 | let rawEvent = { 7 | "type": "merge_request", 8 | "shortcut": "!1", 9 | "targetProjectId": 1422, 10 | "targetPath": "example/jira-test", 11 | "targetUrl": "https://git.example.org/example/jira-test", 12 | "targetBranch": "master", 13 | "sourceBranch": "feature/TEST-12345-gitlab-ci-test", 14 | "mergeTitle": "Integration für TEST-12345 eingefügt", 15 | "mergeStatus": "can_be_merged", 16 | "mergeId": 10820, 17 | "mergeState": "merged", 18 | "isWorkInProgress": false, 19 | "updatedAt": "2018-09-14T09:08:01.240Z", 20 | "mergeUrl": "https://git.example.org/example/jira-test/merge_requests/1", 21 | "pipelineBadgeUrl": "https://git.example.org/example/jira-test/badges/feature/TEST-12345-gitlab-ci-test/pipeline.svg", 22 | "pipelineId": 38052, 23 | }; 24 | 25 | let rawResponse = jiraRemoteLinkGenerator.generateRemoteLinkFromEvent(rawEvent); 26 | 27 | const jiraRemoteLink = { 28 | "globalId": "gitlabUrl=https%3A%2F%2Fgit.example.org%2F&projectId=1422&mergeId=10820", 29 | "application": { 30 | "type":"com.gitlab", 31 | "name":"Gitlab" 32 | }, 33 | "relationship":"Gitlab Merge Request", 34 | "object": { 35 | "url": "https://git.example.org/example/jira-test/merge_requests/1", 36 | "title": "example/jira-test !1 Integration für TEST-12345 eingefügt", 37 | "summary": "", 38 | "icon": { 39 | "url16x16":"https://raw.githubusercontent.com/webdog/octicons-png/master/black/git-merge.png", 40 | "title":"Merged Merge Request" 41 | }, 42 | "status": { 43 | "resolved": true, 44 | "icon": { 45 | "url16x16":"https://raw.githubusercontent.com/webdog/octicons-png/master/black/checklist.png", 46 | "title":"Can be Merged", 47 | "link": "https://git.example.org/example/jira-test/merge_requests/1" 48 | } 49 | } 50 | } 51 | }; 52 | 53 | expect(rawResponse).toEqual(jiraRemoteLink); 54 | }); 55 | 56 | 57 | test('generate wip mergeRequest', () => { 58 | 59 | let rawEvent = { 60 | "type": "merge_request", 61 | "shortcut": "!1", 62 | "targetProjectId": 1422, 63 | "targetPath": "example/jira-test", 64 | "targetUrl": "https://git.example.org/example/jira-test", 65 | "targetBranch": "master", 66 | "sourceBranch": "feature/TEST-12345-gitlab-ci-test", 67 | "mergeTitle": "WIP: Integration für TEST-12345 eingefügt", 68 | "mergeStatus": "can_be_merged", 69 | "mergeId": 10820, 70 | "mergeState": "opened", 71 | "isWorkInProgress": true, 72 | "updatedAt": "2018-09-14T09:08:01.240Z", 73 | "mergeUrl": "https://git.example.org/example/jira-test/merge_requests/1", 74 | "pipelineBadgeUrl": "https://git.example.org/example/jira-test/badges/feature/TEST-12345-gitlab-ci-test/pipeline.svg", 75 | "pipelineId": 38052, 76 | }; 77 | 78 | let rawResponse = jiraRemoteLinkGenerator.generateRemoteLinkFromEvent(rawEvent); 79 | 80 | const jiraRemoteLink = { 81 | "globalId": "gitlabUrl=https%3A%2F%2Fgit.example.org%2F&projectId=1422&mergeId=10820", 82 | "application": { 83 | "type":"com.gitlab", 84 | "name":"Gitlab" 85 | }, 86 | "relationship":"Gitlab Merge Request", 87 | "object": { 88 | "url": "https://git.example.org/example/jira-test/merge_requests/1", 89 | "summary": "", 90 | "title": "example/jira-test !1 WIP: Integration für TEST-12345 eingefügt", 91 | "icon": { 92 | "url16x16":"https://raw.githubusercontent.com/webdog/octicons-png/master/black/git-pull-request.png", 93 | "title":"Open Merge Request" 94 | }, 95 | "status": { 96 | "resolved": false, 97 | "icon": { 98 | "url16x16":"https://raw.githubusercontent.com/webdog/octicons-png/master/black/checklist.png", 99 | "title":"Can be Merged", 100 | "link": "https://git.example.org/example/jira-test/merge_requests/1" 101 | } 102 | } 103 | } 104 | }; 105 | 106 | expect(rawResponse).toEqual(jiraRemoteLink); 107 | }); 108 | 109 | test('generate canceled mergeRequest', () => { 110 | 111 | let rawEvent = { 112 | "type": "merge_request", 113 | "shortcut": "!2", 114 | "targetProjectId": 1422, 115 | "targetPath": "example/jira-test", 116 | "targetUrl": "https://git.example.org/example/jira-test", 117 | "targetBranch": "master", 118 | "sourceBranch": "feature/TEST-12345-canceled-test", 119 | "mergeTitle": "Canceled test", 120 | "mergeStatus": "can_be_merged", 121 | "mergeId": 10840, 122 | "mergeState": "closed", 123 | "isWorkInProgress": false, 124 | "updatedAt": "2018-09-16 06:33:17 UTC", 125 | "mergeUrl": "https://git.example.org/example/jira-test/merge_requests/2", 126 | "pipelineBadgeUrl": "https://git.example.org/example/jira-test/badges/feature/TEST-12345-canceled-test/pipeline.svg", 127 | "pipelineId": 38532, 128 | }; 129 | 130 | let rawResponse = jiraRemoteLinkGenerator.generateRemoteLinkFromEvent(rawEvent); 131 | 132 | const jiraRemoteLink = { 133 | "globalId": "gitlabUrl=https%3A%2F%2Fgit.example.org%2F&projectId=1422&mergeId=10840", 134 | "application": { 135 | "type":"com.gitlab", 136 | "name":"Gitlab" 137 | }, 138 | "relationship":"Gitlab Merge Request", 139 | "object": { 140 | "url": "https://git.example.org/example/jira-test/merge_requests/2", 141 | "summary": "", 142 | "title": "example/jira-test !2 Canceled test", 143 | "icon": { 144 | "url16x16":"https://raw.githubusercontent.com/webdog/octicons-png/master/black/circle-slash.png", 145 | "title":"Canceled Merge Request" 146 | }, 147 | "status": { 148 | "resolved": true, 149 | "icon": { 150 | "url16x16":"https://raw.githubusercontent.com/webdog/octicons-png/master/black/checklist.png", 151 | "title":"Can be Merged", 152 | "link": "https://git.example.org/example/jira-test/merge_requests/2" 153 | } 154 | } 155 | } 156 | }; 157 | 158 | expect(rawResponse).toEqual(jiraRemoteLink); 159 | }); 160 | 161 | test('generate open mergeRequest', () => { 162 | 163 | let rawEvent = { 164 | "type": "merge_request", 165 | "shortcut": "!3", 166 | "targetProjectId": 1422, 167 | "targetPath": "example/jira-test", 168 | "targetUrl": "https://git.example.org/example/jira-test", 169 | "targetBranch": "master", 170 | "sourceBranch": "feature/TEST-12345-open-test", 171 | "mergeTitle": "Open MR", 172 | "mergeStatus": "unchecked", 173 | "mergeId": 10841, 174 | "mergeState": "opened", 175 | "isWorkInProgress": false, 176 | "updatedAt": "2018-09-16 06:50:00 UTC", 177 | "mergeUrl": "https://git.example.org/example/jira-test/merge_requests/3", 178 | "pipelineBadgeUrl": "https://git.example.org/example/jira-test/badges/feature/TEST-12345-open-test/pipeline.svg", 179 | "pipelineId": 38537, 180 | }; 181 | 182 | let rawResponse = jiraRemoteLinkGenerator.generateRemoteLinkFromEvent(rawEvent); 183 | 184 | const jiraRemoteLink = { 185 | "globalId": "gitlabUrl=https%3A%2F%2Fgit.example.org%2F&projectId=1422&mergeId=10841", 186 | "application": { 187 | "type":"com.gitlab", 188 | "name":"Gitlab" 189 | }, 190 | "relationship":"Gitlab Merge Request", 191 | "object": { 192 | "url": "https://git.example.org/example/jira-test/merge_requests/3", 193 | "summary": "", 194 | "title": "example/jira-test !3 Open MR", 195 | "icon": { 196 | "url16x16":"https://raw.githubusercontent.com/webdog/octicons-png/master/black/git-pull-request.png", 197 | "title":"Open Merge Request" 198 | }, 199 | "status": { 200 | "resolved": false, 201 | "icon": { 202 | "url16x16":"https://raw.githubusercontent.com/webdog/octicons-png/master/black/clock.png", 203 | "title":"Unchecked", 204 | "link": "https://git.example.org/example/jira-test/merge_requests/3" 205 | } 206 | } 207 | } 208 | }; 209 | 210 | expect(rawResponse).toEqual(jiraRemoteLink); 211 | }); 212 | 213 | test('generate conflicting mergeRequest', () => { 214 | 215 | let rawEvent = { 216 | "type": "merge_request", 217 | "shortcut": "!5", 218 | "targetProjectId": 1422, 219 | "targetPath": "example/jira-test", 220 | "targetUrl": "https://git.example.org/example/jira-test", 221 | "targetBranch": "master", 222 | "sourceBranch": "feature/TEST-12345-conflicting-merge-request", 223 | "mergeTitle": "Added conflict !", 224 | "mergeStatus": "cannot_be_merged", 225 | "mergeId": 10843, 226 | "mergeState": "opened", 227 | "isWorkInProgress": false, 228 | "updatedAt": "2018-09-16 07:40:45 UTC", 229 | "mergeUrl": "https://git.example.org/example/jira-test/merge_requests/5", 230 | "pipelineBadgeUrl": "https://git.example.org/example/jira-test/badges/feature/TEST-12345-conflicting-merge-request/pipeline.svg", 231 | "pipelineId": 38550, 232 | }; 233 | 234 | let rawResponse = jiraRemoteLinkGenerator.generateRemoteLinkFromEvent(rawEvent); 235 | 236 | const jiraRemoteLink = { 237 | "globalId": "gitlabUrl=https%3A%2F%2Fgit.example.org%2F&projectId=1422&mergeId=10843", 238 | "application": { 239 | "type":"com.gitlab", 240 | "name":"Gitlab" 241 | }, 242 | "relationship":"Gitlab Merge Request", 243 | "object": { 244 | "url": "https://git.example.org/example/jira-test/merge_requests/5", 245 | "summary": "", 246 | "title": "example/jira-test !5 Added conflict !", 247 | "icon": { 248 | "url16x16":"https://raw.githubusercontent.com/webdog/octicons-png/master/black/git-pull-request.png", 249 | "title":"Open Merge Request" 250 | }, 251 | "status": { 252 | "resolved": false, 253 | "icon": { 254 | "url16x16":"https://raw.githubusercontent.com/webdog/octicons-png/master/black/alert.png", 255 | "title":"Cannot be Merged", 256 | "link": "https://git.example.org/example/jira-test/merge_requests/5" 257 | } 258 | } 259 | } 260 | }; 261 | 262 | expect(rawResponse).toEqual(jiraRemoteLink); 263 | }); 264 | 265 | 266 | 267 | test('generate open mergeRequest with failed pipeline', () => { 268 | 269 | let rawEvent = { 270 | "type": "merge_request", 271 | "shortcut": "!3", 272 | "targetProjectId": 1422, 273 | "targetPath": "example/jira-test", 274 | "targetUrl": "https://git.example.org/example/jira-test", 275 | "targetBranch": "master", 276 | "sourceBranch": "feature/TEST-12345-open-test", 277 | "mergeTitle": "Open MR", 278 | "mergeStatus": "unchecked", 279 | "mergeId": 10841, 280 | "mergeState": "opened", 281 | "isWorkInProgress": false, 282 | "updatedAt": "2018-09-16 06:50:00 UTC", 283 | "mergeUrl": "https://git.example.org/example/jira-test/merge_requests/3", 284 | "pipelineBadgeUrl": "https://git.example.org/example/jira-test/badges/feature/TEST-12345-open-test/pipeline.svg", 285 | "pipelineId": 38537, 286 | "pipelineStatus": "failed", 287 | "pipelineUrl": "https://git.example.org/example/jira-test/pipelines/38537", 288 | }; 289 | 290 | let rawResponse = jiraRemoteLinkGenerator.generateRemoteLinkFromEvent(rawEvent); 291 | 292 | const jiraRemoteLink = { 293 | "globalId": "gitlabUrl=https%3A%2F%2Fgit.example.org%2F&projectId=1422&mergeId=10841", 294 | "application": { 295 | "type":"com.gitlab", 296 | "name":"Gitlab" 297 | }, 298 | "relationship":"Gitlab Merge Request", 299 | "object": { 300 | "url": "https://git.example.org/example/jira-test/merge_requests/3", 301 | "summary": "", 302 | "title": "example/jira-test !3 Open MR", 303 | "icon": { 304 | "url16x16":"https://raw.githubusercontent.com/webdog/octicons-png/master/black/git-pull-request.png", 305 | "title":"Open Merge Request" 306 | }, 307 | "status": { 308 | "resolved": false, 309 | "icon": { 310 | "url16x16":"https://raw.githubusercontent.com/webdog/octicons-png/master/black/stop.png", 311 | "title":"Pipeline failed", 312 | "link": "https://git.example.org/example/jira-test/pipelines/38537" 313 | } 314 | } 315 | } 316 | }; 317 | 318 | expect(rawResponse).toEqual(jiraRemoteLink); 319 | }); 320 | 321 | 322 | test('generate open mergeRequest with pending pipeline', () => { 323 | 324 | let rawEvent = { 325 | "type": "merge_request", 326 | "shortcut": "!3", 327 | "targetProjectId": 1422, 328 | "targetPath": "example/jira-test", 329 | "targetUrl": "https://git.example.org/example/jira-test", 330 | "targetBranch": "master", 331 | "sourceBranch": "feature/TEST-12345-open-test", 332 | "mergeTitle": "Open MR", 333 | "mergeStatus": "unchecked", 334 | "mergeId": 10841, 335 | "mergeState": "opened", 336 | "isWorkInProgress": false, 337 | "updatedAt": "2018-09-16 06:50:00 UTC", 338 | "mergeUrl": "https://git.example.org/example/jira-test/merge_requests/3", 339 | "pipelineBadgeUrl": "https://git.example.org/example/jira-test/badges/feature/TEST-12345-open-test/pipeline.svg", 340 | "pipelineId": 38537, 341 | "pipelineStatus": "pending", 342 | "pipelineUrl": "https://git.example.org/example/jira-test/pipelines/38537", 343 | }; 344 | 345 | let rawResponse = jiraRemoteLinkGenerator.generateRemoteLinkFromEvent(rawEvent); 346 | 347 | const jiraRemoteLink = { 348 | "globalId": "gitlabUrl=https%3A%2F%2Fgit.example.org%2F&projectId=1422&mergeId=10841", 349 | "application": { 350 | "type":"com.gitlab", 351 | "name":"Gitlab" 352 | }, 353 | "relationship":"Gitlab Merge Request", 354 | "object": { 355 | "url": "https://git.example.org/example/jira-test/merge_requests/3", 356 | "summary": "", 357 | "title": "example/jira-test !3 Open MR", 358 | "icon": { 359 | "url16x16":"https://raw.githubusercontent.com/webdog/octicons-png/master/black/git-pull-request.png", 360 | "title":"Open Merge Request" 361 | }, 362 | "status": { 363 | "resolved": false, 364 | "icon": { 365 | "url16x16":"https://raw.githubusercontent.com/webdog/octicons-png/master/black/clock.png", 366 | "title":"Pipeline pending", 367 | "link": "https://git.example.org/example/jira-test/pipelines/38537" 368 | } 369 | } 370 | } 371 | }; 372 | 373 | expect(rawResponse).toEqual(jiraRemoteLink); 374 | }); 375 | 376 | 377 | 378 | test('generate open mergeRequest with running pipeline', () => { 379 | 380 | let rawEvent = { 381 | "type": "merge_request", 382 | "shortcut": "!3", 383 | "targetProjectId": 1422, 384 | "targetPath": "example/jira-test", 385 | "targetUrl": "https://git.example.org/example/jira-test", 386 | "targetBranch": "master", 387 | "sourceBranch": "feature/TEST-12345-open-test", 388 | "mergeTitle": "Open MR", 389 | "mergeStatus": "unchecked", 390 | "mergeId": 10841, 391 | "mergeState": "opened", 392 | "isWorkInProgress": false, 393 | "updatedAt": "2018-09-16 06:50:00 UTC", 394 | "mergeUrl": "https://git.example.org/example/jira-test/merge_requests/3", 395 | "pipelineBadgeUrl": "https://git.example.org/example/jira-test/badges/feature/TEST-12345-open-test/pipeline.svg", 396 | "pipelineId": 38537, 397 | "pipelineStatus": "running", 398 | "pipelineUrl": "https://git.example.org/example/jira-test/pipelines/38537", 399 | }; 400 | 401 | let rawResponse = jiraRemoteLinkGenerator.generateRemoteLinkFromEvent(rawEvent); 402 | 403 | const jiraRemoteLink = { 404 | "globalId": "gitlabUrl=https%3A%2F%2Fgit.example.org%2F&projectId=1422&mergeId=10841", 405 | "application": { 406 | "type":"com.gitlab", 407 | "name":"Gitlab" 408 | }, 409 | "relationship":"Gitlab Merge Request", 410 | "object": { 411 | "url": "https://git.example.org/example/jira-test/merge_requests/3", 412 | "summary": "", 413 | "title": "example/jira-test !3 Open MR", 414 | "icon": { 415 | "url16x16":"https://raw.githubusercontent.com/webdog/octicons-png/master/black/git-pull-request.png", 416 | "title":"Open Merge Request" 417 | }, 418 | "status": { 419 | "resolved": false, 420 | "icon": { 421 | "url16x16":"https://raw.githubusercontent.com/webdog/octicons-png/master/black/server.png", 422 | "title":"Pipeline running", 423 | "link": "https://git.example.org/example/jira-test/pipelines/38537" 424 | } 425 | } 426 | } 427 | }; 428 | 429 | expect(rawResponse).toEqual(jiraRemoteLink); 430 | }); 431 | 432 | test('generate open mergeRequest with success pipeline', () => { 433 | 434 | let rawEvent = { 435 | "type": "merge_request", 436 | "shortcut": "!3", 437 | "targetProjectId": 1422, 438 | "targetPath": "example/jira-test", 439 | "targetUrl": "https://git.example.org/example/jira-test", 440 | "targetBranch": "master", 441 | "sourceBranch": "feature/TEST-12345-open-test", 442 | "mergeTitle": "Open MR", 443 | "mergeStatus": "unchecked", 444 | "mergeId": 10841, 445 | "mergeState": "opened", 446 | "isWorkInProgress": false, 447 | "updatedAt": "2018-09-16 06:50:00 UTC", 448 | "mergeUrl": "https://git.example.org/example/jira-test/merge_requests/3", 449 | "pipelineBadgeUrl": "https://git.example.org/example/jira-test/badges/feature/TEST-12345-open-test/pipeline.svg", 450 | "pipelineId": 38537, 451 | "pipelineStatus": "success", 452 | "pipelineUrl": "https://git.example.org/example/jira-test/pipelines/38537", 453 | }; 454 | 455 | let rawResponse = jiraRemoteLinkGenerator.generateRemoteLinkFromEvent(rawEvent); 456 | 457 | const jiraRemoteLink = { 458 | "globalId": "gitlabUrl=https%3A%2F%2Fgit.example.org%2F&projectId=1422&mergeId=10841", 459 | "application": { 460 | "type":"com.gitlab", 461 | "name":"Gitlab" 462 | }, 463 | "relationship":"Gitlab Merge Request", 464 | "object": { 465 | "url": "https://git.example.org/example/jira-test/merge_requests/3", 466 | "summary": "", 467 | "title": "example/jira-test !3 Open MR", 468 | "icon": { 469 | "url16x16":"https://raw.githubusercontent.com/webdog/octicons-png/master/black/git-pull-request.png", 470 | "title":"Open Merge Request" 471 | }, 472 | "status": { 473 | "resolved": false, 474 | "icon": { 475 | "url16x16":"https://raw.githubusercontent.com/webdog/octicons-png/master/black/clock.png", 476 | "title":"Unchecked", 477 | "link": "https://git.example.org/example/jira-test/merge_requests/3" 478 | } 479 | } 480 | } 481 | }; 482 | 483 | expect(rawResponse).toEqual(jiraRemoteLink); 484 | }); -------------------------------------------------------------------------------- /src/JiraTicketExtractor.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('bridge:JiraTicketExtractor'); 2 | 3 | class JiraTicketExtractor { 4 | 5 | constructor () { 6 | debug('created') 7 | } 8 | 9 | getTicketIdsFromEvent (event) { 10 | debug('getTicketIdsFromEvent') 11 | var ticketIds = []; 12 | 13 | var rawString = " " + [ 14 | event.sourceBranch, 15 | event.mergeTitle, 16 | event.mergeDescription 17 | ].join(" ") + " "; 18 | 19 | var matches = rawString.match(/([A-Z][A-Z]+\-\d+)/g); 20 | 21 | return [ ...new Set(matches || [])]; 22 | } 23 | 24 | } 25 | 26 | module.exports = JiraTicketExtractor; -------------------------------------------------------------------------------- /src/JiraTicketExtractor.test.js: -------------------------------------------------------------------------------- 1 | var JiraTicketExtractor = require('./JiraTicketExtractor'); 2 | var jiraTicketExtractor = new JiraTicketExtractor(); 3 | 4 | test('parse without match', () => { 5 | var ticketIds = jiraTicketExtractor.getTicketIdsFromEvent({ 6 | "sourceBranch": "feature/we-did-things", 7 | "mergeTitle": "We did things", 8 | "mergeDescription": "There has been lots of things to do.", 9 | }); 10 | expect(ticketIds).toEqual([ 11 | ]); 12 | }); 13 | 14 | test('parse from source_branch', () => { 15 | var ticketIds = jiraTicketExtractor.getTicketIdsFromEvent({ 16 | "sourceBranch": "feature/TEST-123-we-did-things", 17 | "mergeTitle": "We did things", 18 | "mergeDescription": "There has been lots of things to do.", 19 | }); 20 | expect(ticketIds).toEqual([ 21 | "TEST-123" 22 | ]); 23 | }); 24 | 25 | test('parse from title', () => { 26 | var ticketIds = jiraTicketExtractor.getTicketIdsFromEvent({ 27 | "sourceBranch": "finally-fix-fix", 28 | "mergeTitle": "Finally fix TEST-123 and TEST-125", 29 | "mergeDescription": "There has been lots of things to do.", 30 | }); 31 | expect(ticketIds).toEqual([ 32 | "TEST-123", 33 | "TEST-125" 34 | ]); 35 | }); 36 | 37 | test('parse from description', () => { 38 | var ticketIds = jiraTicketExtractor.getTicketIdsFromEvent({ 39 | "sourceBranch": "finally-fix-fix", 40 | "mergeTitle": "Finally fix", 41 | "mergeDescription": "There has been work for TEST-123 and TEST-125 to do.", 42 | }); 43 | expect(ticketIds).toEqual([ 44 | "TEST-123", 45 | "TEST-125" 46 | ]); 47 | }); 48 | 49 | test('parse from description, title and sourceBranch', () => { 50 | var ticketIds = jiraTicketExtractor.getTicketIdsFromEvent({ 51 | "sourceBranch": "finally-TEST-555-fix-fix", 52 | "mergeTitle": "Finally fix TEST-666", 53 | "mergeDescription": "There has been work for TEST-123 and TEST-125 to do.", 54 | }); 55 | expect(ticketIds).toEqual([ 56 | "TEST-555", 57 | "TEST-666", 58 | "TEST-123", 59 | "TEST-125", 60 | ]); 61 | }); 62 | 63 | test('unique parse from description, title and sourceBranch', () => { 64 | var ticketIds = jiraTicketExtractor.getTicketIdsFromEvent({ 65 | "sourceBranch": "finally-TEST-555-fix-fix", 66 | "mergeTitle": "Finally fix TEST-666", 67 | "mergeDescription": "There has TEST-555 been work for TEST-123 and TEST-125 to do.", 68 | }); 69 | expect(ticketIds).toEqual([ 70 | "TEST-555", 71 | "TEST-666", 72 | "TEST-123", 73 | "TEST-125", 74 | ]); 75 | }); -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | 3 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; 4 | 5 | var assert = require('assert'); 6 | var express = require('express'); 7 | var app = express(); 8 | var bodyParser = require('body-parser'); 9 | var debug = require('debug')('bridge:server'); 10 | var tunnel = require('tunnel'); 11 | 12 | var GitlabEventParser = require('./GitlabEventParser'); 13 | var parser = new GitlabEventParser(); 14 | var JiraRemoteLinkGenerator = require('./JiraRemoteLinkGenerator'); 15 | var jiraRemoteLinkGenerator = new JiraRemoteLinkGenerator(true); 16 | var JiraTicketExtractor = require('./JiraTicketExtractor'); 17 | var jiraTicketExtractor = new JiraTicketExtractor(); 18 | const got = require('got'); 19 | 20 | assert(process.env.JIRA_BASE_URL, "JIRA_BASE_URL is not set") 21 | assert(process.env.GITLAB_WEBHOOK_TOKEN, "GITLAB_WEBHOOK_TOKEN is not set") 22 | assert(process.env.GITLAB_BASE_URL, "GITLAB_BASE_URL is not set") 23 | assert(process.env.GITLAB_PERSONAL_ACCESS_TOKEN, "GITLAB_PERSONAL_ACCESS_TOKEN is not set") 24 | 25 | app.use(bodyParser.json()); 26 | 27 | app.use(function (req, res, next) { 28 | res.header('X-App-Version', process.env.APP_VERSION); 29 | res.header('X-App-Name', 'gitlab-jira-webhook'); 30 | next(); 31 | }); 32 | 33 | app.get('/', function (req, res) { 34 | res.send('OK'); 35 | }); 36 | 37 | app.post('/events', function (req, res) { 38 | 39 | var fs = require('fs'); 40 | 41 | debug('X-Gitlab-Token', req.headers['x-gitlab-token']); 42 | 43 | if (req.headers['x-gitlab-token'] !== process.env.GITLAB_WEBHOOK_TOKEN) { 44 | debug('invalid gitlab token') 45 | res.sendStatus(403); 46 | return ; 47 | } 48 | 49 | if (process.env.STORE_EVENTS) { 50 | var cacheFileName = 'data/'; 51 | cacheFileName += req.body.object_kind; 52 | cacheFileName += "." + (new Date().getTime()) + ".json"; 53 | debug("file:", cacheFileName) 54 | fs.writeFile(cacheFileName, JSON.stringify(req.body, null, 2), () => { 55 | }); 56 | } 57 | 58 | var notifyTicketsForEvent = (event) => { 59 | var remoteLink = jiraRemoteLinkGenerator.generateRemoteLinkFromEvent(event); 60 | var ticketIds = jiraTicketExtractor.getTicketIdsFromEvent(event); 61 | 62 | ticketIds.forEach((ticketId) => { 63 | debug('notify ticket ' + ticketId); 64 | debug(JSON.stringify(remoteLink, null, 2)); 65 | var options = { 66 | headers: { 67 | "Content-Type": "application/json" 68 | }, 69 | body: JSON.stringify(remoteLink) 70 | }; 71 | 72 | if (process.env.JIRA_USERNAME) { 73 | options.auth = process.env.JIRA_USERNAME + ':' + process.env.JIRA_PASSWORD; 74 | } 75 | 76 | if (process.env.JIRA_PFX_PATH) { 77 | options.pfx = fs.readFileSync(process.env.JIRA_PFX_PATH); 78 | } 79 | if (process.env.JIRA_PFX_PASSWORD) { 80 | options.passphrase = process.env.JIRA_PFX_PASSWORD; 81 | } 82 | 83 | if (process.env.JIRA_HTTPS_PROXY) { 84 | var urlParts = require('url').parse(process.env.JIRA_HTTPS_PROXY); 85 | options.agent = tunnel.httpsOverHttp({ 86 | proxy: { 87 | host: urlParts.hostname, 88 | port: urlParts.port, 89 | proxyAuth: urlParts.auth, 90 | } 91 | }); 92 | } 93 | 94 | got.post(process.env.JIRA_BASE_URL + '/rest/api/latest/issue/'+ ticketId +'/remotelink', options).then((response) => { 95 | debug(response.body); 96 | }); 97 | }); 98 | }; 99 | 100 | var notfyMergeRequestIidAndRawProject = (mergeRequestIid, rawProject) => { 101 | got.get(process.env.GITLAB_BASE_URL + '/api/v4/projects/' + rawProject.id + '/merge_requests/' + mergeRequestIid, { 102 | json: true, 103 | headers: { 104 | "Private-Token": process.env.GITLAB_PERSONAL_ACCESS_TOKEN 105 | } 106 | }).then((rawMergeRequestResponse) => { 107 | var event = parser.parseMergeRequestAndTargetProject(rawMergeRequestResponse.body, rawProject); 108 | notifyTicketsForEvent(event); 109 | }); 110 | }; 111 | 112 | if (req.body.object_kind === "merge_request") { 113 | notfyMergeRequestIidAndRawProject(req.body.object_attributes.iid, req.body.project); 114 | } 115 | 116 | /* 117 | * If a merge request is created, it's defined as "unchecked" and doesn't recieve any updates! 118 | * If we recieve an update for a pipeline, we want to notify all related merge requests, too! 119 | */ 120 | if (req.body.object_kind === "pipeline") { 121 | var projectId = req.body.project.id; 122 | var rawProject = req.body.project; 123 | var commitId = req.body.commit.id; 124 | 125 | got.get(process.env.GITLAB_BASE_URL + '/api/v4/projects/' + projectId + '/repository/commits/'+ commitId +'/merge_requests', { 126 | json: true, 127 | headers: { 128 | "Private-Token": process.env.GITLAB_PERSONAL_ACCESS_TOKEN 129 | } 130 | }).then((rawMergeRequestSummariesResponse) => { 131 | /* The summaries don't contain the pipeline property + status -> thus we need to refetch it */ 132 | rawMergeRequestSummariesResponse.body.forEach((rawMergeRequestSummaryResponse) => { 133 | notfyMergeRequestIidAndRawProject(rawMergeRequestSummaryResponse.iid, rawProject); 134 | }); 135 | }); 136 | } 137 | res.send('OK'); 138 | }); 139 | 140 | var port = process.env.PORT || 3000; 141 | 142 | app.listen(port, function () { 143 | debug('listening on port ' + port); 144 | }); 145 | --------------------------------------------------------------------------------