├── CODEOWNERS ├── public ├── favicon.ico ├── index.js └── vendor │ ├── bootstrap-table.min.css │ └── bootstrap.min.js ├── nodemon.json ├── axios.js ├── .github ├── release-drafter.yml └── settings.yml ├── .editorconfig ├── .eslintrc.js ├── Dockerfile ├── Jenkinsfile ├── Makefile ├── README.md ├── views ├── header.ejs ├── index.ejs ├── progress.ejs └── navigation.ejs ├── graphql.js ├── package.json ├── .gitignore ├── .dockerignore ├── .eslintignore ├── reports.test.js ├── confluence.js ├── utils.test.js ├── index.test.js ├── reports.js ├── test-data ├── with-classes-removed.html └── with-classes.html ├── index.js ├── utils.js ├── __snapshots__ └── index.test.js.snap └── __testData ├── cron_column.json ├── html5-notifier-plugin.json └── github.json /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @halkeye @jenkins-infra/docs 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkins-infra/jenkins-wiki-exporter/master/public/favicon.ico -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": true, 3 | "ignore": ["*.test.js", "public/*"], 4 | "watch": ["./*.js"] 5 | } 6 | -------------------------------------------------------------------------------- /axios.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const pkg = require('./package.json'); 3 | 4 | module.exports = axios.create({ 5 | headers: {'User-Agent': `jenkins-wiki-exporter/${pkg.version}`}, 6 | }); 7 | 8 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | # See https://github.com/jenkinsci/.github/blob/master/.github/release-drafter.adoc 2 | _extends: jenkinsci/.github 3 | # Semantic versioning: https://semver.org/ 4 | version-template: $MAJOR.$MINOR.$PATCH 5 | tag-template: v$NEXT_MINOR_VERSION 6 | name-template: v$NEXT_MINOR_VERSION 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 2 12 | charset = utf-8 13 | 14 | [Makefile] 15 | indent_style = tab 16 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'env': { 3 | 'commonjs': true, 4 | 'es6': true, 5 | }, 6 | 'extends': [ 7 | 'google', 8 | ], 9 | 'globals': { 10 | 'Atomics': 'readonly', 11 | 'SharedArrayBuffer': 'readonly', 12 | }, 13 | 'parserOptions': { 14 | 'ecmaVersion': 2018, 15 | }, 16 | 'rules': { 17 | 'max-len': 0, 18 | 'no-undef': 2, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16.8.0 2 | SHELL ["/bin/bash", "-o", "pipefail", "-c"] 3 | RUN wget -q -nc -O - https://github.com/jgm/pandoc/releases/download/2.7.3/pandoc-2.7.3-linux.tar.gz | tar xvzf - --strip-components=2 -C /usr/bin pandoc-2.7.3/bin/pandoc 4 | 5 | USER node 6 | ENV NODE_ENV=production 7 | WORKDIR /home/node 8 | COPY --chown=node package.json package-lock.json ./ 9 | RUN npm install 10 | COPY --chown=node . . 11 | CMD ["npm","run","start"] 12 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/groovy 2 | if (JENKINS_URL.contains('infra.ci.jenkins.io')) { 3 | buildDockerAndPublishImage('jenkins-wiki-exporter') 4 | return; 5 | } 6 | 7 | if (JENKINS_URL.contains('ci.jenkins.io')) { 8 | node('docker&&linux') { 9 | checkout scm 10 | sh "docker build -t jenkins-wiki-exporter ." 11 | docker.image('jenkins-wiki-exporter').inside { 12 | sh "NODE_ENV=development HOME=${env.WORKSPACE} npm install" 13 | sh "NODE_ENV=development HOME=${env.WORKSPACE} npm run test" 14 | } 15 | } 16 | return; 17 | } 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := help 2 | DOCKER_PREFIX ?= halkeye 3 | DOCKER_IMAGE ?= jenkins-wiki-exporter 4 | CONFLUENCE_USERNAME ?= $(shell bash -c 'read -s -p "Jenkins Wiki Username: " username; echo $$username') 5 | CONFLUENCE_PASSWORD ?= $(shell bash -c 'read -s -p "Jenkins Wiki Password: " pwd; echo $$pwd') 6 | 7 | dev: ## run the dev server 8 | npm run dev 9 | 10 | build: ## build the docker version 11 | docker build -t $(DOCKER_PREFIX)/$(DOCKER_IMAGE) . 12 | 13 | run: ## run the latest docker build 14 | docker run --rm -it -e CONFLUENCE_USERNAME -e CONFLUENCE_PASSWORD --name $(DOCKER_IMAGE) -p 3000:3000 -t $(DOCKER_PREFIX)/$(DOCKER_IMAGE) 15 | 16 | push: ## push to docker registry 17 | docker push $(DOCKER_PREFIX)/$(DOCKER_IMAGE) 18 | 19 | release: ## bump the version and push a release 20 | np 21 | 22 | .PHONY: help 23 | help: 24 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Jenkins Wiki Exporter 2 | ===================== 3 | 4 | Service to convert jenkins plugin Wiki format to github Markdown or Asciidoc. 5 | It was hosted at https://jenkins-wiki-exporter.jenkins.io, now deprecated and archived. 6 | The corresponding code has been imported in [jenkins-infra/infra-reports](https://github.com/jenkins-infra/infra-reports). 7 | See https://github.com/jenkins-infra/helpdesk/issues/3059 8 | 9 | ## Usage 10 | 11 | See the documentation [here](https://jenkins.io/doc/developer/publishing/wiki-page/#migrating-from-wiki-to-github). 12 | 13 | ## Configuration 14 | 15 | ### Environmental Variables 16 | 17 | * CONFLUENCE_USERNAME - username that connects to wiki.jenkins.io 18 | * CONFLUENCE_PASSWORD - password that connects to wiki.jenkins.io 19 | * GITHUB_TOKEN - token for GitHub API, needs `read:org` and `public_repo` scopes 20 | 21 | # Contributing 22 | 23 | See the Makefile in the repository 24 | 25 | # Releasing 26 | 27 | `npm run release` 28 | -------------------------------------------------------------------------------- /views/header.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= title %> - Jenkins Wiki Converter 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /graphql.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | const {GraphQLClient} = require('graphql-request'); 3 | require('cross-fetch/polyfill'); 4 | 5 | const getPullRequestsQuery = ` 6 | query getPullRequests($login: String!) { 7 | organization(login: $login) { 8 | project(number:3) { 9 | columns (first:100) { 10 | edges { 11 | node { 12 | id 13 | cards { 14 | edges { 15 | node { 16 | content { 17 | ... on PullRequest { 18 | url 19 | } 20 | } 21 | } 22 | } 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | }`; 30 | 31 | /** 32 | * Creates a graphql github client 33 | * @private 34 | * @return {GraphQLClient} 35 | */ 36 | function getGithubClient() { 37 | return new GraphQLClient( 38 | process.env.GITHUB_SERVER || 'https://api.github.com/graphql', 39 | { 40 | headers: { 41 | Authorization: `bearer ${process.env.GITHUB_TOKEN}`, 42 | }, 43 | }, 44 | ); 45 | } 46 | 47 | /** 48 | * Get all the pull requests from the jenkins project 49 | * @return {object} 50 | */ 51 | async function getPullRequests() { 52 | return getGithubClient().request( 53 | getPullRequestsQuery, 54 | { 55 | login: 'jenkinsci', 56 | }, 57 | ); 58 | } 59 | 60 | module.exports = { 61 | getPullRequests, 62 | }; 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jenkins-wiki-exporter", 3 | "version": "1.12.1", 4 | "description": "", 5 | "main": "index.js", 6 | "private": true, 7 | "scripts": { 8 | "dev": "nodemon", 9 | "build": "true", 10 | "start": "node index.js", 11 | "lint": "eslint .", 12 | "release": "np", 13 | "test": "jest" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/jenkins-infra/jenkins-wiki-exporter/.git" 18 | }, 19 | "author": "Gavin Mogan (https://www.gavinmogan.com/)", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/jenkins-infra/jenkins-wiki-exporter/issues" 23 | }, 24 | "homepage": "https://github.com/jenkins-infra/jenkins-wiki-exporter#readme", 25 | "dependencies": { 26 | "@sentry/node": "^6.11.0", 27 | "archiver": "^5.3.0", 28 | "axios": "^0.21.1", 29 | "bunyan": "^1.8.15", 30 | "cheerio": "^1.0.0-rc.10", 31 | "confluence-api": "^1.4.0", 32 | "cross-fetch": "^3.1.4", 33 | "dom-parser": "^0.1.6", 34 | "ejs": "^3.1.6", 35 | "express": "^4.17.1", 36 | "express-bunyan-logger": "^1.3.3", 37 | "graphql": "^15.5.1", 38 | "graphql-request": "^3.5.0", 39 | "http-errors": "^1.8.0", 40 | "moment": "^2.29.1", 41 | "talkback": "^2.4.2", 42 | "xml2js": "^0.4.23" 43 | }, 44 | "devDependencies": { 45 | "eslint": "^7.32.0", 46 | "eslint-config-google": "^0.14.0", 47 | "jest": "^27.1.0", 48 | "mock-express-request": "^0.2.2", 49 | "mock-express-response": "^0.3.0", 50 | "mockdate": "^3.0.5", 51 | "nodemon": "^2.0.12", 52 | "np": "*" 53 | }, 54 | "np": { 55 | "yarn": false, 56 | "releaseDraft": false 57 | }, 58 | "jest": { 59 | "verbose": true, 60 | "testEnvironment": "node" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node 3 | # Edit at https://www.gitignore.io/?templates=node 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # TypeScript v1 declaration files 50 | typings/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Optional REPL history 62 | .node_repl_history 63 | 64 | # Output of 'npm pack' 65 | *.tgz 66 | 67 | # Yarn Integrity file 68 | .yarn-integrity 69 | 70 | # dotenv environment variables file 71 | .env 72 | .env.test 73 | 74 | # parcel-bundler cache (https://parceljs.org/) 75 | .cache 76 | 77 | # next.js build output 78 | .next 79 | 80 | # nuxt.js build output 81 | .nuxt 82 | 83 | # vuepress build output 84 | .vuepress/dist 85 | 86 | # Serverless directories 87 | .serverless/ 88 | 89 | # FuseBox cache 90 | .fusebox/ 91 | 92 | # DynamoDB Local files 93 | .dynamodb/ 94 | 95 | # End of https://www.gitignore.io/api/node 96 | .envrc 97 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node 3 | # Edit at https://www.gitignore.io/?templates=node 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # TypeScript v1 declaration files 50 | typings/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Optional REPL history 62 | .node_repl_history 63 | 64 | # Output of 'npm pack' 65 | *.tgz 66 | 67 | # Yarn Integrity file 68 | .yarn-integrity 69 | 70 | # dotenv environment variables file 71 | .env 72 | .env.test 73 | 74 | # parcel-bundler cache (https://parceljs.org/) 75 | .cache 76 | 77 | # next.js build output 78 | .next 79 | 80 | # nuxt.js build output 81 | .nuxt 82 | 83 | # vuepress build output 84 | .vuepress/dist 85 | 86 | # Serverless directories 87 | .serverless/ 88 | 89 | # FuseBox cache 90 | .fusebox/ 91 | 92 | # DynamoDB Local files 93 | .dynamodb/ 94 | 95 | # End of https://www.gitignore.io/api/node 96 | Makefile 97 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node 3 | # Edit at https://www.gitignore.io/?templates=node 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # TypeScript v1 declaration files 50 | typings/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Optional REPL history 62 | .node_repl_history 63 | 64 | # Output of 'npm pack' 65 | *.tgz 66 | 67 | # Yarn Integrity file 68 | .yarn-integrity 69 | 70 | # dotenv environment variables file 71 | .env 72 | .env.test 73 | 74 | # parcel-bundler cache (https://parceljs.org/) 75 | .cache 76 | 77 | # next.js build output 78 | .next 79 | 80 | # nuxt.js build output 81 | .nuxt 82 | 83 | # vuepress build output 84 | .vuepress/dist 85 | 86 | # Serverless directories 87 | .serverless/ 88 | 89 | # FuseBox cache 90 | .fusebox/ 91 | 92 | # DynamoDB Local files 93 | .dynamodb/ 94 | 95 | # End of https://www.gitignore.io/api/node 96 | !.eslintrc.js 97 | public/vendor 98 | -------------------------------------------------------------------------------- /views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%- include('header', {title: "Export"}); %> 4 | 5 |
6 | <%- include('navigation'); %> 7 |
8 |
9 | 16 |
17 | 18 |
19 |
20 | Format 21 |
22 | 23 | 24 | 25 | 26 |
27 |
28 | 29 |
30 |
31 | 32 |
33 | 34 |
35 |
36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /views/progress.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%- include('header', {title: "Plugin Migration Progress"}); %> 4 | 5 |
6 | <%- include('navigation'); %> 7 |

Plugin Migration Progress

8 |

Todo: <%= statuses.todo %>, PR open: <%= statuses["pr open"] %>, PR merged: <%= statuses["pr merged"] %>, Done: <%= statuses.ok + statuses.deprecated %>, Total: <%= statuses.total %>

9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | <% plugins.forEach(function(plugin) { %> 18 | 19 | 24 | 29 | 30 | 33 | 36 | 37 | <% }); %> 38 |
Plugin NameStatusLast releaseInstalls
20 | 21 | <%= plugin.name %> 22 | 23 | 25 | 26 | <%= plugin.status %> 27 | 28 | 31 | <%= plugin.releaseDate %> 32 | 34 | <%= plugin.installs %> 35 |
39 | <% if (recent.length) { %> 40 |

Recently merged

41 | <%= recent.join(", ") %> 42 | <% } %> 43 |
44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /views/navigation.ejs: -------------------------------------------------------------------------------- 1 | 44 | -------------------------------------------------------------------------------- /public/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | const $markdown = document.getElementById('markdown'); 3 | const $pluginName = document.getElementById('pluginName'); 4 | const $convertButton = document.getElementById('convertButton'); 5 | 6 | const onsubmit = function(ev) { 7 | ev.preventDefault(); 8 | const type = document.querySelector('#format:checked').value; 9 | const pluginName = $pluginName.value; 10 | fetchContent(pluginName, type); 11 | }; 12 | 13 | const onPageLoad = function() { 14 | const params = parseUrl(); 15 | const pluginName = params.pluginName; 16 | if (pluginName) { 17 | $pluginName.value = pluginName; 18 | fetchContent(pluginName, params.type || '.md'); 19 | } 20 | }; 21 | 22 | const fetchContent = function(pluginName, type) { 23 | $markdown.value = 'Pending....'; 24 | let url = './plugin/' + encodeURIComponent(pluginName) + type; 25 | if (pluginName.match(/^https?\:\/\//)) { 26 | url = '/confluence-url/' + encodeURIComponent(pluginName) + type; 27 | } 28 | 29 | fetch(url, {responseType: 'blob'}) 30 | .then(function(response) { 31 | if (!response.ok) { 32 | return response.text().then((text) => { 33 | throw Error(text || response.statusText); 34 | }); 35 | } 36 | return response; 37 | }) 38 | .then((response) => { 39 | if (response.headers.get('content-type').includes('/zip')) { 40 | return response.blob() 41 | .then((blob) => { 42 | $markdown.value = 'Saving...'; 43 | window.saveAs(blob, pluginName + type); 44 | }); 45 | } else { 46 | return response.text().then((body) => $markdown.value = body); 47 | } 48 | }) 49 | .catch((err) => $markdown.value = 'Error: ' + err.toString()); 50 | }; 51 | 52 | const parseUrl = function() { 53 | const query = location.search.replace(/^\?/, '').split('&'); 54 | const params = {}; 55 | query.forEach(function(tuple) { 56 | const keyAndVal = tuple.split('='); 57 | params[keyAndVal[0]] = keyAndVal[1]; 58 | }); 59 | return params; 60 | }; 61 | 62 | window.addEventListener('load', onPageLoad); 63 | $pluginName.addEventListener('keydown', function(e) { 64 | if (e.key == 'Enter') { 65 | e.preventDefault(); 66 | onsubmit(e); 67 | return; 68 | } 69 | }); 70 | $convertButton.addEventListener('click', onsubmit); 71 | $markdown.value = ''; 72 | -------------------------------------------------------------------------------- /reports.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, jest */ 2 | const MockDate = require('mockdate'); 3 | const crypto = require('crypto'); 4 | const path = require('path'); 5 | 6 | const talkback = require('talkback'); 7 | 8 | const {pluginsReport} = require('./reports.js'); 9 | 10 | jest.setTimeout(30000); // 30 seconds 11 | 12 | let server; 13 | beforeEach(function() { 14 | MockDate.set(new Date('2020-05-25 00:00:00Z')); 15 | }); 16 | 17 | beforeAll(async function() { 18 | const port = 8888; 19 | process.env.GITHUB_SERVER = `http://localhost:${port}/graphql`; 20 | server = await talkback({ 21 | host: 'https://api.github.com', 22 | port: port, 23 | path: path.join(__dirname, '__testData', 'vcr'), 24 | silent: true, 25 | ignoreHeaders: ['authorization'], 26 | ignoreQueryParams: ['adminuser', 'admintoken'], 27 | tapeNameGenerator: (tapeNumber, tape) => { 28 | const content = tape.req.body.toString(); 29 | const hash = crypto.createHash('md5').update(content).digest('hex'); 30 | const matches = content.match(/query (\w+)/); 31 | if (matches && matches[1]) { 32 | const variables = Object.entries( 33 | JSON.parse(content).variables, 34 | ).map( 35 | ([key, value]) => `${key}_${value}`, 36 | ).join('_'); 37 | return `${tape.req.method.toLowerCase()}_${matches[1]}_${variables}_${hash}.json5`; 38 | } 39 | 40 | return `${tape.req.method.toLowerCase()}_${hash}.json5`; 41 | }, 42 | }).start(); 43 | }); 44 | afterAll(function(done) { 45 | server.close(done); 46 | }); 47 | 48 | 49 | describe('reports', function() { 50 | describe('happy path', function() { 51 | beforeAll( async () => { 52 | this.reportData = await pluginsReport(); 53 | }); 54 | it('should mark multiple-scms as deprecated', () => { 55 | expect(this.reportData.plugins.find( 56 | (plugin) => plugin.name == 'multiple-scms', 57 | )['labels']).toEqual([ 58 | 'deprecated', 'scm', 59 | ]); 60 | }); 61 | it('have all the statuses ', () => { 62 | expect(this.reportData.statuses).toHaveProperty('ok'); 63 | expect(this.reportData.statuses).toHaveProperty('pr merged'); 64 | expect(this.reportData.statuses).toHaveProperty('pr open'); 65 | expect(this.reportData.statuses).toHaveProperty('deprecated'); 66 | expect(this.reportData.statuses).toHaveProperty('todo'); 67 | expect(this.reportData.statuses).toHaveProperty('total'); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /confluence.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | const {basename} = require('path'); 3 | const URL = require('url'); 4 | const bunyan = require('bunyan'); 5 | const axios = require('./axios'); 6 | const Confluence = require('confluence-api'); 7 | const DomParser = require('dom-parser'); 8 | const cheerio = require('cheerio'); 9 | const {promisify} = require('util'); 10 | 11 | const logger = bunyan.createLogger({ 12 | name: basename(process.argv[0]) + ':confluence', 13 | }); 14 | 15 | const confluence = new Confluence({ 16 | username: process.env.CONFLUENCE_USERNAME, 17 | password: process.env.CONFLUENCE_PASSWORD, 18 | baseUrl: 'https://wiki.jenkins.io', 19 | version: 4, 20 | }); 21 | confluence.getContentByIdPromise = promisify(confluence.getContentById); 22 | 23 | 24 | /** 25 | * Get the content part of Confluence page, without URL processing 26 | * @param {string} url confluence url 27 | * @return {string} content 28 | */ 29 | async function getRawConfluenceContent(url) { 30 | logger.info('getRawConfluenceContent: looking up ' + url); 31 | const body = await axios.get(url).then((response) => response.data); 32 | const domParser = new DomParser(); 33 | const dom = domParser.parseFromString(body); 34 | const meta = dom.getElementsByTagName('meta').find((elm)=>elm.getAttribute('name') === 'ajs-page-id'); 35 | if (!meta) { 36 | throw new Error('No meta page id found'); 37 | } 38 | const pageId = meta.getAttribute('content'); 39 | if (!pageId) { 40 | throw new Error('No page id found'); 41 | } 42 | return cheerio.load(body)('.wiki-content').html(); 43 | } 44 | 45 | /** 46 | * Get's content from a confluence page 47 | * Remove's some content that we don't want 48 | * 49 | * @param {*} url confluence url 50 | * @param {*} fragment relevant part of page content 51 | * @return {string} processed html 52 | */ 53 | function getContentFromConfluencePage(url, fragment) { 54 | const $ = cheerio.load(fragment); 55 | $('.conf-macro.output-inline th:contains("Plugin Information")').parents('table').remove(); 56 | 57 | // Remove any table of contents 58 | $('.toc').remove(); 59 | 60 | // Replace href/src with the wiki url 61 | $('[href]').each((idx, elm) => { 62 | $(elm).attr('href', URL.resolve(url, $(elm).attr('href'))); 63 | }); 64 | $('[src]').each((idx, elm) => { 65 | $(elm).attr('src', URL.resolve(url, $(elm).attr('data-image-src') || $(elm).attr('src'))); 66 | }); 67 | return $.html(); 68 | } 69 | 70 | module.exports = { 71 | getRawConfluenceContent, 72 | getContentFromConfluencePage, 73 | }; 74 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | repository: 2 | name: jenkins-wiki-exporter 3 | description: Quick util to convert jenkins plugin wiki format to github markdown or asciidoc 4 | homepage: https://jenkins-wiki-exporter.jenkins.io 5 | topics: 'confluence, jenkins, hacktoberfest, pandoc' 6 | private: false 7 | default_branch: master 8 | has_issues: true 9 | has_wiki: false 10 | allow_merge_commit: true 11 | allow_squash_merge: true 12 | allow_rebase_merge: true 13 | 14 | #TODO(oleg-nenashev): Consider moving to https://github.com/jenkinsci/.github once https://github.com/probot/settings/issues/107 is fixed 15 | labels: 16 | - name: good first issue 17 | color: 7057ff 18 | - name: hackathon 19 | color: 85f920 20 | - name: help wanted 21 | color: 008672 22 | - name: question 23 | color: D876E3 24 | - name: wontfix 25 | color: FFFFFF 26 | - name: duplicate 27 | color: CFD3D7 28 | - name: bug 29 | color: D73A4A 30 | - name: documentation 31 | color: 0e8a16 32 | description: A PR that adds to documentation - used by Release Drafter 33 | - name: hacktoberfest 34 | color: bdff3a 35 | description: 'Hacktoberfest. https://jenkins.io/blog/2018/10/01/hacktoberfest/' 36 | - name: feature 37 | color: 1d00ff 38 | description: A PR that adds a feature - used by Release Drafter 39 | - name: bugfix 40 | oldname: fix 41 | color: c9e85c 42 | description: A PR that fixes a bug - used by Release Drafter 43 | - name: chore 44 | color: c9abea 45 | description: a PR that adds to maintenance - used by Release Drafter 46 | - name: test 47 | color: d6e819 48 | description: A PR that adds to testing - used by Release Drafter 49 | - name: pinned 50 | color: 5ed5e5 51 | description: 'Used to avoid stale[bot] marking a issue/PR stale' 52 | - name: plugin-compatibility 53 | color: 8425c4 54 | - name: stale 55 | color: ffffff 56 | description: 'Used by stale[bot] to mark a issue/PR stale' 57 | - name: skip-changelog 58 | color: f44271 59 | description: A PR that is excluded from Release draft - used by Release Drafter 60 | - name: removed 61 | color: aa0f1c 62 | description: A PR that removes code - used by Release Drafter 63 | - name: deprecated 64 | color: e2b626 65 | description: A PR that deprecates code - used by Release Drafter 66 | - name: breaking 67 | color: 640910 68 | description: A PR that is a breaking change - used by Release Drafter 69 | - name: dependencies 70 | color: 0366d6 71 | description: A PR that updates dependencies - used by Release Drafter 72 | -------------------------------------------------------------------------------- /utils.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, jest */ 2 | const { 3 | findImages, 4 | checkUrl, 5 | replaceConfluenceContent, 6 | getAllPluginNamesForRepo, 7 | } = require('./utils.js'); 8 | 9 | const fs = require('fs'); 10 | describe('utils', function() { 11 | it('findImages', async () => { 12 | const images = findImages(require('./__testData/cron_column.json').wiki.content); 13 | expect(images).toEqual([ 14 | 'https://wiki.jenkins.io/download/attachments/43712750/Hudson%20Plugin%20-%20Cron%20Column.jpg?version=1&modificationDate=1271470566000&api=v2', 15 | 'https://wiki.jenkins.io/download/attachments/43712750/Hudson%20Plugin%20-%20Cron%20Column.jpg?version=1&modificationDate=1271470566000&api=v2', 16 | ]); 17 | }); 18 | it('findImages with empty URL', async () => { 19 | const images = findImages(require('./__testData/github.json').wiki.content); 20 | expect(images).toEqual([ 21 | 'https://wiki.jenkins.io/download/attachments/37749162/changes.png?version=1&modificationDate=1244761866000&api=v2', 22 | 'https://wiki.jenkins.io/download/thumbnails/37749162/changes.png?version=1&modificationDate=1244761866000&api=v2', 23 | 'https://wiki.jenkins.io/download/attachments/37749162/changes-2.png?version=1&modificationDate=1244761881000&api=v2', 24 | 'https://wiki.jenkins.io/download/thumbnails/37749162/changes-2.png?version=1&modificationDate=1244761881000&api=v2', 25 | 'https://wiki.jenkins.io/s/en_GB/8100/5084f018d64a97dc638ca9a178856f851ea353ff/_/images/icons/emoticons/help_16.svg', 26 | 'https://wiki.jenkins.io/download/attachments/37749162/ghserver-config.png?version=1&modificationDate=1441295981000&api=v2', 27 | 'https://wiki.jenkins.io/download/thumbnails/37749162/ghserver-config.png?version=1&modificationDate=1441295981000&api=v2', 28 | 'https://wiki.jenkins.io/download/attachments/37749162/manage-token.png?version=1&modificationDate=1441297409000&api=v2', 29 | 'https://wiki.jenkins.io/download/thumbnails/37749162/manage-token.png?version=1&modificationDate=1441297409000&api=v2', 30 | 'https://wiki.jenkins.io/download/attachments/37749162/secret-text.png?version=1&modificationDate=1441295988000&api=v2', 31 | 'https://wiki.jenkins.io/download/thumbnails/37749162/secret-text.png?version=1&modificationDate=1441295988000&api=v2', 32 | ]); 33 | }); 34 | it('checkUrl', () => { 35 | expect(checkUrl(['wiki.jenkins.io'], 'https://wiki.jenkins.io/something')).toBe(true); 36 | }); 37 | it('replaceConfluenceContent', () => { 38 | const input = fs.readFileSync('test-data/with-classes.html', 'utf8').trim(); 39 | const expected = fs.readFileSync('test-data/with-classes-removed.html', 'utf8').trim(); 40 | expect(replaceConfluenceContent(input)).toBe(expected); 41 | }); 42 | describe.each([ 43 | ['https://github.com/jenkinsci/cloudbees-disk-usage-simple-plugin/pull/28', 'cloudbees-disk-usage-simple'], 44 | ['https://github.com/jenkinsci/groovy', 'groovy'], 45 | ['https://github.com/jenkinsci/backend-pull-request-greeter', 'backend-pull-request-greeter'], 46 | ])('pluginNameFromUrl(%s)', (url, expected) => { 47 | test(`returns ${expected}`, () => { 48 | expect(getAllPluginNamesForRepo(url, {})[0]).toBe(expected); 49 | }); 50 | }); 51 | }); 52 | 53 | -------------------------------------------------------------------------------- /index.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, jest */ 2 | process.env.CONFLUENCE_USERNAME = process.env.CONFLUENCE_PASSWORD = 'fake'; 3 | 4 | const sut = require('./index.js'); 5 | const MockExpressRequest = require('mock-express-request'); 6 | const MockExpressResponse = require('mock-express-response'); 7 | const mockWikiPrefix = 'https://wiki.jenkins.io/display/JENKINS/'; 8 | jest.mock('./confluence.js', () => ({ 9 | ...jest.requireActual('./confluence.js'), 10 | getRawConfluenceContent: (url) => { 11 | const pluginName = url.replace(mockWikiPrefix, ''); 12 | return require('fs').promises.readFile(`__testData/${pluginName}.json`) 13 | .then((buf) => JSON.parse(buf.toString()).wiki.content); 14 | }, 15 | })); 16 | 17 | jest.mock('./reports.js', () => ({ 18 | ...jest.requireActual('./reports.js'), 19 | getPluginWikiUrl: (pluginName) => { 20 | return `${mockWikiPrefix}${pluginName}`; 21 | }, 22 | })); 23 | 24 | const makeLogger = () => { 25 | return { 26 | debug: jest.fn(), 27 | error: jest.fn(), 28 | }; 29 | }; 30 | 31 | describe('/plugin/:pluginName', function() { 32 | it('handle the basics', async () => { 33 | const logger = makeLogger(); 34 | const req = new MockExpressRequest({ 35 | log: logger, 36 | params: { 37 | plugin: 'html5-notifier-plugin', 38 | }, 39 | }); 40 | const res = { 41 | send: jest.fn(), 42 | type: jest.fn(), 43 | }; 44 | 45 | await sut.requestPluginHandler(req, res); 46 | expect(res.send.mock.calls).toMatchSnapshot(); 47 | expect(logger.error.mock.calls).toEqual([]); 48 | }); 49 | 50 | it('markdown should return markdown', async () => { 51 | const logger = makeLogger(); 52 | const req = new MockExpressRequest({ 53 | log: logger, 54 | params: { 55 | plugin: 'html5-notifier-plugin', 56 | format: 'md', 57 | }, 58 | }); 59 | const res = { 60 | send: jest.fn(), 61 | type: jest.fn(), 62 | }; 63 | 64 | await sut.requestPluginHandler(req, res); 65 | expect(res.send.mock.calls).toMatchSnapshot(); 66 | expect(logger.error.mock.calls).toEqual([]); 67 | }); 68 | // / this one is actually talking to the wiki for some reason, so ignore it 69 | it.skip('markdown.zip should return markdown and images in zip', async () => { 70 | const logger = makeLogger(); 71 | const req = new MockExpressRequest({ 72 | log: logger, 73 | params: { 74 | plugin: 'html5-notifier-plugin', 75 | format: 'md.zip', 76 | }, 77 | }); 78 | const res = new MockExpressResponse({ }); 79 | 80 | await sut.requestPluginHandler(req, res); 81 | expect(res._getString()).toMatchSnapshot(); 82 | expect(logger.error.mock.calls).toEqual([]); 83 | }); 84 | it('adoc should return asciidoc', async () => { 85 | const logger = makeLogger(); 86 | const req = new MockExpressRequest({ 87 | log: logger, 88 | params: { 89 | plugin: 'html5-notifier-plugin', 90 | format: 'adoc', 91 | }, 92 | }); 93 | const res = { 94 | send: jest.fn(), 95 | type: jest.fn(), 96 | }; 97 | 98 | await sut.requestPluginHandler(req, res); 99 | expect(res.send.mock.calls).toMatchSnapshot(); 100 | expect(logger.error.mock.calls).toEqual([]); 101 | }); 102 | }); 103 | 104 | -------------------------------------------------------------------------------- /reports.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | const axios = require('./axios'); 3 | const {getPullRequests} = require('./graphql.js'); 4 | const {getCached, getAllPluginNamesForRepo} = require('./utils.js'); 5 | const moment = require('moment'); 6 | 7 | const docsUrl = 'http://updates.jenkins.io/plugin-documentation-urls.json'; 8 | 9 | /** 10 | * Get the content of the progress report 11 | * @return {array} of objects representing table rows 12 | */ 13 | async function pluginsReport() { 14 | const updateCenterUrl = 'https://updates.jenkins.io/current/update-center.actual.json'; 15 | 16 | 17 | const documentation = await getContent(docsUrl, 'json'); 18 | const uc = await getContent(updateCenterUrl, 'json'); 19 | const pulls = await getCached('get-pulls', () => getPulls(uc.plugins)); 20 | const report = []; 21 | const recent = []; 22 | Object.keys(uc.plugins).forEach(function(key) { 23 | const plugin = uc.plugins[key]; 24 | const url = documentation[key].url || ''; 25 | plugin.name = key; 26 | plugin.installs = plugin.popularity || 0; 27 | plugin.releaseDate = moment(plugin.releaseTimestamp).format('YYYY-MM-DD'); 28 | if (url.match('https?://github.com/jenkinsci/')) { 29 | plugin.status = 'OK'; 30 | plugin.className = 'success'; 31 | if (pulls['merged'][key]) { 32 | recent.push(key); 33 | } 34 | } else if (plugin.labels.includes('deprecated') || 35 | Object.keys(uc.deprecations).includes(plugin.name)) { 36 | plugin.status = 'deprecated'; 37 | plugin.className = 'success'; 38 | } else if (pulls['merged'][key]) { 39 | plugin.status = 'PR merged'; 40 | plugin.className = 'info'; 41 | plugin.action = pulls['merged'][key]; 42 | } else if (pulls['open'][key]) { 43 | plugin.status = 'PR open'; 44 | plugin.className = 'info'; 45 | plugin.action = pulls['open'][key]; 46 | } else { 47 | plugin.status = 'TODO'; 48 | plugin.action = '/?pluginName=' + plugin.name; 49 | } 50 | report.push(plugin); 51 | }); 52 | report.sort((a, b) => b.installs - a.installs); 53 | 54 | const statuses = report.reduce((statuses, report) => { 55 | statuses[report.status.toLowerCase()] = (statuses[report.status.toLowerCase()] || 0) + 1; 56 | return statuses; 57 | }, {}); 58 | statuses.total = report.length; 59 | return { 60 | plugins: report, 61 | statuses, 62 | recent, 63 | }; 64 | } 65 | 66 | /** 67 | * Gets documentation URL for a plugin from Update Center 68 | * @param {string} pluginId plugin ID 69 | * @return {string} documentation URL 70 | */ 71 | async function getPluginWikiUrl(pluginId) { 72 | const documentation = await getContent(docsUrl, 'json'); 73 | if (documentation[pluginId]) { 74 | return documentation[pluginId].url.replace('//wiki.jenkins-ci.org', '//wiki.jenkins.io'); 75 | } 76 | return ''; 77 | } 78 | 79 | /** 80 | * Gets list of all unreleased pull requests from GitHub project 81 | * @param {plugins} plugins map (plugin name) => plugin properties (see update-center.actual.json) 82 | * @return {object} nested map (state) => (plugin name) => url 83 | */ 84 | async function getPulls(plugins) { 85 | const repoToPlugins = {}; 86 | Object.keys(plugins).forEach(function(key) { 87 | const scm = plugins[key].scm; 88 | repoToPlugins[scm] = repoToPlugins[scm] || []; 89 | repoToPlugins[scm].push(key); 90 | }); 91 | const data = await getPullRequests(); 92 | const columns = data.organization.project.columns.edges; 93 | return {'open': await getPullMap(columns[1], repoToPlugins), 'merged': await getPullMap(columns[2], repoToPlugins)}; 94 | } 95 | 96 | /** 97 | * Gets PRs for one column 98 | * @param {object} column from GraphQL 99 | * @param {object} repoToPlugins map (repo URL) => list of plugin IDs 100 | * @return {object} map (plugin name) => url 101 | */ 102 | async function getPullMap(column, repoToPlugins) { 103 | const projectToPull = {}; 104 | for (const edge of column.node.cards.edges) { 105 | if (!edge.node.content) { 106 | continue; 107 | } 108 | if (!edge.node.content.url) { 109 | continue; 110 | } 111 | const {url} = edge.node.content; 112 | const pluginNames = getAllPluginNamesForRepo(url, repoToPlugins); 113 | pluginNames.forEach(function(pluginName) { 114 | projectToPull[pluginName] = url; 115 | }); 116 | } 117 | return projectToPull; 118 | } 119 | 120 | /** 121 | * Load content from URL, using cache. 122 | * @param {string} url 123 | * @param {string} type 'json' or 'blob' 124 | * @return {object} JSON object or string 125 | */ 126 | async function getContent(url, type) { 127 | return getCached( 128 | url, 129 | () => axios.get(url, {'type': type}).then((response) => response.data), 130 | ); 131 | } 132 | 133 | module.exports = { 134 | pluginsReport, 135 | getPluginWikiUrl, 136 | }; 137 | -------------------------------------------------------------------------------- /test-data/with-classes-removed.html: -------------------------------------------------------------------------------- 1 |

Summary

2 |

This Plugin will notify the ChatWork any message.

3 | 4 | 5 |

6 |

Table of content

7 |

8 |
9 |
10 |

11 |

Usage

12 |

Global Configuration

13 |

14 |

Job Configuration

15 |

16 | 24 | 27 |

example

28 | 33 |

Changelog

34 |

Details https://github.com/jenkinsci/chatwork-plugin/blob/master/CHANGELOG.md

35 |

Version 1.0.9 (Sep 14, 2019)

36 | 42 |

Version 1.0.8 (Jan 27, 2017)

43 | 46 |

Version 1.0.6 (Dec 1, 2016)

47 | 50 |

Version 1.0.5 (Mar 23, 2016)

51 | 54 |

Version 1.0.4 (Nov 11, 2015)

55 | 58 |

Version 1.0.3 (Oct 8, 2015)

59 | 62 |

Version 1.0.2 (Sep 25, 2015)

63 | 66 |

Version 1.0.1 (Sep 25, 2015)

67 | 70 |

Version 1.0.0 (Mar 8, 2015)

71 | 75 |
NOTE
76 |

Default message (below v0.6.2) is removed. If upgrade from v0.x to 1.0.0+, Default message is copied to Success message, Failure message, Unstable message, Not built message and Aborted message

77 |

Version 0.6.2 (Jan 22, 2015)

78 | 81 |

Version 0.6.1 (Jan 14, 2015)

82 | 85 | -------------------------------------------------------------------------------- /test-data/with-classes.html: -------------------------------------------------------------------------------- 1 |

Summary

2 |

This Plugin will notify the ChatWork any message.

3 | 4 | 5 |

6 |

Table of content

7 |

8 |
9 |
10 |

11 |

Usage

12 |

Global Configuration

13 |

14 |

Job Configuration

15 |

16 | 24 | 27 |

example

28 | 33 |

Changelog

34 |

Details https://github.com/jenkinsci/chatwork-plugin/blob/master/CHANGELOG.md

35 |

Version 1.0.9 (Sep 14, 2019)

36 | 42 |

Version 1.0.8 (Jan 27, 2017)

43 | 46 |

Version 1.0.6 (Dec 1, 2016)

47 | 50 |

Version 1.0.5 (Mar 23, 2016)

51 | 54 |

Version 1.0.4 (Nov 11, 2015)

55 | 58 |

Version 1.0.3 (Oct 8, 2015)

59 | 62 |

Version 1.0.2 (Sep 25, 2015)

63 | 66 |

Version 1.0.1 (Sep 25, 2015)

67 | 70 |

Version 1.0.0 (Mar 8, 2015)

71 | 75 |
NOTE
76 |

Default message (below v0.6.2) is removed. If upgrade from v0.x to 1.0.0+, Default message is copied to Success message, Failure message, Unstable message, Not built message and Aborted message

77 |

Version 0.6.2 (Jan 22, 2015)

78 | 81 |

Version 0.6.1 (Jan 14, 2015)

82 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | const Sentry = require('@sentry/node'); 3 | const expressBunyanLogger = require('express-bunyan-logger'); 4 | const archiver = require('archiver'); 5 | const {basename} = require('path'); 6 | const {parse: urlParse} = require('url'); 7 | const { 8 | checkUrl, 9 | convertBody, 10 | decodeEntities, 11 | findImages, 12 | getFormatType, 13 | getUrlAsStream, 14 | recordPandoc, 15 | replaceAsync, 16 | } = require('./utils.js'); 17 | const { 18 | getRawConfluenceContent, 19 | getContentFromConfluencePage, 20 | } = require('./confluence.js'); 21 | const {pluginsReport, getPluginWikiUrl} = require('./reports.js'); 22 | 23 | const validWikiDomains = [ 24 | 'wiki.jenkins-ci.org', // primary 25 | 'wiki.jenkins.io', 26 | ]; 27 | 28 | const supportedArchiveFormats = [ 29 | 'zip', 30 | ]; 31 | 32 | 33 | // server.js 34 | // load the things we need 35 | const express = require('express'); 36 | const app = express(); 37 | 38 | Sentry.init({ 39 | dsn: process.env.SENTRY_DSN, 40 | }); 41 | 42 | const wrap = (fn) => (...args) => fn(...args).catch(args[2]); 43 | // set the view engine to ejs 44 | app.set('view engine', 'ejs'); 45 | app.set('trust proxy', 1); // trust first proxy 46 | 47 | // The request handler must be the first middleware on the app 48 | app.use(Sentry.Handlers.requestHandler()); 49 | app.use(expressBunyanLogger()); 50 | 51 | app.use(express.static('public')); 52 | 53 | app.get('/healthcheck', function healthcheck(req, res) { 54 | res.json({ 55 | 'OK': true, 56 | 'version': require('./package.json').version, 57 | }); 58 | }); 59 | 60 | app.get('/progress', async function(req, res) { 61 | const report = await pluginsReport(); 62 | res.render('progress', report); 63 | }); 64 | 65 | app.get('/progress.json', async function(req, res) { 66 | const report = await pluginsReport(); 67 | res.json(report); 68 | }); 69 | 70 | app.get('/', function(req, res) { 71 | res.render('index'); 72 | }); 73 | 74 | 75 | /** 76 | * processing incoming parameter parts of urls 77 | * @param {request} req 78 | * @return {object} extension and isZip values 79 | */ 80 | function handleParams(req) { 81 | if (!req.params.format) { 82 | req.params.format = 'md'; 83 | } 84 | const formats = req.params.format.split('.'); 85 | const archiveFormat = formats[1] && supportedArchiveFormats.includes(formats[1]) ? formats[1] : ''; 86 | return { 87 | extension: formats[0], 88 | archiveFormat, 89 | }; 90 | } 91 | 92 | /** 93 | * Get the page id from a confluence page 94 | * @param {string} url to look up 95 | * @return {int} pageId 96 | */ 97 | async function getConfluencePageFromId(url) { 98 | const fragment = await getRawConfluenceContent(url); 99 | return getContentFromConfluencePage(url, fragment); 100 | } 101 | 102 | /** 103 | * Handles the /plugin/ action 104 | * @param {request} req 105 | * @param {response} res 106 | */ 107 | async function requestPluginHandler(req, res) { 108 | const {extension, archiveFormat} = handleParams(req); 109 | 110 | const pluginWikiUrl = await getPluginWikiUrl(req.params.plugin); 111 | checkUrl(validWikiDomains, pluginWikiUrl); 112 | const content = await getConfluencePageFromId(pluginWikiUrl); 113 | return processContent(req, res, content, extension, archiveFormat); 114 | } 115 | 116 | /** 117 | * 118 | * @param {request} req 119 | * @param {response} res 120 | */ 121 | async function requestConfluenceUrlHandler(req, res) { 122 | const urlParts = req.originalUrl.replace(/^\/confluence-url\//, '').split('.'); 123 | let archiveFormat = ''; 124 | if (supportedArchiveFormats.includes(urlParts[urlParts.length - 1])) { 125 | archiveFormat = urlParts.pop(); 126 | } 127 | const extension = urlParts.pop(); 128 | getFormatType(extension); 129 | const url = decodeURIComponent(urlParts.join('.')); 130 | checkUrl(validWikiDomains, url); 131 | const content = await getConfluencePageFromId(url); 132 | 133 | return processContent(req, res, content, extension, archiveFormat); 134 | } 135 | 136 | 137 | /** 138 | * Handles the /plugin/ action 139 | * @param {request} req 140 | * @param {response} res 141 | * @param {string} wikiContent content to process 142 | * @param {string} extension which format/extension do we want 143 | * @param {string} archiveFormat do we want a zip 144 | */ 145 | async function processContent(req, res, wikiContent, extension, archiveFormat) { 146 | res.type('text/plain; charset=utf-8'); 147 | const outputType = getFormatType(extension); 148 | const {stdout} = await convertBody(req.log, wikiContent, outputType); 149 | if (archiveFormat) { 150 | const archive = archiver(archiveFormat, { 151 | zlib: {level: 9}, // Sets the compression level. 152 | }); 153 | const files = []; 154 | const images = findImages(wikiContent).map(decodeEntities); 155 | const urlRE = new RegExp('(' + images.map((i) => i.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')).join('|') + ')', 'gi'); 156 | 157 | const content = await replaceAsync(stdout, urlRE, async function(val, grab) { 158 | if (!images.includes(val)) { 159 | return val; 160 | } 161 | 162 | 163 | try { 164 | checkUrl(validWikiDomains, grab); 165 | } catch (e) { 166 | // not a wiki image url, so leave it where it is 167 | return val; 168 | } 169 | 170 | const dir = 'docs/images/'; 171 | const filename = decodeURIComponent(basename(urlParse(grab).pathname)).replace(/\s+/g, '_').substr((250-dir.length)*-1); 172 | if (!filename) { 173 | return val; 174 | } 175 | try { 176 | files.push({ 177 | content: await getUrlAsStream(grab), 178 | filename: `${dir}${filename}`, 179 | }); 180 | return val.replace(grab, `${dir}${filename}`); 181 | } catch (e) { 182 | Sentry.captureException(e); 183 | req.log.error(e); 184 | return val; 185 | } 186 | }); 187 | files.push({ 188 | content: Buffer.from(content), 189 | filename: 'README.' + extension, 190 | }); 191 | files.forEach((file) => { 192 | archive.append(file.content, {name: file.filename}); 193 | }); 194 | archive.on('error', function(err) { 195 | Sentry.captureException(err); 196 | req.log.error(err); 197 | }); 198 | archive.on('warning', function(err) { 199 | Sentry.captureException(err); 200 | }); 201 | res.attachment(`${req.params.plugin}.${archiveFormat}`).type(archiveFormat); 202 | archive.on('end', () => res.end()); // end response when archive stream ends 203 | archive.pipe(res); 204 | archive.finalize(); 205 | return; 206 | } else { 207 | res.send(stdout); 208 | } 209 | } 210 | app.get('/plugin/:plugin([^\\.]+)\.?:format?', wrap(requestPluginHandler)); 211 | app.get('/confluence-url/:plugin([^\\.]+)\.?:format?', wrap(requestConfluenceUrlHandler)); 212 | 213 | 214 | // The error handler must be before any other error middleware and after all controllers 215 | app.use(Sentry.Handlers.errorHandler()); 216 | app.use(function onError(err, req, res, next) { 217 | req.log.error(err.stack); 218 | res.status(err.code || 500).send(err.message); 219 | }); 220 | 221 | 222 | if (typeof require !== 'undefined' && require.main === module) { 223 | recordPandoc().then(() => { 224 | app.listen(3000); 225 | console.log('3000 is the magic port'); 226 | }).catch(console.error); 227 | } else { 228 | module.exports = { 229 | requestPluginHandler, 230 | }; 231 | } 232 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | const bunyan = require('bunyan'); 3 | const axios = require('./axios'); 4 | const {basename} = require('path'); 5 | const {spawn, execFile} = require('child_process'); 6 | const {parse: urlParse} = require('url'); 7 | const createError = require('http-errors'); 8 | const DomParser = require('dom-parser'); 9 | const cheerio = require('cheerio'); 10 | 11 | const logger = bunyan.createLogger({ 12 | name: basename(process.argv[0]) + ':utils', 13 | }); 14 | 15 | 16 | /** 17 | * decodeEntities - https://stackoverflow.com/a/39243641 18 | * @param {string} encodedString string to decode 19 | * @return {string} the decoded one 20 | */ 21 | function decodeEntities(encodedString) { 22 | const htmlEntities = { 23 | nbsp: ' ', 24 | cent: '¢', 25 | pound: '£', 26 | yen: '¥', 27 | euro: '€', 28 | copy: '©', 29 | reg: '®', 30 | lt: '<', 31 | gt: '>', 32 | quot: '"', 33 | amp: '&', 34 | apos: '\'', 35 | }; 36 | 37 | return encodedString.replace(/\&([^;]+);/g, function(entity, entityCode) { 38 | let match; 39 | 40 | if (entityCode in htmlEntities) { 41 | return htmlEntities[entityCode]; 42 | // eslint-disable-next-line no-cond-assign 43 | } else if (match = entityCode.match(/^#x([\da-fA-F]+)$/)) { 44 | return String.fromCharCode(parseInt(match[1], 16)); 45 | // eslint-disable-next-line no-cond-assign 46 | } else if (match = entityCode.match(/^#(\d+)$/)) { 47 | return String.fromCharCode(~~match[1]); 48 | } else { 49 | return entity; 50 | } 51 | }); 52 | } 53 | 54 | /** 55 | * Find those images 56 | * 57 | * @param {string} body html content to find images in 58 | * @return {array} array of string urls 59 | */ 60 | function findImages(body) { 61 | const domParser = new DomParser(); 62 | const dom = domParser.parseFromString(body); 63 | const images = dom.getElementsByTagName('img'); 64 | const ret = images.map((image) => { 65 | return [ 66 | (image.attributes.find((attr) => attr.name === 'data-image-src') || {}).value, 67 | (image.attributes.find((attr) => attr.name === 'src') || {}).value, 68 | ]; 69 | }) 70 | .flat() 71 | .map((url) => !url || url.startsWith('http') ? url : `https://wiki.jenkins.io${url}`) 72 | .filter(Boolean); 73 | return ret; 74 | } 75 | 76 | /** 77 | * Standard string replace function that allows async functions 78 | * 79 | * https://stackoverflow.com/a/48032528 80 | * 81 | * @param {string} str string to be searched 82 | * @param {string} regex regex to search 83 | * @param {function} asyncFn to do the replace 84 | * @return {string} the new string 85 | */ 86 | async function replaceAsync(str, regex, asyncFn) { 87 | const promises = []; 88 | str.replace(regex, (match, ...args) => { 89 | const promise = asyncFn(match, ...args); 90 | promises.push(promise); 91 | }); 92 | const data = await Promise.all(promises); 93 | return str.replace(regex, () => data.shift()); 94 | } 95 | 96 | /** 97 | * Grabs the data from plugins.jenkins.io 98 | * @param {string} url 99 | * @return {Stream} 100 | */ 101 | async function getUrlAsStream(url) { 102 | if (!url) { 103 | throw new Error('url'); 104 | } 105 | 106 | return axios.get(url, {responseType: 'stream'}) 107 | .then((resp) => resp.data); 108 | } 109 | 110 | /** 111 | * Do the main conversion 112 | * 113 | * @param {Logger} log Logger 114 | * @param {str} body The body to be converted 115 | * @param {str} format What format to output as 116 | * @return {string} converted string 117 | */ 118 | async function convertBody(log, body, format) { 119 | return new Promise(function(resolve, reject) { 120 | const command = 'pandoc'; 121 | const args = [ 122 | '-f', 123 | 'html', 124 | '-t', 125 | format+'-raw_html+blank_before_header+link_attributes', 126 | '--atx-headers', 127 | '-o', 128 | '-', 129 | '-', 130 | ]; 131 | log.debug(`${command} ${args.map((a) => `"${a}"`).join(' ')}`); 132 | const p = spawn( 133 | command, args, { 134 | encoding: 'utf8', 135 | env: {...process.env, LANG: 'en_US.UTF-8', LC_CTYPE: 'en_US.UTF-8'}, 136 | stdio: ['pipe', 'pipe', 'pipe'], 137 | }, 138 | ); 139 | p.once('error', reject); 140 | p.once('exit', (code, signal) => { 141 | resolve({ 142 | stderr, 143 | stdout, 144 | }); 145 | }); 146 | 147 | let stderr = ''; 148 | p.stderr.on('data', (data) => stderr += data); 149 | p.stderr.on('end', () => { 150 | if (stderr.trim()) { 151 | log.error(stderr.trim()); 152 | } 153 | }); 154 | let stdout = ''; 155 | p.stdout.on('data', (data) => stdout += data); 156 | p.stdin.write(replaceConfluenceContent(body)); 157 | p.stdin.end(); 158 | }); 159 | } 160 | 161 | /** 162 | * Remove's content that we don't want 163 | * 164 | * @param {*} body page content 165 | * @return {string} processed html 166 | */ 167 | function replaceConfluenceContent(body) { 168 | const $ = cheerio.load(cheerio.load(body).html()); 169 | $('img').removeClass(); 170 | $('a').removeClass(); 171 | 172 | return $.html(); 173 | } 174 | 175 | /** 176 | * Which pandoc format do we want to output as 177 | * @param {string} type Which file extension do we want 178 | * @return {string} 179 | */ 180 | function getFormatType(type) { 181 | if (type === 'md') { 182 | return 'markdown_github'; 183 | } 184 | if (type === 'adoc') { 185 | return 'asciidoc'; 186 | } 187 | throw new Error('Unknown format: ' + type); 188 | } 189 | 190 | /** outputPandocVersion 191 | * @return {promise} 192 | */ 193 | function recordPandoc() { 194 | return new Promise(function(resolve, reject) { 195 | execFile( 196 | 'pandoc', 197 | ['--version'], { 198 | encoding: 'utf8', 199 | env: {...process.env, LANG: 'en_US.UTF-8', LC_CTYPE: 'en_US.UTF-8'}, 200 | }, 201 | (error, stdout, stderr) => { 202 | if (error) { 203 | logger.error(stderr); 204 | reject(error); 205 | return; 206 | } 207 | logger.info(stdout + stderr); 208 | resolve(); 209 | }, 210 | ); 211 | }); 212 | } 213 | 214 | /** 215 | * Checks the given url to see if its valid confluence domain 216 | * @param {string[]} validWikiDomains valid domains 217 | * @param {string} url to check 218 | * @return {boolean} true valid 219 | * @throws error if not valid 220 | */ 221 | function checkUrl(validWikiDomains, url) { 222 | if (!validWikiDomains.includes(urlParse(url).host)) { 223 | throw createError(400, url + ' is not a valid wiki url.'); 224 | } 225 | return true; 226 | } 227 | 228 | const httpCache = {}; 229 | /** 230 | * Returns response from cache, updates cache if required. 231 | * @param {string} url 232 | * @param {function} callback update callback 233 | * @return {object} JSON object or string 234 | */ 235 | async function getCached(url, callback) { 236 | const now = new Date().getTime(); 237 | if (httpCache[url] && httpCache[url].timestamp > now - 60 * 60 * 1000) { 238 | return httpCache[url].data; 239 | } 240 | return callback().then(function(response) { 241 | httpCache[url] = {'data': response, 'timestamp': new Date().getTime()}; 242 | return response; 243 | }); 244 | } 245 | 246 | /** 247 | * Strips out all the extra github url type stuff and returns the plugin name from github 248 | * @param {string} url 249 | * @param {object} repoToPlugins maps repo URLs to lists of plugin IDs 250 | * @return {string} plugin name 251 | */ 252 | function getAllPluginNamesForRepo(url, repoToPlugins) { 253 | const match = url.match(/https?:\/\/github.com\/([^/]*)\/([^/.]*)/); 254 | const byUrl = repoToPlugins[match[0]]; 255 | return byUrl ? byUrl : [match[2].replace(/-plugin$/, '')]; 256 | } 257 | 258 | 259 | module.exports = { 260 | checkUrl, 261 | convertBody, 262 | decodeEntities, 263 | findImages, 264 | replaceConfluenceContent, 265 | getFormatType, 266 | getUrlAsStream, 267 | recordPandoc, 268 | replaceAsync, 269 | getCached, 270 | getAllPluginNamesForRepo, 271 | }; 272 | -------------------------------------------------------------------------------- /public/vendor/bootstrap-table.min.css: -------------------------------------------------------------------------------- 1 | /** 2 | * bootstrap-table - An extended table to integration with some of the most widely used CSS frameworks. (Supports Bootstrap, Semantic UI, Bulma, Material Design, Foundation) 3 | * 4 | * @version v1.15.5 5 | * @homepage https://bootstrap-table.com 6 | * @author wenzhixin (http://wenzhixin.net.cn/) 7 | * @license MIT 8 | */ 9 | 10 | @charset "UTF-8";.bootstrap-table .fixed-table-toolbar::after{content:"";display:block;clear:both}.bootstrap-table .fixed-table-toolbar .bs-bars,.bootstrap-table .fixed-table-toolbar .search,.bootstrap-table .fixed-table-toolbar .columns{position:relative;margin-top:10px;margin-bottom:10px}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group{display:inline-block;margin-left:-1px!important}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group>.btn{border-radius:0}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group:first-child>.btn{border-top-left-radius:4px;border-bottom-left-radius:4px}.bootstrap-table .fixed-table-toolbar .columns .btn-group>.btn-group:last-child>.btn{border-top-right-radius:4px;border-bottom-right-radius:4px}.bootstrap-table .fixed-table-toolbar .columns .dropdown-menu{text-align:left;max-height:300px;overflow:auto;-ms-overflow-style:scrollbar;z-index:1001}.bootstrap-table .fixed-table-toolbar .columns label{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:1.428571429}.bootstrap-table .fixed-table-toolbar .columns-left{margin-right:5px}.bootstrap-table .fixed-table-toolbar .columns-right{margin-left:5px}.bootstrap-table .fixed-table-toolbar .pull-right .dropdown-menu{right:0;left:auto}.bootstrap-table .fixed-table-container{position:relative;clear:both}.bootstrap-table .fixed-table-container .table{width:100%;margin-bottom:0!important}.bootstrap-table .fixed-table-container .table th,.bootstrap-table .fixed-table-container .table td{vertical-align:middle;box-sizing:border-box}.bootstrap-table .fixed-table-container .table thead th{vertical-align:bottom;padding:0;margin:0}.bootstrap-table .fixed-table-container .table thead th:focus{outline:0 solid transparent}.bootstrap-table .fixed-table-container .table thead th.detail{width:30px}.bootstrap-table .fixed-table-container .table thead th .th-inner{padding:.75rem;vertical-align:bottom;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.bootstrap-table .fixed-table-container .table thead th .sortable{cursor:pointer;background-position:right;background-repeat:no-repeat;padding-right:30px}.bootstrap-table .fixed-table-container .table thead th .both{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAQAAADYWf5HAAAAkElEQVQoz7X QMQ5AQBCF4dWQSJxC5wwax1Cq1e7BAdxD5SL+Tq/QCM1oNiJidwox0355mXnG/DrEtIQ6azioNZQxI0ykPhTQIwhCR+BmBYtlK7kLJYwWCcJA9M4qdrZrd8pPjZWPtOqdRQy320YSV17OatFC4euts6z39GYMKRPCTKY9UnPQ6P+GtMRfGtPnBCiqhAeJPmkqAAAAAElFTkSuQmCC")}.bootstrap-table .fixed-table-container .table thead th .asc{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZ0lEQVQ4y2NgGLKgquEuFxBPAGI2ahhWCsS/gDibUoO0gPgxEP8H4ttArEyuQYxAPBdqEAxPBImTY5gjEL9DM+wTENuQahAvEO9DMwiGdwAxOymGJQLxTyD+jgWDxCMZRsEoGAVoAADeemwtPcZI2wAAAABJRU5ErkJggg==")}.bootstrap-table .fixed-table-container .table thead th .desc{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZUlEQVQ4y2NgGAWjYBSggaqGu5FA/BOIv2PBIPFEUgxjB+IdQPwfC94HxLykus4GiD+hGfQOiB3J8SojEE9EM2wuSJzcsFMG4ttQgx4DsRalkZENxL+AuJQaMcsGxBOAmGvopk8AVz1sLZgg0bsAAAAASUVORK5CYII= ")}.bootstrap-table .fixed-table-container .table tbody tr.selected td{background-color:rgba(0,0,0,0.075)}.bootstrap-table .fixed-table-container .table tbody tr.no-records-found{text-align:center}.bootstrap-table .fixed-table-container .table tbody tr .card-view{display:flex}.bootstrap-table .fixed-table-container .table tbody tr .card-view .card-view-title{font-weight:bold;display:inline-block;min-width:30%;text-align:left!important}.bootstrap-table .fixed-table-container .table .bs-checkbox{text-align:center}.bootstrap-table .fixed-table-container .table .bs-checkbox label{margin-bottom:0}.bootstrap-table .fixed-table-container .table input[type=radio],.bootstrap-table .fixed-table-container .table input[type=checkbox]{margin:0 auto!important}.bootstrap-table .fixed-table-container .table.table-sm .th-inner{padding:.3rem}.bootstrap-table .fixed-table-container.fixed-height:not(.has-footer){border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height.has-card-view{border-top:1px solid #dee2e6;border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .fixed-table-border{border-left:1px solid #dee2e6;border-right:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .table thead th{border-bottom:1px solid #dee2e6}.bootstrap-table .fixed-table-container.fixed-height .table-dark thead th{border-bottom:1px solid #32383e}.bootstrap-table .fixed-table-container .fixed-table-header{overflow:hidden}.bootstrap-table .fixed-table-container .fixed-table-body{overflow-x:auto;overflow-y:auto;height:100%}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading{align-items:center;background:#fff;display:none;justify-content:center;position:absolute;bottom:0;width:100%;z-index:1000}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap{align-items:baseline;display:flex;justify-content:center}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .loading-text{font-size:2rem;margin-right:6px}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap{align-items:center;display:flex;justify-content:center}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-dot,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::after,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::before{content:"";animation-duration:1.5s;animation-iteration-count:infinite;animation-name:LOADING;background:#212529;border-radius:50%;display:block;height:5px;margin:0 4px;opacity:0;width:5px}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-dot{animation-delay:.3s}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::after{animation-delay:.6s}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark{background:#212529}.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-dot,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-wrap::after,.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-wrap::before{background:#fff}.bootstrap-table .fixed-table-container .fixed-table-footer{overflow:hidden}.bootstrap-table .fixed-table-pagination::after{content:"";display:block;clear:both}.bootstrap-table .fixed-table-pagination>.pagination-detail,.bootstrap-table .fixed-table-pagination>.pagination{margin-top:10px;margin-bottom:10px}.bootstrap-table .fixed-table-pagination>.pagination-detail .pagination-info{line-height:34px;margin-right:5px}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list{display:inline-block}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list .btn-group{position:relative;display:inline-block;vertical-align:middle}.bootstrap-table .fixed-table-pagination>.pagination-detail .page-list .btn-group .dropdown-menu{margin-bottom:0}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination{margin:0}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination a{padding:6px 12px;line-height:1.428571429}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a{color:#c8c8c8}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a::before{content:"⬅"}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.page-intermediate a::after{content:"➡"}.bootstrap-table .fixed-table-pagination>.pagination ul.pagination li.disabled a{pointer-events:none;cursor:default}.bootstrap-table.fullscreen{position:fixed;top:0;left:0;z-index:1050;width:100%!important;background:#fff;height:calc(100vh);overflow-y:scroll}div.fixed-table-scroll-inner{width:100%;height:200px}div.fixed-table-scroll-outer{top:0;left:0;visibility:hidden;width:200px;height:150px;overflow:hidden}@keyframes LOADING{0%{opacity:0}50%{opacity:1}to{opacity:0}} -------------------------------------------------------------------------------- /__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`/plugin/:pluginName handle the basics 1`] = ` 4 | Array [ 5 | Array [ 6 | "# HTML5 Notifier Plugin 7 | 8 | 9 | 10 | Provides [W3C Web 11 | Notifications](http://dev.w3.org/2006/webapi/WebNotifications/publish/Notifications.html) 12 | support for builds. 13 | 14 | ### Browser compatibility 15 | 16 | This plugin is compatible with Google Chrome, Safari, and Firefox 17 | 18 | Other browsers do not support HTML5 notifications right now. You can 19 | check this website for details 20 | :  21 | 22 | ### Screenshots 23 | 24 | | | | 25 | |----------------------|---------------------------------------------------------------------------------------------------------------------------------------------| 26 | | Safari Notification | ![](https://wiki.jenkins.io/download/attachments/58917110/screenshot-notifications-1.3.png?version=1&modificationDate=1407001198000&api=v2) | 27 | | System Configuration | ![](https://wiki.jenkins.io/download/attachments/58917110/screenshot-configure-1.1.png?version=1&modificationDate=1328215062000&api=v2) | 28 | | Job Configuration | ![](https://wiki.jenkins.io/download/attachments/58917110/screenshot-job-configure-1.1.png?version=1&modificationDate=1328215063000&api=v2) | 29 | 30 | 31 | 32 | ------------------------------------------------------------------------ 33 | 34 | 35 | 36 | ### Changelog 37 | 38 | #### 1.5 (released 2015-03-06) 39 | 40 | - [JENKINS-27222](https://issues.jenkins-ci.org/browse/JENKINS-27222) - 41 | Fixes global configuration not saving/loading. 42 | 43 | #### 1.3 (released 2014-08-01) 44 | 45 | - [JENKINS-16236](https://issues.jenkins-ci.org/browse/JENKINS-16236) - 46 | upgrade to use current html5 notification spec. Removes html in the 47 | notifications but now works again. 48 | 49 | #### 1.2 (released 2012-04-06) 50 | 51 | - updated minimum required core to v1.455 to pick up prototype.js:1.7 52 | - [JENKINS-13138](https://issues.jenkins-ci.org/browse/JENKINS-13138) - 53 | Verify action doesn't work with CSRF option 54 | - [JENKINS-13322](https://issues.jenkins-ci.org/browse/JENKINS-13322) - 55 | Configure tab freezes when using both the HTML5 notifier plugin and 56 | the Android Emulator Plugin 57 | - [JENKINS-12538](https://issues.jenkins-ci.org/browse/JENKINS-12538) - 58 | java.lang.String cannot be cast to net.sf.json.JSONArray 59 | - [JENKINS-13038](https://issues.jenkins-ci.org/browse/JENKINS-13038) - 60 | HTML5 notifier plugin breaks Jenkins with CSRF protection 61 | 62 | #### 1.1 (released 2012-02-02) 63 | 64 | - JENKINS-11618 - prototype.js 65 | - added per-job job property to skip notifications 66 | - added ability to only notify on different build result than previous 67 | - cleaned up the notification area html 68 | 69 | #### 1.0 (released 2011-10-19) 70 | 71 | - initial implementation 72 | - fully I18N compliant 73 | ", 74 | ], 75 | ] 76 | `; 77 | 78 | exports[`/plugin/:pluginName markdown should return markdown 1`] = ` 79 | Array [ 80 | Array [ 81 | "# HTML5 Notifier Plugin 82 | 83 | 84 | 85 | Provides [W3C Web 86 | Notifications](http://dev.w3.org/2006/webapi/WebNotifications/publish/Notifications.html) 87 | support for builds. 88 | 89 | ### Browser compatibility 90 | 91 | This plugin is compatible with Google Chrome, Safari, and Firefox 92 | 93 | Other browsers do not support HTML5 notifications right now. You can 94 | check this website for details 95 | :  96 | 97 | ### Screenshots 98 | 99 | | | | 100 | |----------------------|---------------------------------------------------------------------------------------------------------------------------------------------| 101 | | Safari Notification | ![](https://wiki.jenkins.io/download/attachments/58917110/screenshot-notifications-1.3.png?version=1&modificationDate=1407001198000&api=v2) | 102 | | System Configuration | ![](https://wiki.jenkins.io/download/attachments/58917110/screenshot-configure-1.1.png?version=1&modificationDate=1328215062000&api=v2) | 103 | | Job Configuration | ![](https://wiki.jenkins.io/download/attachments/58917110/screenshot-job-configure-1.1.png?version=1&modificationDate=1328215063000&api=v2) | 104 | 105 | 106 | 107 | ------------------------------------------------------------------------ 108 | 109 | 110 | 111 | ### Changelog 112 | 113 | #### 1.5 (released 2015-03-06) 114 | 115 | - [JENKINS-27222](https://issues.jenkins-ci.org/browse/JENKINS-27222) - 116 | Fixes global configuration not saving/loading. 117 | 118 | #### 1.3 (released 2014-08-01) 119 | 120 | - [JENKINS-16236](https://issues.jenkins-ci.org/browse/JENKINS-16236) - 121 | upgrade to use current html5 notification spec. Removes html in the 122 | notifications but now works again. 123 | 124 | #### 1.2 (released 2012-04-06) 125 | 126 | - updated minimum required core to v1.455 to pick up prototype.js:1.7 127 | - [JENKINS-13138](https://issues.jenkins-ci.org/browse/JENKINS-13138) - 128 | Verify action doesn't work with CSRF option 129 | - [JENKINS-13322](https://issues.jenkins-ci.org/browse/JENKINS-13322) - 130 | Configure tab freezes when using both the HTML5 notifier plugin and 131 | the Android Emulator Plugin 132 | - [JENKINS-12538](https://issues.jenkins-ci.org/browse/JENKINS-12538) - 133 | java.lang.String cannot be cast to net.sf.json.JSONArray 134 | - [JENKINS-13038](https://issues.jenkins-ci.org/browse/JENKINS-13038) - 135 | HTML5 notifier plugin breaks Jenkins with CSRF protection 136 | 137 | #### 1.1 (released 2012-02-02) 138 | 139 | - JENKINS-11618 - prototype.js 140 | - added per-job job property to skip notifications 141 | - added ability to only notify on different build result than previous 142 | - cleaned up the notification area html 143 | 144 | #### 1.0 (released 2011-10-19) 145 | 146 | - initial implementation 147 | - fully I18N compliant 148 | ", 149 | ], 150 | ] 151 | `; 152 | 153 | exports[`/plugin/:pluginName adoc should return asciidoc 1`] = ` 154 | Array [ 155 | Array [ 156 | "[[HTML5NotifierPlugin-HTML5NotifierPlugin]] 157 | == HTML5 Notifier Plugin 158 | 159 | + 160 | 161 | [.conf-macro .output-inline]#Provides 162 | http://dev.w3.org/2006/webapi/WebNotifications/publish/Notifications.html[W3C 163 | Web Notifications] support for builds.# 164 | 165 | [[HTML5NotifierPlugin-Browsercompatibility]] 166 | === Browser compatibility 167 | 168 | This plugin is compatible with Google Chrome, Safari, and Firefox 169 | 170 | Other browsers do not support HTML5 notifications right now. You can 171 | check this website for details : http://caniuse.com/#feat=notifications 172 | 173 | [[HTML5NotifierPlugin-Screenshots]] 174 | === Screenshots 175 | 176 | [cols=\\",\\",] 177 | |=== 178 | |Safari Notification 179 | |[.confluence-embedded-file-wrapper]#image:https://wiki.jenkins.io/download/attachments/58917110/screenshot-notifications-1.3.png?version=1&modificationDate=1407001198000&api=v2[image]# 180 | 181 | |System Configuration 182 | |[.confluence-embedded-file-wrapper]#image:https://wiki.jenkins.io/download/attachments/58917110/screenshot-configure-1.1.png?version=1&modificationDate=1328215062000&api=v2[image]# 183 | 184 | |Job Configuration 185 | |[.confluence-embedded-file-wrapper]#image:https://wiki.jenkins.io/download/attachments/58917110/screenshot-job-configure-1.1.png?version=1&modificationDate=1328215063000&api=v2[image]# 186 | |=== 187 | 188 | + 189 | 190 | ''''' 191 | 192 | + 193 | 194 | [[HTML5NotifierPlugin-Changelog]] 195 | === Changelog 196 | 197 | [[HTML5NotifierPlugin-1.5(released2015-03-06)]] 198 | ==== 1.5 (released 2015-03-06) 199 | 200 | * https://issues.jenkins-ci.org/browse/JENKINS-27222[JENKINS-27222] - 201 | Fixes global configuration not saving/loading. 202 | 203 | [[HTML5NotifierPlugin-1.3(released2014-08-01)]] 204 | ==== 1.3 (released 2014-08-01) 205 | 206 | * https://issues.jenkins-ci.org/browse/JENKINS-16236[JENKINS-16236] - 207 | upgrade to use current html5 notification spec. Removes html in the 208 | notifications but now works again. 209 | 210 | [[HTML5NotifierPlugin-1.2(released2012-04-06)]] 211 | ==== 1.2 (released 2012-04-06) 212 | 213 | * updated minimum required core to v1.455 to pick up prototype.js:1.7 214 | * https://issues.jenkins-ci.org/browse/JENKINS-13138[JENKINS-13138] - 215 | Verify action doesn't work with CSRF option 216 | * https://issues.jenkins-ci.org/browse/JENKINS-13322[JENKINS-13322] - 217 | Configure tab freezes when using both the HTML5 notifier plugin and the 218 | Android Emulator Plugin 219 | * https://issues.jenkins-ci.org/browse/JENKINS-12538[JENKINS-12538] - 220 | java.lang.String cannot be cast to net.sf.json.JSONArray 221 | * https://issues.jenkins-ci.org/browse/JENKINS-13038[JENKINS-13038] - 222 | HTML5 notifier plugin breaks Jenkins with CSRF protection 223 | 224 | [[HTML5NotifierPlugin-1.1(released2012-02-02)]] 225 | ==== 1.1 (released 2012-02-02) 226 | 227 | * JENKINS-11618 - prototype.js 228 | * added per-job job property to skip notifications 229 | * added ability to only notify on different build result than previous 230 | * cleaned up the notification area html 231 | 232 | [[HTML5NotifierPlugin-1.0(released2011-10-19)]] 233 | ==== 1.0 (released 2011-10-19) 234 | 235 | * initial implementation 236 | * fully I18N compliant 237 | ", 238 | ], 239 | ] 240 | `; 241 | 242 | exports[`/plugin/:pluginName markdown.zip should return markdown and images in zip 1`] = `""`; 243 | -------------------------------------------------------------------------------- /__testData/cron_column.json: -------------------------------------------------------------------------------- 1 | {"firstRelease":"2014-06-12T14:17:40.20Z","buildDate":"2014-06-16","categories":["userInterface"],"dependencies":[{"name":"ant","title":"Ant","optional":false,"version":"1.0","implied":true},{"name":"javadoc","title":"Javadoc","optional":false,"version":"1.0","implied":true},{"name":"external-monitor-job","title":"External Monitor Job Type","optional":false,"version":"1.0","implied":true},{"name":"ldap","title":"LDAP","optional":false,"version":"1.0","implied":true},{"name":"pam-auth","title":"PAM Authentication","optional":false,"version":"1.0","implied":true},{"name":"mailer","title":"Mailer","optional":false,"version":"1.2","implied":true},{"name":"matrix-auth","title":"Matrix Authorization Strategy","optional":false,"version":"1.0.2","implied":true},{"name":"windows-slaves","title":"WMI Windows Agents","optional":false,"version":"1.0","implied":true},{"name":"antisamy-markup-formatter","title":"OWASP Markup Formatter","optional":false,"version":"1.0","implied":true},{"name":"matrix-project","title":"Matrix Project","optional":false,"version":"1.0","implied":true},{"name":"junit","title":"JUnit","optional":false,"version":"1.0","implied":true},{"name":"bouncycastle-api","title":"bouncycastle API","optional":false,"version":"2.16.0","implied":true},{"name":"command-launcher","title":"Command Agent Launcher","optional":false,"version":"1.0","implied":true},{"name":"jdk-tool","title":"Oracle Java SE Development Kit Installer","optional":false,"version":"1.0","implied":true},{"name":"jaxb","title":"JAXB","optional":false,"version":"2.3.0","implied":true},{"name":"trilead-api","title":"Trilead API","optional":false,"version":"1.0.4","implied":true}],"maintainers":[{"id":"eelco_de_vlieger","name":"Eelco de Vlieger","email":"eelcodevlieger@hotmail.com"}],"excerpt":"Column showing the cron trigger expressions that can be configured on a job (Subversion, Scheduled Builds etc).","gav":"org.jenkins-ci.plugins:cron_column:1.4","labels":["listview-column"],"name":"cron_column","previousTimestamp":"2014-06-12T14:17:40.00Z","previousVersion":"1.1","releaseTimestamp":"2014-06-16T13:49:40.00Z","requiredCore":"1.424","scm":{"issues":null,"link":"https://github.com/jenkinsci/cron_column-plugin","inLatestRelease":null,"sinceLatestRelease":null,"pullRequests":null},"sha1":"EvJHANv5RiQIWz1iRIBs2BlWyUs=","stats":{"installations":[{"timestamp":1267401600000,"total":3},{"timestamp":1270080000000,"total":59},{"timestamp":1272672000000,"total":133},{"timestamp":1275350400000,"total":183},{"timestamp":1277942400000,"total":245},{"timestamp":1280620800000,"total":197},{"timestamp":1283299200000,"total":288},{"timestamp":1285891200000,"total":359},{"timestamp":1288569600000,"total":403},{"timestamp":1291161600000,"total":437},{"timestamp":1293840000000,"total":515},{"timestamp":1296518400000,"total":540},{"timestamp":1298937600000,"total":578},{"timestamp":1301616000000,"total":560},{"timestamp":1304208000000,"total":604},{"timestamp":1306886400000,"total":623},{"timestamp":1309478400000,"total":643},{"timestamp":1312156800000,"total":705},{"timestamp":1314835200000,"total":761},{"timestamp":1317427200000,"total":819},{"timestamp":1320105600000,"total":834},{"timestamp":1322697600000,"total":816},{"timestamp":1325376000000,"total":871},{"timestamp":1328054400000,"total":947},{"timestamp":1330560000000,"total":1041},{"timestamp":1333238400000,"total":1054},{"timestamp":1335830400000,"total":1103},{"timestamp":1338508800000,"total":1094},{"timestamp":1341100800000,"total":1153},{"timestamp":1343779200000,"total":1179},{"timestamp":1346457600000,"total":1249},{"timestamp":1349049600000,"total":1331},{"timestamp":1351728000000,"total":1316},{"timestamp":1354320000000,"total":1316},{"timestamp":1356998400000,"total":1418},{"timestamp":1359676800000,"total":1457},{"timestamp":1362096000000,"total":1564},{"timestamp":1364774400000,"total":1631},{"timestamp":1367366400000,"total":1620},{"timestamp":1370044800000,"total":1632},{"timestamp":1372636800000,"total":1700},{"timestamp":1375315200000,"total":1736},{"timestamp":1377993600000,"total":1752},{"timestamp":1380585600000,"total":1786},{"timestamp":1383264000000,"total":1787},{"timestamp":1385856000000,"total":1754},{"timestamp":1388534400000,"total":1825},{"timestamp":1391212800000,"total":1879},{"timestamp":1393632000000,"total":1954},{"timestamp":1396310400000,"total":1930},{"timestamp":1398902400000,"total":2004},{"timestamp":1401580800000,"total":2044},{"timestamp":1404172800000,"total":2140},{"timestamp":1406851200000,"total":2132},{"timestamp":1409529600000,"total":2202},{"timestamp":1412121600000,"total":2287},{"timestamp":1414800000000,"total":2298},{"timestamp":1417392000000,"total":2293},{"timestamp":1420070400000,"total":2377},{"timestamp":1422748800000,"total":2416},{"timestamp":1425168000000,"total":2559},{"timestamp":1427846400000,"total":2503},{"timestamp":1430438400000,"total":2491},{"timestamp":1433116800000,"total":2542},{"timestamp":1435708800000,"total":2603},{"timestamp":1438387200000,"total":2561},{"timestamp":1441065600000,"total":2667},{"timestamp":1443657600000,"total":2695},{"timestamp":1446336000000,"total":2714},{"timestamp":1448928000000,"total":2697},{"timestamp":1451606400000,"total":2738},{"timestamp":1454284800000,"total":2539},{"timestamp":1456790400000,"total":2474},{"timestamp":1459468800000,"total":2591},{"timestamp":1462060800000,"total":2639},{"timestamp":1464739200000,"total":2711},{"timestamp":1467331200000,"total":2688},{"timestamp":1470009600000,"total":2655},{"timestamp":1472688000000,"total":2759},{"timestamp":1475280000000,"total":2713},{"timestamp":1477958400000,"total":2749},{"timestamp":1480550400000,"total":2678},{"timestamp":1483228800000,"total":2770},{"timestamp":1485907200000,"total":2751},{"timestamp":1488326400000,"total":2843},{"timestamp":1491004800000,"total":2763},{"timestamp":1493596800000,"total":2784},{"timestamp":1496275200000,"total":2833},{"timestamp":1498867200000,"total":2793},{"timestamp":1501545600000,"total":2810},{"timestamp":1504224000000,"total":2826},{"timestamp":1506816000000,"total":2802},{"timestamp":1509494400000,"total":2851},{"timestamp":1512086400000,"total":2725},{"timestamp":1514764800000,"total":2758},{"timestamp":1517443200000,"total":2729},{"timestamp":1519862400000,"total":2713},{"timestamp":1522540800000,"total":2709},{"timestamp":1525132800000,"total":2650},{"timestamp":1527811200000,"total":2652},{"timestamp":1530403200000,"total":2643},{"timestamp":1533081600000,"total":2668},{"timestamp":1535760000000,"total":2648},{"timestamp":1538352000000,"total":2823},{"timestamp":1541030400000,"total":2811},{"timestamp":1543640400000,"total":2732},{"timestamp":1546318800000,"total":2825},{"timestamp":1548997200000,"total":2817},{"timestamp":1551416400000,"total":2931},{"timestamp":1554091200000,"total":2985},{"timestamp":1556683200000,"total":2952},{"timestamp":1559361600000,"total":2918},{"timestamp":1561953600000,"total":2966},{"timestamp":1564632000000,"total":2929},{"timestamp":1567310400000,"total":2886}],"installationsPercentage":[{"timestamp":1267401600000,"percentage":0.015202189115232594},{"timestamp":1270080000000,"percentage":0.28962741151637134},{"timestamp":1272672000000,"percentage":0.6350570596380652},{"timestamp":1275350400000,"percentage":0.8378353630619907},{"timestamp":1277942400000,"percentage":1.0736196319018405},{"timestamp":1280620800000,"percentage":2.2798287235273693},{"timestamp":1283299200000,"percentage":2.254756126203711},{"timestamp":1285891200000,"percentage":2.2424886001624085},{"timestamp":1288569600000,"percentage":2.1887899196176406},{"timestamp":1291161600000,"percentage":2.1973049074818984},{"timestamp":1293840000000,"percentage":2.300236723390951},{"timestamp":1296518400000,"percentage":2.4205477609933213},{"timestamp":1298937600000,"percentage":2.503898804366661},{"timestamp":1301616000000,"percentage":2.4714241581711462},{"timestamp":1304208000000,"percentage":2.5205525184659683},{"timestamp":1306886400000,"percentage":2.4909040022390148},{"timestamp":1309478400000,"percentage":2.5512835773519025},{"timestamp":1312156800000,"percentage":2.5996533795493932},{"timestamp":1314835200000,"percentage":2.6453003337041157},{"timestamp":1317427200000,"percentage":2.6990508832059055},{"timestamp":1320105600000,"percentage":2.593283582089552},{"timestamp":1322697600000,"percentage":2.5734830326731424},{"timestamp":1325376000000,"percentage":2.4831085896741456},{"timestamp":1328054400000,"percentage":2.529515465569742},{"timestamp":1330560000000,"percentage":2.598407508174625},{"timestamp":1333238400000,"percentage":2.593631576357104},{"timestamp":1335830400000,"percentage":2.571575118903292},{"timestamp":1338508800000,"percentage":2.5138445276775663},{"timestamp":1341100800000,"percentage":2.5503771372956714},{"timestamp":1343779200000,"percentage":2.5079236774371956},{"timestamp":1346457600000,"percentage":2.5932231542230713},{"timestamp":1349049600000,"percentage":2.544446568533741},{"timestamp":1351728000000,"percentage":2.464742569250651},{"timestamp":1354320000000,"percentage":2.5572763840578303},{"timestamp":1356998400000,"percentage":2.4923542025521144},{"timestamp":1359676800000,"percentage":2.5412495203544143},{"timestamp":1362096000000,"percentage":2.5330806731127415},{"timestamp":1364774400000,"percentage":2.5415277215071526},{"timestamp":1367366400000,"percentage":2.4810475534114405},{"timestamp":1370044800000,"percentage":2.4832245401013373},{"timestamp":1372636800000,"percentage":2.463553893864302},{"timestamp":1375315200000,"percentage":2.540648919199754},{"timestamp":1377993600000,"percentage":2.5050401063784156},{"timestamp":1380585600000,"percentage":2.407235183912229},{"timestamp":1383264000000,"percentage":2.417641885950078},{"timestamp":1385856000000,"percentage":2.4326985756092148},{"timestamp":1388534400000,"percentage":2.3683459212541202},{"timestamp":1391212800000,"percentage":2.390007504547247},{"timestamp":1393632000000,"percentage":2.3613293051359516},{"timestamp":1396310400000,"percentage":2.3096622866853354},{"timestamp":1398902400000,"percentage":2.3533556455874582},{"timestamp":1401580800000,"percentage":2.3727160866436052},{"timestamp":1404172800000,"percentage":2.340050956249795},{"timestamp":1406851200000,"percentage":2.3340084295801633},{"timestamp":1409529600000,"percentage":2.2954955330615987},{"timestamp":1412121600000,"percentage":2.3101243446903506},{"timestamp":1414800000000,"percentage":2.3268058565035137},{"timestamp":1417392000000,"percentage":2.3239786352073133},{"timestamp":1420070400000,"percentage":2.3079462482522914},{"timestamp":1422748800000,"percentage":2.3258276615612696},{"timestamp":1425168000000,"percentage":2.2750104460229545},{"timestamp":1427846400000,"percentage":2.2399012045173876},{"timestamp":1430438400000,"percentage":2.230160435467698},{"timestamp":1433116800000,"percentage":2.1621897487368797},{"timestamp":1435708800000,"percentage":2.1519154775880027},{"timestamp":1438387200000,"percentage":2.1356972496956153},{"timestamp":1441065600000,"percentage":2.1589023353705428},{"timestamp":1443657600000,"percentage":2.1379386939138163},{"timestamp":1446336000000,"percentage":2.1368564432441794},{"timestamp":1448928000000,"percentage":2.1250443210022456},{"timestamp":1451606400000,"percentage":2.1584548679542768},{"timestamp":1454284800000,"percentage":2.355571636653771},{"timestamp":1456790400000,"percentage":2.258741897197115},{"timestamp":1459468800000,"percentage":2.0998289988734995},{"timestamp":1462060800000,"percentage":2.025637089346024},{"timestamp":1464739200000,"percentage":2.015478518166071},{"timestamp":1467331200000,"percentage":2.064373430408036},{"timestamp":1470009600000,"percentage":2.0081688223281144},{"timestamp":1472688000000,"percentage":2.0665892663196135},{"timestamp":1475280000000,"percentage":2.0437065439287676},{"timestamp":1477958400000,"percentage":2.013639127154462},{"timestamp":1480550400000,"percentage":2.0024525931686306},{"timestamp":1483228800000,"percentage":1.9647758949660596},{"timestamp":1485907200000,"percentage":1.9717320565932255},{"timestamp":1488326400000,"percentage":1.8939444407434547},{"timestamp":1491004800000,"percentage":1.9170060569898217},{"timestamp":1493596800000,"percentage":1.8454807596698817},{"timestamp":1496275200000,"percentage":1.8780992283418632},{"timestamp":1498867200000,"percentage":1.8574924849033014},{"timestamp":1501545600000,"percentage":1.8202311240089133},{"timestamp":1504224000000,"percentage":1.8246384297520661},{"timestamp":1506816000000,"percentage":1.8015701049951456},{"timestamp":1509494400000,"percentage":1.7689067027355698},{"timestamp":1512086400000,"percentage":1.784989060800985},{"timestamp":1514764800000,"percentage":1.7292294959653403},{"timestamp":1517443200000,"percentage":1.778881566510876},{"timestamp":1519862400000,"percentage":1.6511371728003603},{"timestamp":1522540800000,"percentage":1.6558679706601467},{"timestamp":1525132800000,"percentage":1.5971456295465916},{"timestamp":1527811200000,"percentage":1.5671350738066254},{"timestamp":1530403200000,"percentage":1.4781879194630871},{"timestamp":1533081600000,"percentage":1.4560457552009431},{"timestamp":1535760000000,"percentage":1.3747060319899493},{"timestamp":1538352000000,"percentage":1.3104024509121293},{"timestamp":1541030400000,"percentage":1.258551524051721},{"timestamp":1543640400000,"percentage":1.2463617368771613},{"timestamp":1546318800000,"percentage":1.2136078736301268},{"timestamp":1548997200000,"percentage":1.231071779744346},{"timestamp":1551416400000,"percentage":1.163266035092454},{"timestamp":1554091200000,"percentage":1.17945496141582},{"timestamp":1556683200000,"percentage":1.145504708909093},{"timestamp":1559361600000,"percentage":1.1593074349826382},{"timestamp":1561953600000,"percentage":1.1152220668080435},{"timestamp":1564632000000,"percentage":1.1165028188932555},{"timestamp":1567310400000,"percentage":1.0856235752601209}],"installationsPerVersion":[{"version":"1.003","total":189},{"version":"1.1","total":1},{"version":"1.4","total":2690},{"version":"1.5-SNAPSHOT (private)","total":6}],"installationsPercentagePerVersion":[{"version":"1.003","percentage":0.07109593060435303},{"version":"1.1","percentage":3.761689449965769E-4},{"version":"1.4","percentage":1.0118944620407917},{"version":"1.5-SNAPSHOT (private)","percentage":0.0022570136699794612}],"currentInstalls":2886,"trend":-43},"title":"Cron Column","url":"http://updates.jenkins-ci.org/download/plugins/cron_column/1.4/cron_column.hpi","version":"1.4","securityWarnings":null,"wiki":{"content":"

Cron Column

\n

View column showing the cron trigger expressions that can be configured on a job
(Subversion, Scheduled Builds, etc.)
This is a ListViewColumn plugin that adds a column to a jobs overview page.
The column displays the cron-like expression of each Trigger that can be configured on a job (Subversion, Scheduled Builds etc).

\n

Plugin Source: https://hudson.dev.java.net/svn/hudson/trunk/hudson/plugins/cron_column

\n

\n

How to Use

\n

Once you have installed this plugin, you still have to update a view to add this column.  The pages Changing the Columns of a View and Editing or Replacing the All View will show you how.

\n

Changelog

\n

Version 1.4 (Jun 16 2014)

\n

Same as 1.1, just released to make sure it is considered newer than 1.003 (a nonstandard version string).

\n

Version 1.1 (Jun 12 2014)

\n \n

Version 1.003 (6 Jul, 2010)

\n
    \n
  • Plugin now works with other project types (previously only worked with Freestyle Projects)
  • \n
","url":"https://wiki.jenkins-ci.org/display/JENKINS/Cron+Column+Plugin"}} -------------------------------------------------------------------------------- /__testData/html5-notifier-plugin.json: -------------------------------------------------------------------------------- 1 | {"firstRelease":"2011-10-20T03:57:55.00Z","buildDate":"2015-03-06","categories":["buildManagement","adminTools"],"dependencies":[{"name":"external-monitor-job","title":"External Monitor Job Type","optional":false,"version":"1.0","implied":true},{"name":"ldap","title":"LDAP","optional":false,"version":"1.0","implied":true},{"name":"pam-auth","title":"PAM Authentication","optional":false,"version":"1.0","implied":true},{"name":"mailer","title":"Mailer","optional":false,"version":"1.2","implied":true},{"name":"matrix-auth","title":"Matrix Authorization Strategy","optional":false,"version":"1.0.2","implied":true},{"name":"windows-slaves","title":"WMI Windows Agents","optional":false,"version":"1.0","implied":true},{"name":"antisamy-markup-formatter","title":"OWASP Markup Formatter","optional":false,"version":"1.0","implied":true},{"name":"matrix-project","title":"Matrix Project","optional":false,"version":"1.0","implied":true},{"name":"junit","title":"JUnit","optional":false,"version":"1.0","implied":true},{"name":"bouncycastle-api","title":"bouncycastle API","optional":false,"version":"2.16.0","implied":true},{"name":"command-launcher","title":"Command Agent Launcher","optional":false,"version":"1.0","implied":true},{"name":"jdk-tool","title":"Oracle Java SE Development Kit Installer","optional":false,"version":"1.0","implied":true},{"name":"jaxb","title":"JAXB","optional":false,"version":"2.3.0","implied":true},{"name":"trilead-api","title":"Trilead API","optional":false,"version":"1.0.4","implied":true}],"maintainers":[{"id":"jieryn","name":"Jesse Farinacci","email":"jieryn@gmail.com"},{"id":"halkeye","name":"Gavin Mogan","email":"halkeye@gmail.com"}],"excerpt":"The HTML5 Notifier Plugin provides W3C Web Notifications support for builds.","gav":"org.jenkins-ci.plugins:html5-notifier-plugin:1.5","labels":["notifier","page-decorator"],"name":"html5-notifier-plugin","previousTimestamp":"2014-08-02T05:39:42.00Z","previousVersion":"1.3","releaseTimestamp":"2015-03-06T07:49:55.00Z","requiredCore":"1.455","scm":{"issues":null,"link":"https://github.com/jenkinsci/html5-notifier-plugin","inLatestRelease":null,"sinceLatestRelease":null,"pullRequests":null},"sha1":"Ip/RVzN3uevTRt1MX/dBH/iW8A0=","stats":{"installations":[{"timestamp":1325376000000,"total":163},{"timestamp":1328054400000,"total":183},{"timestamp":1330560000000,"total":237},{"timestamp":1333238400000,"total":290},{"timestamp":1335830400000,"total":352},{"timestamp":1338508800000,"total":385},{"timestamp":1341100800000,"total":454},{"timestamp":1343779200000,"total":494},{"timestamp":1346457600000,"total":518},{"timestamp":1349049600000,"total":552},{"timestamp":1351728000000,"total":587},{"timestamp":1354320000000,"total":593},{"timestamp":1356998400000,"total":653},{"timestamp":1359676800000,"total":697},{"timestamp":1362096000000,"total":767},{"timestamp":1364774400000,"total":812},{"timestamp":1367366400000,"total":855},{"timestamp":1370044800000,"total":863},{"timestamp":1372636800000,"total":950},{"timestamp":1375315200000,"total":905},{"timestamp":1377993600000,"total":900},{"timestamp":1380585600000,"total":960},{"timestamp":1383264000000,"total":916},{"timestamp":1385856000000,"total":875},{"timestamp":1388534400000,"total":950},{"timestamp":1391212800000,"total":970},{"timestamp":1393632000000,"total":983},{"timestamp":1396310400000,"total":1011},{"timestamp":1398902400000,"total":998},{"timestamp":1401580800000,"total":1023},{"timestamp":1404172800000,"total":1048},{"timestamp":1406851200000,"total":1095},{"timestamp":1409529600000,"total":1132},{"timestamp":1412121600000,"total":1128},{"timestamp":1414800000000,"total":1103},{"timestamp":1417392000000,"total":1131},{"timestamp":1420070400000,"total":1190},{"timestamp":1422748800000,"total":1179},{"timestamp":1425168000000,"total":1260},{"timestamp":1427846400000,"total":1240},{"timestamp":1430438400000,"total":1240},{"timestamp":1433116800000,"total":1290},{"timestamp":1435708800000,"total":1295},{"timestamp":1438387200000,"total":1292},{"timestamp":1441065600000,"total":1305},{"timestamp":1443657600000,"total":1331},{"timestamp":1446336000000,"total":1353},{"timestamp":1448928000000,"total":1339},{"timestamp":1451606400000,"total":1374},{"timestamp":1454284800000,"total":1173},{"timestamp":1456790400000,"total":1160},{"timestamp":1459468800000,"total":1254},{"timestamp":1462060800000,"total":1307},{"timestamp":1464739200000,"total":1330},{"timestamp":1467331200000,"total":1362},{"timestamp":1470009600000,"total":1404},{"timestamp":1472688000000,"total":1398},{"timestamp":1475280000000,"total":1391},{"timestamp":1477958400000,"total":1447},{"timestamp":1480550400000,"total":1404},{"timestamp":1483228800000,"total":1475},{"timestamp":1485907200000,"total":1461},{"timestamp":1488326400000,"total":1542},{"timestamp":1491004800000,"total":1470},{"timestamp":1493596800000,"total":1503},{"timestamp":1496275200000,"total":1498},{"timestamp":1498867200000,"total":1476},{"timestamp":1501545600000,"total":1463},{"timestamp":1504224000000,"total":1468},{"timestamp":1506816000000,"total":1424},{"timestamp":1509494400000,"total":1484},{"timestamp":1512086400000,"total":1437},{"timestamp":1514764800000,"total":1498},{"timestamp":1517443200000,"total":1445},{"timestamp":1519862400000,"total":1505},{"timestamp":1522540800000,"total":1486},{"timestamp":1525132800000,"total":1532},{"timestamp":1527811200000,"total":1521},{"timestamp":1530403200000,"total":1550},{"timestamp":1533081600000,"total":1574},{"timestamp":1535760000000,"total":1545},{"timestamp":1538352000000,"total":1672},{"timestamp":1541030400000,"total":1674},{"timestamp":1543640400000,"total":1614},{"timestamp":1546318800000,"total":1644},{"timestamp":1548997200000,"total":1650},{"timestamp":1551416400000,"total":1681},{"timestamp":1554091200000,"total":1680},{"timestamp":1556683200000,"total":1685},{"timestamp":1559361600000,"total":1652},{"timestamp":1561953600000,"total":1691},{"timestamp":1564632000000,"total":1637},{"timestamp":1567310400000,"total":1661}],"installationsPercentage":[{"timestamp":1325376000000,"percentage":0.4646919633948171},{"timestamp":1328054400000,"percentage":0.48880816282921097},{"timestamp":1330560000000,"percentage":0.5915682799590645},{"timestamp":1333238400000,"percentage":0.7136177961513854},{"timestamp":1335830400000,"percentage":0.8206658584351394},{"timestamp":1338508800000,"percentage":0.8846710632137686},{"timestamp":1341100800000,"percentage":1.004224822491097},{"timestamp":1343779200000,"percentage":1.0508178936844568},{"timestamp":1346457600000,"percentage":1.0754920687650527},{"timestamp":1349049600000,"percentage":1.055247562607532},{"timestamp":1351728000000,"percentage":1.0993950517858146},{"timestamp":1354320000000,"percentage":1.1523289481354813},{"timestamp":1356998400000,"percentage":1.1477484444756916},{"timestamp":1359676800000,"percentage":1.215683538563505},{"timestamp":1362096000000,"percentage":1.2422460845763892},{"timestamp":1364774400000,"percentage":1.2653099386044193},{"timestamp":1367366400000,"percentage":1.3094417643004825},{"timestamp":1370044800000,"percentage":1.3131267022717243},{"timestamp":1372636800000,"percentage":1.376691881865345},{"timestamp":1375315200000,"percentage":1.3244742349514846},{"timestamp":1377993600000,"percentage":1.2868356710848026},{"timestamp":1380585600000,"percentage":1.2939226072540537},{"timestamp":1383264000000,"percentage":1.2392613136711088},{"timestamp":1385856000000,"percentage":1.213575401173354},{"timestamp":1388534400000,"percentage":1.2328376028446106},{"timestamp":1391212800000,"percentage":1.23379844566835},{"timestamp":1393632000000,"percentage":1.187915407854985},{"timestamp":1396310400000,"percentage":1.209880089035686},{"timestamp":1398902400000,"percentage":1.17198050613587},{"timestamp":1401580800000,"percentage":1.1875188633250529},{"timestamp":1404172800000,"percentage":1.1459688795092453},{"timestamp":1406851200000,"percentage":1.1987519842355903},{"timestamp":1409529600000,"percentage":1.180064007005327},{"timestamp":1412121600000,"percentage":1.1394054485398843},{"timestamp":1414800000000,"percentage":1.1168263097142626},{"timestamp":1417392000000,"percentage":1.146279911216516},{"timestamp":1420070400000,"percentage":1.1554295479260526},{"timestamp":1422748800000,"percentage":1.1349961974258016},{"timestamp":1425168000000,"percentage":1.1201692700230257},{"timestamp":1427846400000,"percentage":1.1096594061532403},{"timestamp":1430438400000,"percentage":1.110156138089099},{"timestamp":1433116800000,"percentage":1.09725600939047},{"timestamp":1435708800000,"percentage":1.0705841503943387},{"timestamp":1438387200000,"percentage":1.077438831162333},{"timestamp":1441065600000,"percentage":1.0563807827741125},{"timestamp":1443657600000,"percentage":1.0558799263819254},{"timestamp":1446336000000,"percentage":1.0652788385075074},{"timestamp":1448928000000,"percentage":1.0550368356774218},{"timestamp":1451606400000,"percentage":1.0831690973590855},{"timestamp":1454284800000,"percentage":1.0882573965320492},{"timestamp":1456790400000,"percentage":1.0590705742718889},{"timestamp":1459468800000,"percentage":1.0162815764520914},{"timestamp":1462060800000,"percentage":1.0032238256063863},{"timestamp":1464739200000,"percentage":0.9887814198306433},{"timestamp":1467331200000,"percentage":1.0460106444255006},{"timestamp":1470009600000,"percentage":1.0619469026548674},{"timestamp":1472688000000,"percentage":1.047151792067713},{"timestamp":1475280000000,"percentage":1.047842168302586},{"timestamp":1477958400000,"percentage":1.0599257246244111},{"timestamp":1480550400000,"percentage":1.0498295148651073},{"timestamp":1483228800000,"percentage":1.0462254314349957},{"timestamp":1485907200000,"percentage":1.0471466865440575},{"timestamp":1488326400000,"percentage":1.0272466857637732},{"timestamp":1491004800000,"percentage":1.0199055026330213},{"timestamp":1493596800000,"percentage":0.9963209704683306},{"timestamp":1496275200000,"percentage":0.9930789424836255},{"timestamp":1498867200000,"percentage":0.9816179404644729},{"timestamp":1501545600000,"percentage":0.9476861688345338},{"timestamp":1504224000000,"percentage":0.9478305785123967},{"timestamp":1506816000000,"percentage":0.915573101182401},{"timestamp":1509494400000,"percentage":0.9207497533706018},{"timestamp":1512086400000,"percentage":0.9412951487600058},{"timestamp":1514764800000,"percentage":0.9392261729354894},{"timestamp":1517443200000,"percentage":0.9419142043269387},{"timestamp":1519862400000,"percentage":0.9159459804882205},{"timestamp":1522540800000,"percentage":0.9083129584352079},{"timestamp":1525132800000,"percentage":0.9233309828171238},{"timestamp":1527811200000,"percentage":0.8987980570361528},{"timestamp":1530403200000,"percentage":0.8668903803131991},{"timestamp":1533081600000,"percentage":0.8590015062542296},{"timestamp":1535760000000,"percentage":0.8020849015953443},{"timestamp":1538352000000,"percentage":0.7761221742561388},{"timestamp":1541030400000,"percentage":0.7494895948995308},{"timestamp":1543640400000,"percentage":0.7363205868666685},{"timestamp":1546318800000,"percentage":0.7062553430966118},{"timestamp":1548997200000,"percentage":0.7210750573582432},{"timestamp":1551416400000,"percentage":0.6671614483078865},{"timestamp":1554091200000,"percentage":0.6638138476310144},{"timestamp":1556683200000,"percentage":0.6538534669755494},{"timestamp":1559361600000,"percentage":0.6563316938284162},{"timestamp":1561953600000,"percentage":0.6358194588578562},{"timestamp":1564632000000,"percentage":0.6240065259570705},{"timestamp":1567310400000,"percentage":0.6248166176393142}],"installationsPerVersion":[{"version":"1.2","total":30},{"version":"1.3","total":35},{"version":"1.5","total":1595},{"version":"1.6-SNAPSHOT (private)","total":1}],"installationsPercentagePerVersion":[{"version":"1.2","percentage":0.011285068349897306},{"version":"1.3","percentage":0.01316591307488019},{"version":"1.5","percentage":0.59998946726954},{"version":"1.6-SNAPSHOT (private)","percentage":3.761689449965769E-4}],"currentInstalls":1661,"trend":24},"title":"HTML5 Notifier","url":"http://updates.jenkins-ci.org/download/plugins/html5-notifier-plugin/1.5/html5-notifier-plugin.hpi","version":"1.5","securityWarnings":null,"wiki":{"content":"

HTML5 Notifier Plugin

\n


\n

Provides W3C Web Notifications support for builds.

\n

Browser compatibility

\n

This plugin is compatible with Google Chrome, Safari, and Firefox

\n

Other browsers do not support HTML5 notifications right now. You can check this website for details : http://caniuse.com/#feat=notifications

\n

Screenshots

\n
\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n

Safari Notification

System Configuration

Job Configuration

\n
\n


\n
\n


\n

Changelog

\n

1.5 (released 2015-03-06)

\n
    \n
  • JENKINS-27222 - Fixes global configuration not saving/loading.
  • \n
\n

1.3 (released 2014-08-01)

\n
    \n
  • JENKINS-16236 - upgrade to use current html5 notification spec. Removes html in the notifications but now works again.
  • \n
\n

1.2 (released 2012-04-06)

\n
    \n
  • updated minimum required core to v1.455 to pick up prototype.js:1.7
  • \n
  • JENKINS-13138 - Verify action doesn't work with CSRF option
  • \n
  • JENKINS-13322 - Configure tab freezes when using both the HTML5 notifier plugin and the Android Emulator Plugin
  • \n
  • JENKINS-12538 - java.lang.String cannot be cast to net.sf.json.JSONArray
  • \n
  • JENKINS-13038 - HTML5 notifier plugin breaks Jenkins with CSRF protection
  • \n
\n

1.1 (released 2012-02-02)

\n
    \n
  • JENKINS-11618 - prototype.js
  • \n
  • added per-job job property to skip notifications
  • \n
  • added ability to only notify on different build result than previous
  • \n
  • cleaned up the notification area html
  • \n
\n

1.0 (released 2011-10-19)

\n
    \n
  • initial implementation
  • \n
  • fully I18N compliant
  • \n
","url":"https://wiki.jenkins-ci.org/display/JENKINS/HTML5+Notifier+Plugin"}} -------------------------------------------------------------------------------- /__testData/github.json: -------------------------------------------------------------------------------- 1 | {"firstRelease":"2011-02-14T23:44:41.00Z","buildDate":"2019-10-07","categories":[],"dependencies":[{"name":"credentials","title":"Credentials","optional":false,"version":"2.1.13","implied":false},{"name":"display-url-api","title":"Display URL API","optional":false,"version":"2.0","implied":false},{"name":"git","title":"Git","optional":false,"version":"3.4.0","implied":false},{"name":"github-api","title":"GitHub API","optional":false,"version":"1.90","implied":false},{"name":"plain-credentials","title":"Plain Credentials","optional":false,"version":"1.1","implied":false},{"name":"scm-api","title":"SCM API","optional":false,"version":"2.2.0","implied":false},{"name":"structs","title":"Structs","optional":false,"version":"1.17","implied":false},{"name":"token-macro","title":"Token Macro","optional":false,"version":"1.12.1","implied":false},{"name":"command-launcher","title":"Command Agent Launcher","optional":false,"version":"1.0","implied":true},{"name":"jdk-tool","title":"Oracle Java SE Development Kit Installer","optional":false,"version":"1.0","implied":true},{"name":"jaxb","title":"JAXB","optional":false,"version":"2.3.0","implied":true},{"name":"trilead-api","title":"Trilead API","optional":false,"version":"1.0.4","implied":true}],"maintainers":[{"id":"lanwen","name":"Merkushev Kirill","email":null},{"id":"KostyaSha","name":"Kanstantsin Shautsou","email":null}],"excerpt":"This plugin integrates GitHub to Jenkins.","gav":"com.coravy.hudson.plugins.github:github:1.29.5","labels":["external"],"name":"github","previousTimestamp":"2019-02-17T16:01:28.00Z","previousVersion":"1.29.4","releaseTimestamp":"2019-10-07T20:59:25.00Z","requiredCore":"2.60.3","scm":{"issues":null,"link":"https://github.com/jenkinsci/github-plugin","inLatestRelease":null,"sinceLatestRelease":null,"pullRequests":null},"sha1":"XY+jexPOjHaxoI4CO9cDXhU6Uas=","stats":{"installations":[{"timestamp":1241136000000,"total":1},{"timestamp":1243814400000,"total":36},{"timestamp":1246406400000,"total":67},{"timestamp":1249084800000,"total":99},{"timestamp":1251763200000,"total":107},{"timestamp":1254355200000,"total":135},{"timestamp":1257033600000,"total":167},{"timestamp":1259625600000,"total":170},{"timestamp":1262304000000,"total":201},{"timestamp":1264982400000,"total":242},{"timestamp":1267401600000,"total":309},{"timestamp":1270080000000,"total":351},{"timestamp":1272672000000,"total":361},{"timestamp":1275350400000,"total":391},{"timestamp":1277942400000,"total":471},{"timestamp":1280620800000,"total":269},{"timestamp":1283299200000,"total":428},{"timestamp":1285891200000,"total":558},{"timestamp":1288569600000,"total":676},{"timestamp":1291161600000,"total":708},{"timestamp":1293840000000,"total":868},{"timestamp":1296518400000,"total":984},{"timestamp":1298937600000,"total":1103},{"timestamp":1301616000000,"total":1204},{"timestamp":1304208000000,"total":1280},{"timestamp":1306886400000,"total":1481},{"timestamp":1309478400000,"total":1623},{"timestamp":1312156800000,"total":1807},{"timestamp":1314835200000,"total":2041},{"timestamp":1317427200000,"total":2261},{"timestamp":1320105600000,"total":2471},{"timestamp":1322697600000,"total":2599},{"timestamp":1325376000000,"total":2964},{"timestamp":1328054400000,"total":3266},{"timestamp":1330560000000,"total":3598},{"timestamp":1333238400000,"total":3704},{"timestamp":1335830400000,"total":4014},{"timestamp":1338508800000,"total":4027},{"timestamp":1341100800000,"total":4489},{"timestamp":1343779200000,"total":4731},{"timestamp":1346457600000,"total":4946},{"timestamp":1349049600000,"total":5588},{"timestamp":1351728000000,"total":5636},{"timestamp":1354320000000,"total":5618},{"timestamp":1356998400000,"total":6386},{"timestamp":1359676800000,"total":6392},{"timestamp":1362096000000,"total":7004},{"timestamp":1364774400000,"total":7397},{"timestamp":1367366400000,"total":7718},{"timestamp":1370044800000,"total":7860},{"timestamp":1372636800000,"total":8341},{"timestamp":1375315200000,"total":8706},{"timestamp":1377993600000,"total":8852},{"timestamp":1380585600000,"total":9448},{"timestamp":1383264000000,"total":9415},{"timestamp":1385856000000,"total":9402},{"timestamp":1388534400000,"total":10096},{"timestamp":1391212800000,"total":10583},{"timestamp":1393632000000,"total":11107},{"timestamp":1396310400000,"total":11376},{"timestamp":1398902400000,"total":11763},{"timestamp":1401580800000,"total":12065},{"timestamp":1404172800000,"total":12710},{"timestamp":1406851200000,"total":12961},{"timestamp":1409529600000,"total":13601},{"timestamp":1412121600000,"total":14151},{"timestamp":1414800000000,"total":14394},{"timestamp":1417392000000,"total":14389},{"timestamp":1420070400000,"total":15101},{"timestamp":1422748800000,"total":15511},{"timestamp":1425168000000,"total":16876},{"timestamp":1427846400000,"total":17011},{"timestamp":1430438400000,"total":17174},{"timestamp":1433116800000,"total":17713},{"timestamp":1435708800000,"total":18672},{"timestamp":1438387200000,"total":18624},{"timestamp":1441065600000,"total":19249},{"timestamp":1443657600000,"total":19815},{"timestamp":1446336000000,"total":19973},{"timestamp":1448928000000,"total":20194},{"timestamp":1451606400000,"total":20228},{"timestamp":1454284800000,"total":16992},{"timestamp":1456790400000,"total":17353},{"timestamp":1459468800000,"total":25008},{"timestamp":1462060800000,"total":36196},{"timestamp":1464739200000,"total":39234},{"timestamp":1467331200000,"total":42499},{"timestamp":1470009600000,"total":48516},{"timestamp":1472688000000,"total":52376},{"timestamp":1475280000000,"total":55332},{"timestamp":1477958400000,"total":59769},{"timestamp":1480550400000,"total":61562},{"timestamp":1483228800000,"total":69711},{"timestamp":1485907200000,"total":70597},{"timestamp":1488326400000,"total":78789},{"timestamp":1491004800000,"total":78840},{"timestamp":1493596800000,"total":85591},{"timestamp":1496275200000,"total":87191},{"timestamp":1498867200000,"total":89103},{"timestamp":1501545600000,"total":93224},{"timestamp":1504224000000,"total":94456},{"timestamp":1506816000000,"total":96175},{"timestamp":1509494400000,"total":101574},{"timestamp":1512086400000,"total":96827},{"timestamp":1514764800000,"total":101926},{"timestamp":1517443200000,"total":98955},{"timestamp":1519862400000,"total":108394},{"timestamp":1522540800000,"total":109385},{"timestamp":1525132800000,"total":112120},{"timestamp":1527811200000,"total":116357},{"timestamp":1530403200000,"total":125525},{"timestamp":1533081600000,"total":130518},{"timestamp":1535760000000,"total":140360},{"timestamp":1538352000000,"total":159660},{"timestamp":1541030400000,"total":167745},{"timestamp":1543640400000,"total":166140},{"timestamp":1546318800000,"total":177779},{"timestamp":1548997200000,"total":176001},{"timestamp":1551416400000,"total":196077},{"timestamp":1554091200000,"total":197211},{"timestamp":1556683200000,"total":202302},{"timestamp":1559361600000,"total":197778},{"timestamp":1561953600000,"total":210256},{"timestamp":1564632000000,"total":208339},{"timestamp":1567310400000,"total":211392},{"timestamp":1569902400000,"total":213030}],"installationsPercentage":[{"timestamp":1241136000000,"percentage":0.010495382031905962},{"timestamp":1243814400000,"percentage":0.3320726870214925},{"timestamp":1246406400000,"percentage":0.5724538619275461},{"timestamp":1249084800000,"percentage":0.8327024981074943},{"timestamp":1251763200000,"percentage":0.8396107972379159},{"timestamp":1254355200000,"percentage":1.0252904989747096},{"timestamp":1257033600000,"percentage":1.1102985173858122},{"timestamp":1259625600000,"percentage":1.0957138253303256},{"timestamp":1262304000000,"percentage":1.2184033460629204},{"timestamp":1264982400000,"percentage":1.3659968390155792},{"timestamp":1267401600000,"percentage":1.5658254788689572},{"timestamp":1270080000000,"percentage":1.723037651563497},{"timestamp":1272672000000,"percentage":1.7237263047318914},{"timestamp":1275350400000,"percentage":1.7901291090559472},{"timestamp":1277942400000,"percentage":2.0639789658194565},{"timestamp":1280620800000,"percentage":3.113065617405393},{"timestamp":1283299200000,"percentage":3.3508181319971815},{"timestamp":1285891200000,"percentage":3.485539384096446},{"timestamp":1288569600000,"percentage":3.671518574842494},{"timestamp":1291161600000,"percentage":3.5599356395816573},{"timestamp":1293840000000,"percentage":3.876903836705525},{"timestamp":1296518400000,"percentage":4.410775920032274},{"timestamp":1298937600000,"percentage":4.778201351585514},{"timestamp":1301616000000,"percentage":5.313561940067964},{"timestamp":1304208000000,"percentage":5.341568251053708},{"timestamp":1306886400000,"percentage":5.921394586381992},{"timestamp":1309478400000,"percentage":6.439709558385906},{"timestamp":1312156800000,"percentage":6.6632250451712824},{"timestamp":1314835200000,"percentage":7.094688542825361},{"timestamp":1317427200000,"percentage":7.4512259425257055},{"timestamp":1320105600000,"percentage":7.683457711442786},{"timestamp":1322697600000,"percentage":8.19666961019301},{"timestamp":1325376000000,"percentage":8.449981469338883},{"timestamp":1328054400000,"percentage":8.72375661093007},{"timestamp":1330560000000,"percentage":8.980855153133815},{"timestamp":1333238400000,"percentage":9.11462178256804},{"timestamp":1335830400000,"percentage":9.358388510677981},{"timestamp":1338508800000,"percentage":9.253429536524276},{"timestamp":1341100800000,"percentage":9.929438828551836},{"timestamp":1343779200000,"percentage":10.063602135670376},{"timestamp":1346457600000,"percentage":10.269080641142763},{"timestamp":1349049600000,"percentage":10.68246989103422},{"timestamp":1351728000000,"percentage":10.555690820894124},{"timestamp":1354320000000,"percentage":10.917005110666329},{"timestamp":1356998400000,"percentage":11.22438218441312},{"timestamp":1359676800000,"percentage":11.148707573167753},{"timestamp":1362096000000,"percentage":11.343796057852712},{"timestamp":1364774400000,"percentage":11.526474896375479},{"timestamp":1367366400000,"percentage":11.820200627919442},{"timestamp":1370044800000,"percentage":11.959647601223354},{"timestamp":1372636800000,"percentage":12.087354722777729},{"timestamp":1375315200000,"percentage":12.74129578948909},{"timestamp":1377993600000,"percentage":12.656743733825191},{"timestamp":1380585600000,"percentage":12.734354993058645},{"timestamp":1383264000000,"percentage":12.737604004599879},{"timestamp":1385856000000,"percentage":13.040041053522142},{"timestamp":1388534400000,"percentage":13.10181940875704},{"timestamp":1391212800000,"percentage":13.461122629389843},{"timestamp":1393632000000,"percentage":13.422356495468279},{"timestamp":1396310400000,"percentage":13.613843613125583},{"timestamp":1398902400000,"percentage":13.813633961599436},{"timestamp":1401580800000,"percentage":14.005293339214822},{"timestamp":1404172800000,"percentage":13.898153109315372},{"timestamp":1406851200000,"percentage":14.189063440801357},{"timestamp":1409529600000,"percentage":14.178489893356407},{"timestamp":1412121600000,"percentage":14.294083778624026},{"timestamp":1414800000000,"percentage":14.574431461493287},{"timestamp":1417392000000,"percentage":14.583396677713926},{"timestamp":1420070400000,"percentage":14.662303868261613},{"timestamp":1422748800000,"percentage":14.932083136786776},{"timestamp":1425168000000,"percentage":15.003156032467128},{"timestamp":1427846400000,"percentage":15.2229162565103},{"timestamp":1430438400000,"percentage":15.37566251253402},{"timestamp":1433116800000,"percentage":15.066430770801082},{"timestamp":1435708800000,"percentage":15.436252707461847},{"timestamp":1438387200000,"percentage":15.531130643627934},{"timestamp":1441065600000,"percentage":15.581818917715626},{"timestamp":1443657600000,"percentage":15.71920416322904},{"timestamp":1446336000000,"percentage":15.725657236888724},{"timestamp":1448928000000,"percentage":15.911436788401685},{"timestamp":1451606400000,"percentage":15.946393378005519},{"timestamp":1454284800000,"percentage":15.764424281221297},{"timestamp":1456790400000,"percentage":15.843147995982836},{"timestamp":1459468800000,"percentage":20.267280433743142},{"timestamp":1462060800000,"percentage":27.783236106846793},{"timestamp":1464739200000,"percentage":29.16830844032741},{"timestamp":1467331200000,"percentage":32.63906488798777},{"timestamp":1470009600000,"percentage":36.69616519174041},{"timestamp":1472688000000,"percentage":39.2314894573237},{"timestamp":1475280000000,"percentage":41.68166991841746},{"timestamp":1477958400000,"percentage":43.780719167295395},{"timestamp":1480550400000,"percentage":46.032481904647966},{"timestamp":1483228800000,"percentage":49.44638715306101},{"timestamp":1485907200000,"percentage":50.59918865841946},{"timestamp":1488326400000,"percentage":52.48750915994937},{"timestamp":1491004800000,"percentage":54.700237977950614},{"timestamp":1493596800000,"percentage":56.73726426038249},{"timestamp":1496275200000,"percentage":57.802100182970484},{"timestamp":1498867200000,"percentage":59.25820010108803},{"timestamp":1501545600000,"percentage":60.387625019433074},{"timestamp":1504224000000,"percentage":60.986570247933884},{"timestamp":1506816000000,"percentage":61.836547054927955},{"timestamp":1509494400000,"percentage":63.021722000583225},{"timestamp":1512086400000,"percentage":63.42573790465211},{"timestamp":1514764800000,"percentage":63.90625293900046},{"timestamp":1517443200000,"percentage":64.50319729354479},{"timestamp":1519862400000,"percentage":65.96880306248516},{"timestamp":1522540800000,"percentage":66.86124694376528},{"timestamp":1525132800000,"percentage":67.57432754142032},{"timestamp":1527811200000,"percentage":68.75834682613782},{"timestamp":1530403200000,"percentage":70.20413870246085},{"timestamp":1533081600000,"percentage":71.22945272762995},{"timestamp":1535760000000,"percentage":72.86772607632525},{"timestamp":1538352000000,"percentage":74.11224063500906},{"timestamp":1541030400000,"percentage":75.10342419141087},{"timestamp":1543640400000,"percentage":75.79448717597788},{"timestamp":1546318800000,"percentage":76.37309528003196},{"timestamp":1548997200000,"percentage":76.91510980006555},{"timestamp":1551416400000,"percentage":77.81975925036612},{"timestamp":1554091200000,"percentage":77.9234480387857},{"timestamp":1556683200000,"percentage":78.50199648432498},{"timestamp":1559361600000,"percentage":78.57625287045792},{"timestamp":1561953600000,"percentage":79.05668606837222},{"timestamp":1564632000000,"percentage":79.41655199228474},{"timestamp":1567310400000,"percentage":79.51910562071637},{"timestamp":1569902400000,"percentage":79.41383693751794}],"installationsPerVersion":[{"version":"0.4","total":1},{"version":"0.5","total":1},{"version":"0.7","total":8},{"version":"0.8","total":1},{"version":"1.0","total":4},{"version":"1.1","total":1},{"version":"1.10","total":100},{"version":"1.11","total":29},{"version":"1.11-SNAPSHOT (private)","total":1},{"version":"1.11.1","total":5},{"version":"1.11.2","total":13},{"version":"1.11.3","total":156},{"version":"1.12.0","total":27},{"version":"1.12.1","total":28},{"version":"1.13.0","total":11},{"version":"1.13.1","total":1},{"version":"1.13.2","total":2},{"version":"1.13.3","total":83},{"version":"1.14.0","total":179},{"version":"1.14.0-SNAPSHOT (private)","total":1},{"version":"1.14.1","total":11},{"version":"1.14.2","total":21},{"version":"1.15.0","total":12},{"version":"1.16.0","total":21},{"version":"1.17.0","total":43},{"version":"1.17.1","total":57},{"version":"1.18.0","total":2},{"version":"1.18.1","total":39},{"version":"1.18.2","total":139},{"version":"1.19.0","total":150},{"version":"1.19.1","total":485},{"version":"1.19.2","total":268},{"version":"1.19.3","total":55},{"version":"1.2","total":5},{"version":"1.20.0","total":317},{"version":"1.21.0","total":68},{"version":"1.21.1","total":602},{"version":"1.22.0","total":41},{"version":"1.22.1","total":156},{"version":"1.22.2","total":100},{"version":"1.22.3","total":233},{"version":"1.22.4","total":172},{"version":"1.23.0","total":52},{"version":"1.23.1","total":175},{"version":"1.24.0","total":273},{"version":"1.25.0","total":555},{"version":"1.25.1","total":596},{"version":"1.26.0","total":478},{"version":"1.26.1","total":908},{"version":"1.26.2","total":438},{"version":"1.27.0","total":3604},{"version":"1.28.0","total":3073},{"version":"1.28.1","total":3634},{"version":"1.29.0","total":9010},{"version":"1.29.1","total":1668},{"version":"1.29.2","total":12202},{"version":"1.29.3","total":19938},{"version":"1.29.3.webhookfix2","total":1},{"version":"1.29.4","total":104968},{"version":"1.29.5","total":47552},{"version":"1.29.5-SNAPSHOT (private)","total":2},{"version":"1.3","total":2},{"version":"1.30.0-SNAPSHOT (private)","total":2},{"version":"1.4","total":21},{"version":"1.5","total":16},{"version":"1.6","total":18},{"version":"1.6-SNAPSHOT (private)","total":1},{"version":"1.7","total":17},{"version":"1.8","total":85},{"version":"1.9","total":9},{"version":"1.9.1","total":82},{"version":"2.0.1","total":1}],"installationsPercentagePerVersion":[{"version":"0.4","percentage":3.727824106347366E-4},{"version":"0.5","percentage":3.727824106347366E-4},{"version":"0.7","percentage":0.002982259285077893},{"version":"0.8","percentage":3.727824106347366E-4},{"version":"1.0","percentage":0.0014911296425389464},{"version":"1.1","percentage":3.727824106347366E-4},{"version":"1.10","percentage":0.03727824106347366},{"version":"1.11","percentage":0.010810689908407362},{"version":"1.11-SNAPSHOT (private)","percentage":3.727824106347366E-4},{"version":"1.11.1","percentage":0.001863912053173683},{"version":"1.11.2","percentage":0.004846171338251576},{"version":"1.11.3","percentage":0.05815405605901891},{"version":"1.12.0","percentage":0.010065125087137888},{"version":"1.12.1","percentage":0.010437907497772625},{"version":"1.13.0","percentage":0.004100606516982103},{"version":"1.13.1","percentage":3.727824106347366E-4},{"version":"1.13.2","percentage":7.455648212694732E-4},{"version":"1.13.3","percentage":0.030940940082683138},{"version":"1.14.0","percentage":0.06672805150361785},{"version":"1.14.0-SNAPSHOT (private)","percentage":3.727824106347366E-4},{"version":"1.14.1","percentage":0.004100606516982103},{"version":"1.14.2","percentage":0.007828430623329469},{"version":"1.15.0","percentage":0.004473388927616839},{"version":"1.16.0","percentage":0.007828430623329469},{"version":"1.17.0","percentage":0.016029643657293674},{"version":"1.17.1","percentage":0.021248597406179986},{"version":"1.18.0","percentage":7.455648212694732E-4},{"version":"1.18.1","percentage":0.014538514014754728},{"version":"1.18.2","percentage":0.05181675507822839},{"version":"1.19.0","percentage":0.05591736159521049},{"version":"1.19.1","percentage":0.18079946915784725},{"version":"1.19.2","percentage":0.09990568605010941},{"version":"1.19.3","percentage":0.020503032584910513},{"version":"1.2","percentage":0.001863912053173683},{"version":"1.20.0","percentage":0.1181720241712115},{"version":"1.21.0","percentage":0.02534920392316209},{"version":"1.21.1","percentage":0.22441501120211144},{"version":"1.22.0","percentage":0.0152840788360242},{"version":"1.22.1","percentage":0.05815405605901891},{"version":"1.22.2","percentage":0.03727824106347366},{"version":"1.22.3","percentage":0.08685830167789363},{"version":"1.22.4","percentage":0.0641185746291747},{"version":"1.23.0","percentage":0.019384685353006303},{"version":"1.23.1","percentage":0.0652369218610789},{"version":"1.24.0","percentage":0.1017695981032831},{"version":"1.25.0","percentage":0.20689423790227882},{"version":"1.25.1","percentage":0.22217831673830302},{"version":"1.26.0","percentage":0.1781899922834041},{"version":"1.26.1","percentage":0.33848642885634084},{"version":"1.26.2","percentage":0.16327869585801463},{"version":"1.27.0","percentage":1.3435078079275908},{"version":"1.28.0","percentage":1.1455603478805456},{"version":"1.28.1","percentage":1.3546912802466329},{"version":"1.29.0","percentage":3.358769519818977},{"version":"1.29.1","percentage":0.6218010609387407},{"version":"1.29.2","percentage":4.548690974565056},{"version":"1.29.3","percentage":7.432535703235379},{"version":"1.29.3.webhookfix2","percentage":3.727824106347366E-4},{"version":"1.29.4","percentage":39.13022407950703},{"version":"1.29.5","percentage":17.726549190502997},{"version":"1.29.5-SNAPSHOT (private)","percentage":7.455648212694732E-4},{"version":"1.3","percentage":7.455648212694732E-4},{"version":"1.30.0-SNAPSHOT (private)","percentage":7.455648212694732E-4},{"version":"1.4","percentage":0.007828430623329469},{"version":"1.5","percentage":0.005964518570155786},{"version":"1.6","percentage":0.006710083391425259},{"version":"1.6-SNAPSHOT (private)","percentage":3.727824106347366E-4},{"version":"1.7","percentage":0.006337300980790522},{"version":"1.8","percentage":0.03168650490395261},{"version":"1.9","percentage":0.0033550416957126294},{"version":"1.9.1","percentage":0.0305681576720484},{"version":"2.0.1","percentage":3.727824106347366E-4}],"currentInstalls":213030,"trend":1638},"title":"GitHub","url":"http://updates.jenkins-ci.org/download/plugins/github/1.29.5/github.hpi","version":"1.29.5","securityWarnings":[{"versions":[{"firstVersion":null,"lastVersion":"1.29.0"}],"id":"SECURITY-799","message":"Server-side request forgery","url":"https://jenkins.io/security/advisory/2018-06-04/#SECURITY-799","active":false},{"versions":[{"firstVersion":null,"lastVersion":"1.29.0"}],"id":"SECURITY-804","message":"CSRF vulnerability and lack of permission checks allows capturing credentials","url":"https://jenkins.io/security/advisory/2018-06-04/#SECURITY-804","active":false},{"versions":[{"firstVersion":null,"lastVersion":"1.29.1"}],"id":"SECURITY-915","message":"CSRF vulnerability and insufficient permission checks allow capturing credentials","url":"https://jenkins.io/security/advisory/2018-06-25/#SECURITY-915","active":false}],"wiki":{"content":"

\n\n \n \n \n \n \n \n \n \n

Plugin Information

View GitHub on the plugin site for more information.

\n
\n \n
\n

Older versions of this plugin may not be safe to use. Please review the following warnings before using an older version:

\n \n
\n
\n

\n

Github Plugin

\n

This plugin integrates Jenkins with Github projects.The plugin currently has three major functionalities:

\n
    \n
  • Create hyperlinks between your Jenkins projects and GitHub
  • \n
  • Trigger a job when you push to the repository by groking HTTP POSTs from post-receive hook and optionally auto-managing the hook setup.
  • \n
  • Report build status result back to github as Commit Status (documented on SO)
  • \n
  • Base features for other plugins
  • \n
\n

\n
\n
\n

\n

Hyperlinks between changes

\n

The Github plugin decorates Jenkins \"Changes\" pages to create links to your Github commit and issue pages. It adds a sidebar link that links back to the Github project page.

\n

\n

When creating a job, specify that is connects to git. Under \"Github project\", put in: git@github.com:Person/Project.git Under \"Source Code Management\" select Git, and put in git@github.com:Person/Project.git

\n

GitHub hook trigger for GITScm polling

\n

This feature enables builds after post-receive hooks in your GitHub repositories. This trigger only kicks git-plugin internal polling algo for every incoming event against matched repo.

\n
\n

Old name

\n \n
\n

Previously named as \"Build when a change is pushed to GitHub\"

\n
\n
\n

To be able to use this feature:

\n

Manual Mode

\n

In this mode, you'll be responsible for registering the hook URLs to GitHub. Click the \"(question)\" icon (under Manage Jenkins > Configure System > GitHub) to see the URL in Jenkins that receives the post-commit POSTs — but in general the URL is of the form $JENKINS_BASE_URL/github-webhook/ — for example: https://ci.example.com/jenkins/github-webhook/.

\n

Once you have the URL, and have added it as a webhook to the relevant GitHub repositories, continue to Step 3.

\n

Automatic Mode (Jenkins manages hooks for jobs by itself)

\n

In this mode, Jenkins will automatically add/remove hook URLs to GitHub based on the project configuration in the background. You'll specify GitHub OAuth token so that Jenkins can login as you to do this.

\n

Step 1. Go to the global configuration and add GitHub Server Config.

\n

\n

Step 2.1. Create your personal access token in GitHub.

\n

Plugin can help you to do it with all required scopes. Go to Advanced -> Manage Additional GitHub Actions -> Convert Login and Password to token

\n

\n
\n

Two-Factor Authentication

\n \n
\n

Auto-creating token doesn't work with GitHub 2FA

\n
\n
\n

You can create \"Secret text\" credentials with token in corresponding domain with login and password directly, or from username and password credentials.

\n

Step 2.2. Select previously created \"Secret Text\" credentials with GitHub OAuth token.

\n
\n

Required scopes for token

\n \n
\n

To be able manage hooks your token should have admin:org_hook scope.

\n
\n
\n
\n

GitHub Enterprise

\n \n
\n

You can also redefine GitHub url by clicking on Custom GitHub API URL checkbox.
Note that credentials are filtered by entered GH url with help of domain requirements. So you can create credentials in different domains and see only credentials that matched by predefined domains.

\n
\n
\n

\n

Step 3. Once that configuration is done, go to the project config of each job you want triggered automatically and simply check \"Build when a change is pushed to GitHub\" under \"Build Triggers\". With this, every new push to the repository automatically triggers a new build.

\n

Note that there's only one URL and it receives all post-receive POSTs for all your repositories. The server side of this URL is smart enough to figure out which projects need to be triggered, based on the submission.

\n

Security Implications

\n

This plugin requires that you have an HTTP URL reachable from GitHub, which means it's reachable from the whole internet. So it is implemented carefully with the possible malicious fake post-receive POSTS in mind. To cope with this, upon receiving a POST, Jenkins will talk to GitHub to ensure the push was actually made.

\n

Jenkins inside a firewall

\n

In case your Jenkins run inside the firewall and not directly reachable from the internet, this plugin lets you specify an arbitrary endpoint URL as an override in the automatic mode. The plugin will assume that you've set up reverse proxy or some other means so that the POST from GitHub will be routed to the Jenkins.

\n

Trouble-shooting hooks

\n

If you set this up but build aren't triggered, check the following things:

\n
    \n
  • Click the \"admin\" button on the GitHub repository in question and make sure post-receive hooks are there.\n
      \n
    • If it's not there, make sure you have proper credential set in the Jenkins system config page.
    • \n
  • \n
  • Also, enable logging for the class names\n
      \n
    • com.cloudbees.jenkins.GitHubPushTrigger
    • \n
    • org.jenkinsci.plugins.github.webhook.WebhookManager
    • \n
    • com.cloudbees.jenkins.GitHubWebHook
      and you'll see the log of Jenkins trying to install a post-receive hook.
    • \n
  • \n
  • Click \"Test hook\" button from the GitHub UI and see if Jenkins receive a payload.
  • \n
\n

Using cache to GitHub requests

\n

Each GitHub Server Config creates own GitHub client to interact with api. By default it uses cache (with 20MB limit) to speedup process of fetching data and reduce rate-limit consuming. You can change cache limit value in \"Advanced\" section of this config item. If you set 0, then this feature will be disabled for this (and only this) config.

\n
\n \n
\n

Additional info:

\n
    \n
  • This plugin now serves only hooks from github as main feature. Then it starts using git-plugin to fetch sources.
  • \n
  • It works both public and Enterprise GitHub
  • \n
  • Plugin have some limitations
  • \n
\n
\n
\n

Possible Issues between Jenkins and GitHub

\n

Windows:

\n
    \n
  • In windows, Jenkins will use the the SSH key of the user it is running as, which is located in the %USERPROFILE%\\.ssh folder ( on XP, that would be C:\\Documents and Settings\\USERNAME\\.ssh, and on 7 it would be C:\\Users\\USERNAME\\.ssh). Therefore, you need to force Jenkins to run as the user that has the SSH key configured. To do that, right click on My Computer, and hit \"Manage\". Click on \"Services\". Go to Jenkins, right click, and select  \"Properties\". Under the \"Log On\" tab, choose the user Jenkins will run as, and put in the username and password (it requires one). Then restart the Jenkins service by right clicking on Jenkins (in the services window), and hit \"Restart\".
  • \n
  • Jenkins does not support passphrases for SSH keys. Therefore, if you set one while running the initial Github configuration, rerun it and don't set one.
  • \n
\n

Pipeline examples

\n

Setting commit status

\n

This code will set commit status for custom repo with configured context and message (you can also define same way backref)

\n
\n
\n
void setBuildStatus(String message, String state) {\n  step([\n      $class: \"GitHubCommitStatusSetter\",\n      reposSource: [$class: \"ManuallyEnteredRepositorySource\", url: \"https://github.com/my-org/my-repo\"],\n      contextSource: [$class: \"ManuallyEnteredCommitContextSource\", context: \"ci/jenkins/build-status\"],\n      errorHandlers: [[$class: \"ChangingBuildStatusErrorHandler\", result: \"UNSTABLE\"]],\n      statusResultSource: [ $class: \"ConditionalStatusResultSource\", results: [[$class: \"AnyBuildResult\", message: message, state: state]] ]\n  ]);\n}\n\nsetBuildStatus(\"Build complete\", \"SUCCESS\");\n
\n
\n
\n

More complex examle (can be used with multiply scm sources in pipeline)

\n
\n
\n
def getRepoURL() {\n  sh \"git config --get remote.origin.url > .git/remote-url\"\n  return readFile(\".git/remote-url\").trim()\n}\n\ndef getCommitSha() {\n  sh \"git rev-parse HEAD > .git/current-commit\"\n  return readFile(\".git/current-commit\").trim()\n}\n\ndef updateGithubCommitStatus(build) {\n  // workaround https://issues.jenkins-ci.org/browse/JENKINS-38674\n  repoUrl = getRepoURL()\n  commitSha = getCommitSha()\n\n  step([\n    $class: 'GitHubCommitStatusSetter',\n    reposSource: [$class: \"ManuallyEnteredRepositorySource\", url: repoUrl],\n    commitShaSource: [$class: \"ManuallyEnteredShaSource\", sha: commitSha],\n    errorHandlers: [[$class: 'ShallowAnyErrorHandler']],\n    statusResultSource: [\n      $class: 'ConditionalStatusResultSource',\n      results: [\n        [$class: 'BetterThanOrEqualBuildResult', result: 'SUCCESS', state: 'SUCCESS', message: build.description],\n        [$class: 'BetterThanOrEqualBuildResult', result: 'FAILURE', state: 'FAILURE', message: build.description],\n        [$class: 'AnyBuildResult', state: 'FAILURE', message: 'Loophole']\n      ]\n    ]\n  ])\n}\n
\n
\n
\n

Change Log

\n

GitHub Releases

\n

Open Issues

\n

\n
\n

\n
\n
\n
\n \n \n \n \n \n \n \n \n \n \n \n \n \n
TKeySummaryStatusUpdatedCreated
\n
\n
\n \n
\n Loading... \n
Refresh type,key,summary,status,updated,createdtruehttps://issues.jenkins-ci.org/sr/jira.issueviews:searchrequest-xml/temp/SearchRequest.xml?jqlQuery=component+%3D+github-plugin+AND+status+in+%28Open%2C+%22In+Progress%22%2C+Reopened%29+ORDER+BY+updated+DESC&amp;tempMax=1000columns=type,key,summary,status,updated,created|anonymous=true|url=https://issues.jenkins-ci.org/sr/jira.issueviews:searchrequest-xml/temp/SearchRequest.xml?jqlQuery=component+%3D+github-plugin+AND+status+in+%28Open%2C+%22In+Progress%22%2C+Reopened%29+ORDER+BY+updated+DESC&amp;tempMax=1000BLOCK\" data-pageid=\"37749162\">
\n
\n

\n
\n
\n
\n

","url":"https://wiki.jenkins-ci.org/display/JENKINS/Github+Plugin"}} -------------------------------------------------------------------------------- /public/vendor/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v4.3.1 (https://getbootstrap.com/) 3 | * Copyright 2011-2019 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jquery"),require("popper.js")):"function"==typeof define&&define.amd?define(["exports","jquery","popper.js"],e):e((t=t||self).bootstrap={},t.jQuery,t.Popper)}(this,function(t,g,u){"use strict";function i(t,e){for(var n=0;nthis._items.length-1||t<0))if(this._isSliding)g(this._element).one(Q.SLID,function(){return e.to(t)});else{if(n===t)return this.pause(),void this.cycle();var i=ndocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},t._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},t._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=t.left+t.right
',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent",sanitize:!0,sanitizeFn:null,whiteList:Ee},je="show",He="out",Re={HIDE:"hide"+De,HIDDEN:"hidden"+De,SHOW:"show"+De,SHOWN:"shown"+De,INSERTED:"inserted"+De,CLICK:"click"+De,FOCUSIN:"focusin"+De,FOCUSOUT:"focusout"+De,MOUSEENTER:"mouseenter"+De,MOUSELEAVE:"mouseleave"+De},xe="fade",Fe="show",Ue=".tooltip-inner",We=".arrow",qe="hover",Me="focus",Ke="click",Qe="manual",Be=function(){function i(t,e){if("undefined"==typeof u)throw new TypeError("Bootstrap's tooltips require Popper.js (https://popper.js.org/)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var t=i.prototype;return t.enable=function(){this._isEnabled=!0},t.disable=function(){this._isEnabled=!1},t.toggleEnabled=function(){this._isEnabled=!this._isEnabled},t.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=g(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(g(this.getTipElement()).hasClass(Fe))return void this._leave(null,this);this._enter(null,this)}},t.dispose=function(){clearTimeout(this._timeout),g.removeData(this.element,this.constructor.DATA_KEY),g(this.element).off(this.constructor.EVENT_KEY),g(this.element).closest(".modal").off("hide.bs.modal"),this.tip&&g(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,(this._activeTrigger=null)!==this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},t.show=function(){var e=this;if("none"===g(this.element).css("display"))throw new Error("Please use show on visible elements");var t=g.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){g(this.element).trigger(t);var n=_.findShadowRoot(this.element),i=g.contains(null!==n?n:this.element.ownerDocument.documentElement,this.element);if(t.isDefaultPrevented()||!i)return;var o=this.getTipElement(),r=_.getUID(this.constructor.NAME);o.setAttribute("id",r),this.element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&g(o).addClass(xe);var s="function"==typeof this.config.placement?this.config.placement.call(this,o,this.element):this.config.placement,a=this._getAttachment(s);this.addAttachmentClass(a);var l=this._getContainer();g(o).data(this.constructor.DATA_KEY,this),g.contains(this.element.ownerDocument.documentElement,this.tip)||g(o).appendTo(l),g(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new u(this.element,o,{placement:a,modifiers:{offset:this._getOffset(),flip:{behavior:this.config.fallbackPlacement},arrow:{element:We},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){return e._handlePopperPlacementChange(t)}}),g(o).addClass(Fe),"ontouchstart"in document.documentElement&&g(document.body).children().on("mouseover",null,g.noop);var c=function(){e.config.animation&&e._fixTransition();var t=e._hoverState;e._hoverState=null,g(e.element).trigger(e.constructor.Event.SHOWN),t===He&&e._leave(null,e)};if(g(this.tip).hasClass(xe)){var h=_.getTransitionDurationFromElement(this.tip);g(this.tip).one(_.TRANSITION_END,c).emulateTransitionEnd(h)}else c()}},t.hide=function(t){var e=this,n=this.getTipElement(),i=g.Event(this.constructor.Event.HIDE),o=function(){e._hoverState!==je&&n.parentNode&&n.parentNode.removeChild(n),e._cleanTipClass(),e.element.removeAttribute("aria-describedby"),g(e.element).trigger(e.constructor.Event.HIDDEN),null!==e._popper&&e._popper.destroy(),t&&t()};if(g(this.element).trigger(i),!i.isDefaultPrevented()){if(g(n).removeClass(Fe),"ontouchstart"in document.documentElement&&g(document.body).children().off("mouseover",null,g.noop),this._activeTrigger[Ke]=!1,this._activeTrigger[Me]=!1,this._activeTrigger[qe]=!1,g(this.tip).hasClass(xe)){var r=_.getTransitionDurationFromElement(n);g(n).one(_.TRANSITION_END,o).emulateTransitionEnd(r)}else o();this._hoverState=""}},t.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},t.isWithContent=function(){return Boolean(this.getTitle())},t.addAttachmentClass=function(t){g(this.getTipElement()).addClass(Ae+"-"+t)},t.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},t.setContent=function(){var t=this.getTipElement();this.setElementContent(g(t.querySelectorAll(Ue)),this.getTitle()),g(t).removeClass(xe+" "+Fe)},t.setElementContent=function(t,e){"object"!=typeof e||!e.nodeType&&!e.jquery?this.config.html?(this.config.sanitize&&(e=Se(e,this.config.whiteList,this.config.sanitizeFn)),t.html(e)):t.text(e):this.config.html?g(e).parent().is(t)||t.empty().append(e):t.text(g(e).text())},t.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},t._getOffset=function(){var e=this,t={};return"function"==typeof this.config.offset?t.fn=function(t){return t.offsets=l({},t.offsets,e.config.offset(t.offsets,e.element)||{}),t}:t.offset=this.config.offset,t},t._getContainer=function(){return!1===this.config.container?document.body:_.isElement(this.config.container)?g(this.config.container):g(document).find(this.config.container)},t._getAttachment=function(t){return Pe[t.toUpperCase()]},t._setListeners=function(){var i=this;this.config.trigger.split(" ").forEach(function(t){if("click"===t)g(i.element).on(i.constructor.Event.CLICK,i.config.selector,function(t){return i.toggle(t)});else if(t!==Qe){var e=t===qe?i.constructor.Event.MOUSEENTER:i.constructor.Event.FOCUSIN,n=t===qe?i.constructor.Event.MOUSELEAVE:i.constructor.Event.FOCUSOUT;g(i.element).on(e,i.config.selector,function(t){return i._enter(t)}).on(n,i.config.selector,function(t){return i._leave(t)})}}),g(this.element).closest(".modal").on("hide.bs.modal",function(){i.element&&i.hide()}),this.config.selector?this.config=l({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},t._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},t._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?Me:qe]=!0),g(e.getTipElement()).hasClass(Fe)||e._hoverState===je?e._hoverState=je:(clearTimeout(e._timeout),e._hoverState=je,e.config.delay&&e.config.delay.show?e._timeout=setTimeout(function(){e._hoverState===je&&e.show()},e.config.delay.show):e.show())},t._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?Me:qe]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=He,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout(function(){e._hoverState===He&&e.hide()},e.config.delay.hide):e.hide())},t._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},t._getConfig=function(t){var e=g(this.element).data();return Object.keys(e).forEach(function(t){-1!==Oe.indexOf(t)&&delete e[t]}),"number"==typeof(t=l({},this.constructor.Default,e,"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),_.typeCheckConfig(be,t,this.constructor.DefaultType),t.sanitize&&(t.template=Se(t.template,t.whiteList,t.sanitizeFn)),t},t._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},t._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(Ne);null!==e&&e.length&&t.removeClass(e.join(""))},t._handlePopperPlacementChange=function(t){var e=t.instance;this.tip=e.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},t._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(g(t).removeClass(xe),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},i._jQueryInterface=function(n){return this.each(function(){var t=g(this).data(Ie),e="object"==typeof n&&n;if((t||!/dispose|hide/.test(n))&&(t||(t=new i(this,e),g(this).data(Ie,t)),"string"==typeof n)){if("undefined"==typeof t[n])throw new TypeError('No method named "'+n+'"');t[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.3.1"}},{key:"Default",get:function(){return Le}},{key:"NAME",get:function(){return be}},{key:"DATA_KEY",get:function(){return Ie}},{key:"Event",get:function(){return Re}},{key:"EVENT_KEY",get:function(){return De}},{key:"DefaultType",get:function(){return ke}}]),i}();g.fn[be]=Be._jQueryInterface,g.fn[be].Constructor=Be,g.fn[be].noConflict=function(){return g.fn[be]=we,Be._jQueryInterface};var Ve="popover",Ye="bs.popover",ze="."+Ye,Xe=g.fn[Ve],$e="bs-popover",Ge=new RegExp("(^|\\s)"+$e+"\\S+","g"),Je=l({},Be.Default,{placement:"right",trigger:"click",content:"",template:''}),Ze=l({},Be.DefaultType,{content:"(string|element|function)"}),tn="fade",en="show",nn=".popover-header",on=".popover-body",rn={HIDE:"hide"+ze,HIDDEN:"hidden"+ze,SHOW:"show"+ze,SHOWN:"shown"+ze,INSERTED:"inserted"+ze,CLICK:"click"+ze,FOCUSIN:"focusin"+ze,FOCUSOUT:"focusout"+ze,MOUSEENTER:"mouseenter"+ze,MOUSELEAVE:"mouseleave"+ze},sn=function(t){var e,n;function i(){return t.apply(this,arguments)||this}n=t,(e=i).prototype=Object.create(n.prototype),(e.prototype.constructor=e).__proto__=n;var o=i.prototype;return o.isWithContent=function(){return this.getTitle()||this._getContent()},o.addAttachmentClass=function(t){g(this.getTipElement()).addClass($e+"-"+t)},o.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},o.setContent=function(){var t=g(this.getTipElement());this.setElementContent(t.find(nn),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(on),e),t.removeClass(tn+" "+en)},o._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},o._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(Ge);null!==e&&0=this._offsets[o]&&("undefined"==typeof this._offsets[o+1]||t