├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .gitlab-ci.yml ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── Gruntfile.js ├── LICENSE ├── README.md ├── app ├── app.js ├── config.json ├── monitor.js ├── public │ ├── audio │ │ └── woop.mp3 │ ├── images │ │ ├── favicon-unread.png │ │ ├── favicon.png │ │ ├── notification.png │ │ └── pattern-b8gl.png │ ├── scripts │ │ ├── AppViewModel.js │ │ ├── BuildMonitorServer.js │ │ ├── BuildViewModel.js │ │ ├── OptionsViewModel.js │ │ ├── helper.js │ │ ├── knockoutExtensions.js │ │ ├── libs │ │ │ ├── cookies.min.js │ │ │ ├── countdown.min.js │ │ │ ├── jquery-2.1.0.min.js │ │ │ ├── jquery.color-2.1.2.min.js │ │ │ ├── knockout-3.2.0.js │ │ │ ├── moment.min.js │ │ │ └── require-2.1.15.min.js │ │ ├── main.js │ │ ├── notification.js │ │ └── settings.js │ ├── stylesheets │ │ ├── base │ │ │ ├── font-awesome-4.2.0 │ │ │ │ ├── css │ │ │ │ │ ├── font-awesome.css │ │ │ │ │ └── font-awesome.min.css │ │ │ │ └── fonts │ │ │ │ │ ├── FontAwesome.otf │ │ │ │ │ ├── fontawesome-webfont.eot │ │ │ │ │ ├── fontawesome-webfont.svg │ │ │ │ │ ├── fontawesome-webfont.ttf │ │ │ │ │ └── fontawesome-webfont.woff │ │ │ ├── fonts.css │ │ │ ├── opensans-condbold │ │ │ │ ├── css │ │ │ │ │ └── opensans-condbold.css │ │ │ │ └── fonts │ │ │ │ │ ├── OpenSans-CondBold-webfont.eot │ │ │ │ │ ├── OpenSans-CondBold-webfont.svg │ │ │ │ │ ├── OpenSans-CondBold-webfont.ttf │ │ │ │ │ └── OpenSans-CondBold-webfont.woff │ │ │ ├── opensans-condlight │ │ │ │ ├── css │ │ │ │ │ └── opensans-condlight.css │ │ │ │ └── fonts │ │ │ │ │ ├── OpenSans-CondLight-demo.html │ │ │ │ │ ├── OpenSans-CondLight-webfont.eot │ │ │ │ │ ├── OpenSans-CondLight-webfont.svg │ │ │ │ │ ├── OpenSans-CondLight-webfont.ttf │ │ │ │ │ └── OpenSans-CondLight-webfont.woff │ │ │ └── style.css │ │ └── themes │ │ │ ├── default │ │ │ └── style.css │ │ │ ├── lingo │ │ │ ├── marck-script │ │ │ │ ├── css │ │ │ │ │ └── marck-script.css │ │ │ │ └── fonts │ │ │ │ │ ├── Marck-Script.ttf │ │ │ │ │ ├── Marck-Script.ttf.eot │ │ │ │ │ ├── Marck-Script.ttf.svg │ │ │ │ │ └── Marck-Script.ttf.woff │ │ │ └── style.css │ │ │ ├── list │ │ │ └── style.css │ │ │ ├── lowres │ │ │ └── style.css │ │ │ ├── minimal │ │ │ └── style.css │ │ │ ├── stoplight │ │ │ └── style.css │ │ │ ├── twenty │ │ │ └── style.css │ │ │ └── twentyfive │ │ │ └── style.css │ └── templates │ │ └── themes │ │ ├── default.html │ │ ├── lingo.html │ │ ├── list.html │ │ ├── lowres.html │ │ ├── minimal.html │ │ ├── stoplight.html │ │ ├── twenty.html │ │ └── twentyfive.html ├── requests.js ├── services │ ├── Bamboo.js │ ├── BambooDeploy.js │ ├── BitbucketPipelines.js │ ├── Bitrise.js │ ├── BuddyBuild.js │ ├── Buildkite.js │ ├── CCTray.js │ ├── CircleCI.js │ ├── Drone.js │ ├── GitLab.js │ ├── Jenkins.js │ ├── PRTG.js │ ├── Shippable.js │ ├── TeamCity.js │ ├── Tfs.js │ ├── Tfs2015.js │ ├── TfsProxy.js │ ├── TfsRelease.js │ └── Travis.js └── views │ ├── health.pug │ ├── index.pug │ ├── layout.pug │ ├── options.pug │ └── overlay.pug ├── charts ├── README.md └── build-monitor │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── configmap.yaml │ ├── deployment.yaml │ ├── ingress.yaml │ └── service.yaml │ └── values.yaml ├── docker-ci ├── Dockerfile └── config.json ├── docker ├── .gitignore ├── Dockerfile ├── config.example.json ├── docker-compose.with-self-signed-certs.yml ├── docker-compose.with-tfs-proxy.yml └── docker-compose.yml ├── docs └── node-build-monitor.png ├── node-build-monitor.sublime-project ├── package.json ├── test ├── fake.js ├── mocha.opts ├── monitor.js └── services │ ├── BuddyBuild.js │ ├── Buildkite.js │ ├── CircleCI.js │ ├── Drone.js │ ├── GitLab.js │ ├── Jenkins │ ├── Jenkins.js │ ├── scenario_1.js │ ├── scenario_2.js │ └── scenario_3.js │ ├── Travis.js │ └── data │ └── ca.pem └── yarn.lock /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-12 2 | 3 | ENV DEBIAN_FRONTEND=noninteractive 4 | 5 | # RUN apt-get update \ 6 | # && apt-get -y install --no-install-recommends \ 7 | # # 8 | # # Clean up 9 | # && apt-get autoremove -y \ 10 | # && apt-get clean -y \ 11 | # && rm -rf /var/lib/apt/lists/* 12 | 13 | RUN npm install -g grunt-cli 14 | 15 | ENV DEBIAN_FRONTEND=dialog 16 | 17 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.101.1/containers/javascript-node-12 3 | { 4 | "name": "Node.js 12", 5 | "dockerFile": "Dockerfile", 6 | "settings": { 7 | "terminal.integrated.shell.linux": "/bin/bash" 8 | }, 9 | "extensions": [ 10 | "dbaeumer.vscode-eslint" 11 | ],"forwardPorts": [ 3000 ], 12 | "postCreateCommand": "yarn install", 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | end_of_line = lf 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | branches: [ master ] 8 | pull_request: 9 | branches: [ master ] 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | 19 | - name: Use Node.js 14.x 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: 14.x 23 | 24 | - name: Install grunt-cli 25 | run: npm install -g grunt-cli 26 | 27 | - name: Install local dependencies 28 | run: npm install 29 | 30 | - name: Run CI build 31 | run: npm run-script ci 32 | 33 | release: 34 | if: startsWith(github.ref, 'refs/tags/') 35 | runs-on: ubuntu-latest 36 | needs: build 37 | 38 | steps: 39 | - name: Checkout code 40 | uses: actions/checkout@v2 41 | 42 | - name: Use Node.js 14.x 43 | uses: actions/setup-node@v1 44 | with: 45 | node-version: 14.x 46 | 47 | - name: Install global dependencies 48 | run: | 49 | npm install -g grunt-cli 50 | npm install -g pkg 51 | 52 | - name: Install local dependencies 53 | run: npm install 54 | 55 | - name: Build Release Package 56 | run: npm run-script pkg 57 | 58 | - name: Create Release 59 | id: create_release 60 | uses: actions/create-release@v1 61 | env: 62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 63 | with: 64 | tag_name: ${{ github.ref }} 65 | release_name: ${{ github.ref }} 66 | draft: true 67 | prerelease: false 68 | 69 | - name: Upload Release Asset (Linux) 70 | id: upload-release-asset-linux 71 | uses: actions/upload-release-asset@v1 72 | env: 73 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 74 | with: 75 | upload_url: ${{ steps.create_release.outputs.upload_url }} 76 | asset_path: ./release/node-build-monitor-linux 77 | asset_name: node-build-monitor-linux 78 | asset_content_type: application/octet-stream 79 | 80 | - name: Upload Release Asset (Mac OS) 81 | id: upload-release-asset-macos 82 | uses: actions/upload-release-asset@v1 83 | env: 84 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 85 | with: 86 | upload_url: ${{ steps.create_release.outputs.upload_url }} 87 | asset_path: ./release/node-build-monitor-macos 88 | asset_name: node-build-monitor-macos 89 | asset_content_type: application/octet-stream 90 | 91 | - name: Upload Release Asset (Windows) 92 | id: upload-release-asset-windows 93 | uses: actions/upload-release-asset@v1 94 | env: 95 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 96 | with: 97 | upload_url: ${{ steps.create_release.outputs.upload_url }} 98 | asset_path: ./release/node-build-monitor-win.exe 99 | asset_name: node-build-monitor-win.exe 100 | asset_content_type: application/octet-stream 101 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.sublime-workspace 3 | log.txt 4 | .idea/ 5 | /package-lock.json 6 | /npm-debug.log -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: node:latest 2 | # This folder is cached between builds 3 | # http://docs.gitlab.com/ce/ci/yaml/README.html#cache 4 | cache: 5 | paths: 6 | - node_modules/ 7 | 8 | before_script: 9 | - echo "run test" 10 | 11 | stages: 12 | - test 13 | 14 | test_mocha: 15 | stage: test 16 | script: 17 | - npm install 18 | - npm run ci 19 | 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '6' 4 | script: npm run-script ci 5 | before_script: 6 | - npm install -g grunt-cli 7 | - npm install -g pkg@4.3.0 8 | 9 | # before_deploy: 10 | # - npm run-script pkg 11 | # deploy: 12 | # provider: releases 13 | # api_key: 14 | # secure: eF8lW5MUWQlU3atb6AgZ8a+y03pXtp6v829WqoUI6ODyKdYlTnlUqPYRfOXaSX8OmW1gyHS+38z9U7C0Q0b3jC5R4Sm0JfmbnhD6nO9MvD6pt3MijzIuhZxXT0f13BIVl7Gt/gA6nUvuH7yXuVaFeQyucuGmfnNiOz+ih46UfSw= 15 | # file_glob: true 16 | # file: ./release/* 17 | # skip_cleanup: true 18 | # on: 19 | # repo: marcells/node-build-monitor 20 | # tags: true 21 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at conduct-build-monitor@mspi.es. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-slim 2 | 3 | RUN npm install -g forever 4 | 5 | WORKDIR /build-mon 6 | 7 | ADD package.json /build-mon/package.json 8 | RUN npm install 9 | 10 | ADD app /build-mon/app 11 | ADD README.md /build-mon/README.md 12 | 13 | ONBUILD ADD config.json /build-mon/app/config.json 14 | 15 | EXPOSE 3000 16 | 17 | CMD [ "forever","--watch", "--watchDirectory", "/build-mon/app", "/build-mon/app/app.js"] 18 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.initConfig({ 3 | jshint: { 4 | files: [ 'Gruntfile.js', 'app/*.js', 'app/services/*.js', 'app/public/scripts/*.js', 'test/**/*.js', '!test/**/scenario_*.js' ], 5 | options: { 6 | expr: true, 7 | esversion: 6 8 | } 9 | }, 10 | mochaTest: { 11 | test: { 12 | options : { 13 | reporter: 'spec', 14 | require: 'should' 15 | }, 16 | src: ['test/**/*.js'] 17 | }, 18 | watch: { 19 | options : { 20 | reporter: 'dot', 21 | require: 'should' 22 | }, 23 | src: ['test/**/*.js'] 24 | } 25 | }, 26 | watch: { 27 | files: ['<%= jshint.files %>'], 28 | tasks: ['jshint', 'mochaTest:watch'] 29 | }, 30 | bump: { 31 | options: { 32 | pushTo: 'origin' 33 | } 34 | } 35 | }); 36 | 37 | grunt.loadNpmTasks('grunt-contrib-jshint'); 38 | grunt.loadNpmTasks('grunt-contrib-watch'); 39 | grunt.loadNpmTasks('grunt-mocha-test'); 40 | grunt.loadNpmTasks('grunt-bump'); 41 | 42 | grunt.registerTask('ci', ['jshint', 'mochaTest:test' ]); 43 | grunt.registerTask('default', ['ci']); 44 | }; 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Marcell Spies ([@marcells](https://twitter.com/marcells) | http://mspi.es) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'), 2 | http = require('http'), 3 | path = require('path'), 4 | socketio = require('socket.io'), 5 | fs = require('fs'), 6 | morgan = require('morgan'), 7 | errorhandler = require('errorhandler'), 8 | version = require('../package').version, 9 | app = express(); 10 | 11 | function getConfig() { 12 | const defaultConfigFileName = 'config.json'; 13 | const userConfigFileName = 'node-build-monitor-config.json'; 14 | const possibleFileNames = [ 15 | path.join(require('os').homedir(), userConfigFileName), 16 | ...(process.pkg !== undefined ? [ path.join(path.dirname(process.execPath), defaultConfigFileName) ] : []), 17 | path.join(__dirname, defaultConfigFileName) 18 | ]; 19 | 20 | const availableFileNames = possibleFileNames.filter(possibleFileName => fs.existsSync(possibleFileName)); 21 | 22 | if (availableFileNames.length === 0) { 23 | const humanReadableFileList = possibleFileNames.map((value, index) => ` ${index + 1}. ${value}`).join('\n'); 24 | throw new Error(`Please provide a configuration file at one of the following locations: \n${humanReadableFileList}`); 25 | } 26 | 27 | return JSON.parse(fs.readFileSync(availableFileNames[0], 'utf8')); 28 | } 29 | 30 | function printStartupInformation() { 31 | const importantEnvironmentVariables = [ 32 | { 33 | name : 'PORT', 34 | defaultValue : '3000' 35 | }, 36 | { 37 | name : 'NODE_TLS_REJECT_UNAUTHORIZED', 38 | defaultValue : '1' 39 | }]; 40 | 41 | console.log(`Printing environment Variables...`); 42 | importantEnvironmentVariables 43 | .map(x => ({ variable : x, stringValue : process.env.hasOwnProperty(x.name) ? process.env[x.name] : `unset (Default: ${x.defaultValue})` })) 44 | .forEach(x => console.log(` ${x.variable.name} = ${x.stringValue}`)); 45 | 46 | console.log('node-build-monitor is starting...'); 47 | } 48 | 49 | printStartupInformation(); 50 | 51 | var config = getConfig(); 52 | 53 | app.set('port', process.env.PORT || 3000); 54 | app.set('views', path.join(__dirname, 'views')); 55 | app.set('view engine', 'pug'); 56 | app.use(morgan('combined', { skip: (req, res) => res.statusCode < 400 })); 57 | app.get('/', function(req, res) { 58 | res.render('index', { 59 | title: 'Build Monitor' 60 | }); 61 | }); 62 | app.get('/health', function(req, res) { 63 | res.render('health', { 64 | title: 'Build Monitor Health' 65 | }); 66 | }); 67 | app.use(express.static(path.join(__dirname, 'public'))); 68 | 69 | if ('development' === app.get('env')) { 70 | app.use(errorhandler()); 71 | } 72 | 73 | // run express 74 | var server = http.createServer(app), 75 | io = socketio.listen(server); 76 | 77 | server.listen(app.get('port'), function() { 78 | console.log(`node-build-monitor ${version} is listening on port ${app.get('port')}...`); 79 | }); 80 | 81 | // run socket.io 82 | io.sockets.on('connection', function (socket) { 83 | socket.emit('settingsChanged', { 84 | version: version 85 | }); 86 | 87 | socket.emit('buildsLoaded', monitor.currentBuilds); 88 | }); 89 | 90 | // configure monitor 91 | var Monitor = require('./monitor'), 92 | monitor = new Monitor(); 93 | 94 | for (var i = 0; i < config.services.length; i++) { 95 | var serviceConfig = config.services[i], 96 | service = new (require('./services/' + serviceConfig.name))(); 97 | 98 | service.configure(tryExpandEnvironmentVariables(config.monitor, serviceConfig.configuration)); 99 | 100 | monitor.watchOn(service); 101 | } 102 | 103 | monitor.configure(config.monitor); 104 | 105 | monitor.on('buildsChanged', function (changes) { 106 | io.emit('buildsChanged', changes); 107 | }); 108 | 109 | // run monitor 110 | monitor.run(); 111 | 112 | // helpers 113 | function tryExpandEnvironmentVariables(monitorConfiguration, serviceConfiguration) { 114 | if (monitorConfiguration.expandEnvironmentVariables) { 115 | for (var property in serviceConfiguration) { 116 | serviceConfiguration[property] = tryExpandEnvironmentVariable(serviceConfiguration[property]); 117 | } 118 | } 119 | 120 | return serviceConfiguration; 121 | } 122 | 123 | function tryExpandEnvironmentVariable(value) { 124 | const environmentPattern = /^\${(.*?)}$/g; 125 | 126 | if (typeof value === 'string') { 127 | let match = environmentPattern.exec(value); 128 | 129 | if (match && match.length == 2) { 130 | return process.env[match[1]]; 131 | } 132 | } 133 | 134 | return value; 135 | } -------------------------------------------------------------------------------- /app/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "monitor": { 3 | "interval": 5000, 4 | "numberOfBuilds": 12, 5 | "latestBuildOnly": false, 6 | "sortOrder": "date", 7 | "debug": true 8 | }, 9 | "services": [ 10 | { 11 | "name": "Travis", 12 | "configuration": { 13 | "slug": "marcells/bloggy" 14 | } 15 | }, 16 | { 17 | "name": "Travis", 18 | "configuration": { 19 | "slug": "marcells/node-build-monitor" 20 | } 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /app/public/audio/woop.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcells/node-build-monitor/b13c156f7ac4df2559808642c9a408e4e0357f16/app/public/audio/woop.mp3 -------------------------------------------------------------------------------- /app/public/images/favicon-unread.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcells/node-build-monitor/b13c156f7ac4df2559808642c9a408e4e0357f16/app/public/images/favicon-unread.png -------------------------------------------------------------------------------- /app/public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcells/node-build-monitor/b13c156f7ac4df2559808642c9a408e4e0357f16/app/public/images/favicon.png -------------------------------------------------------------------------------- /app/public/images/notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcells/node-build-monitor/b13c156f7ac4df2559808642c9a408e4e0357f16/app/public/images/notification.png -------------------------------------------------------------------------------- /app/public/images/pattern-b8gl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcells/node-build-monitor/b13c156f7ac4df2559808642c9a408e4e0357f16/app/public/images/pattern-b8gl.png -------------------------------------------------------------------------------- /app/public/scripts/AppViewModel.js: -------------------------------------------------------------------------------- 1 | define(['ko', 'notification', 'BuildViewModel', 'OptionsViewModel'], function (ko, notification, BuildViewModel, OptionsViewModel) { 2 | var AppViewModel = function() { 3 | var self = this; 4 | 5 | var isLoadingInitially = true; 6 | 7 | this.isIntercepted = ko.observable(); 8 | this.infoType = ko.observable(); 9 | this.faviconImageUrl = ko.observable('images/favicon-unread.png'); 10 | this.builds = ko.observableArray([]); 11 | this.version = ko.observable(); 12 | this.options = new OptionsViewModel(self); 13 | 14 | this.setIsConnected = function (value) { 15 | if(isLoadingInitially) { 16 | isLoadingInitially = false; 17 | return; 18 | } 19 | 20 | self.isIntercepted(!value); 21 | self.infoType('connection'); 22 | }; 23 | 24 | this.setIsLoading = function (value) { 25 | self.isIntercepted(value); 26 | self.infoType('loading'); 27 | }; 28 | 29 | this.setHasUnreadBuilds = function (value) { 30 | if (value) { 31 | self.faviconImageUrl('images/favicon-unread.png'); 32 | } else { 33 | self.faviconImageUrl('images/favicon.png'); 34 | } 35 | }; 36 | 37 | var getBuildById = function (id) { 38 | return self.builds().filter(function (build) { 39 | return build.id() === id; 40 | })[0]; 41 | }; 42 | 43 | var matchesToNotificationFilter = function (build) { 44 | if (!self.options.notificationFilterEnabled()) { 45 | return true; 46 | } 47 | 48 | var regex = new RegExp(self.options.notificationFilterValue()); 49 | 50 | return regex.test(build.id) || 51 | regex.test(build.project) || 52 | regex.test(build.definition) || 53 | regex.test(build.number) || 54 | regex.test(build.requestedFor) || 55 | regex.test(build.statusText) || 56 | regex.test(build.reason); 57 | }; 58 | 59 | var anyBuildMatchesToNotifcationFilter = function (builds) { 60 | 61 | if (builds.length === 0) { 62 | return false; 63 | } 64 | 65 | return builds.some( function (build) { 66 | return matchesToNotificationFilter(build); 67 | }); 68 | }; 69 | 70 | this.loadBuilds = function (builds) { 71 | self.builds.removeAll(); 72 | 73 | builds.forEach(function (build) { 74 | self.builds.push(new BuildViewModel(build)); 75 | }); 76 | }; 77 | 78 | this.updateCurrentBuildsWithChanges = function (changes) { 79 | if (anyBuildMatchesToNotifcationFilter(changes.removed) || 80 | anyBuildMatchesToNotifcationFilter(changes.added) || 81 | anyBuildMatchesToNotifcationFilter(changes.updated)) { 82 | self.setHasUnreadBuilds(true); 83 | } 84 | 85 | changes.removed.forEach(function (build) { 86 | self.builds.remove(function (item) { 87 | return item.id() === build.id; 88 | }); 89 | }); 90 | 91 | changes.added.forEach(function (build, index) { 92 | self.builds.splice(index, 0, new BuildViewModel(build)); 93 | }); 94 | 95 | changes.updated.forEach(function (build) { 96 | var vm = getBuildById(build.id); 97 | vm.update(build); 98 | 99 | if (build.status === 'Red' && matchesToNotificationFilter(build)) { 100 | if (self.options.soundNotificationEnabled()) { 101 | var audio = new Audio('/audio/woop.mp3'); 102 | audio.play(); 103 | } 104 | 105 | if (self.options.browserNotificationEnabled()) { 106 | notification.show(build); 107 | } 108 | } 109 | }); 110 | 111 | changes.order.forEach(function (id, index) { 112 | var build = getBuildById(id); 113 | var from = self.builds.indexOf(build); 114 | 115 | if (from !== index) { 116 | self.builds.splice(index, 0, self.builds.splice(from, 1)[0]); 117 | } 118 | }); 119 | }; 120 | 121 | this.updateBuildTimes = function () { 122 | self.builds().forEach(function (build) { 123 | build.time.evaluateImmediate(); 124 | build.duration.evaluateImmediate(); 125 | }); 126 | }; 127 | 128 | this.setIsLoading(true); 129 | }; 130 | 131 | return AppViewModel; 132 | }); 133 | -------------------------------------------------------------------------------- /app/public/scripts/BuildMonitorServer.js: -------------------------------------------------------------------------------- 1 | define(['io'], function (io) { 2 | var BuildMonitorServer = function () { 3 | var self = this, 4 | cachedSettings; 5 | 6 | this.connect = function () { 7 | pathname = document.location.pathname; 8 | separator = (pathname.substr(-1) == '/')?'':'/'; 9 | io = io(undefined, {path: pathname + separator + 'socket.io/'}); 10 | var socket = io.connect(); 11 | 12 | socket.on('connect', self.onConnected); 13 | socket.on('disconnect', self.onDisconnected); 14 | socket.on('buildsLoaded', function (builds) { 15 | if (!builds) { 16 | return; 17 | } 18 | 19 | self.onBuildsLoaded(builds); 20 | }); 21 | 22 | socket.on('buildsChanged', self.onBuildsChanged); 23 | socket.on('settingsChanged', function (settings) { 24 | if (!cachedSettings) { 25 | cachedSettings = settings; 26 | self.onSettingsLoaded(settings); 27 | return; 28 | } 29 | 30 | if(cachedSettings.version !== settings.version) { 31 | self.onVersionChanged(); 32 | } 33 | }); 34 | }; 35 | }; 36 | 37 | return BuildMonitorServer; 38 | }); 39 | -------------------------------------------------------------------------------- /app/public/scripts/BuildViewModel.js: -------------------------------------------------------------------------------- 1 | define(['ko', 'moment', 'countdown'], function (ko, moment, countdown) { 2 | var BuildViewModel = function (build) { 3 | this.isMenuVisible = ko.observable(false); 4 | 5 | this.id = ko.observable(); 6 | this.isRunning = ko.observable(); 7 | this.isQueued = ko.observable(); 8 | this.project = ko.observable(); 9 | this.branch = ko.observable(); 10 | this.commit = ko.observable(); 11 | this.definition = ko.observable(); 12 | this.number = ko.observable(); 13 | this.startedAt = ko.observable(); 14 | this.finishedAt = ko.observable(); 15 | this.queuedAt = ko.observable(); 16 | this.status = ko.observable(build.status); 17 | this.statusText = ko.observable(); 18 | this.reason = ko.observable(); 19 | this.requestedFor = ko.observable(); 20 | this.hasWarnings = ko.observable(); 21 | this.hasErrors = ko.observable(); 22 | this.url = ko.observable(); 23 | 24 | this.update = function (build) { 25 | this.id(build.id); 26 | this.isRunning(build.isRunning); 27 | this.isQueued(build.isQueued); 28 | this.project(build.project); 29 | this.branch(build.branch); 30 | this.commit(build.commit); 31 | this.definition(build.definition); 32 | this.number(build.number); 33 | this.startedAt(moment(build.startedAt)); 34 | this.finishedAt(moment(build.finishedAt)); 35 | this.queuedAt(moment(build.queuedAt)); 36 | this.status(build.status); 37 | this.statusText(build.statusText); 38 | this.reason(build.reason); 39 | this.requestedFor(build.requestedFor); 40 | this.hasWarnings(build.hasWarnings); 41 | this.hasErrors(build.hasErrors); 42 | this.url(build.url); 43 | }; 44 | 45 | this.update(build); 46 | 47 | this.style = ko.computed(function () { 48 | if (this.status()) { 49 | return { 50 | 'color': 'white', 51 | 'background-color': this.status().toLowerCase() 52 | }; 53 | } 54 | }, this); 55 | 56 | this.time = ko.forcibleComputed(function () { 57 | return this.isRunning() ? 58 | 'started ' + moment(this.startedAt()).fromNow() : 59 | (this.isQueued() ? 60 | 'not started yet' : 61 | 'finished ' + moment(this.finishedAt()).fromNow() 62 | ); 63 | }, this); 64 | 65 | this.duration = ko.forcibleComputed(function () { 66 | return this.isRunning() ? 67 | 'running for ' + countdown(this.startedAt()).toString() : 68 | (this.isQueued() ? 69 | 'queued for ' + countdown(this.startedAt(), this.queuedAt()).toString() : 70 | 'ran for ' + countdown(this.startedAt(), this.finishedAt()).toString() 71 | ); 72 | }, this); 73 | 74 | this.isMenuAvailable = ko.computed(function () { 75 | return this.url() || false; 76 | }, this); 77 | }; 78 | 79 | return BuildViewModel; 80 | }); 81 | -------------------------------------------------------------------------------- /app/public/scripts/OptionsViewModel.js: -------------------------------------------------------------------------------- 1 | define(['ko', 'helper', 'settings', 'notification'], function (ko, helper, settings, notification) { 2 | var OptionsViewModel = function (app) { 3 | var self = this; 4 | 5 | self.isMenuVisible = ko.observable(false); 6 | self.isMenuButtonVisible = ko.observable(false); 7 | self.theme = ko.observable(helper.getUrlParameter('theme') || settings.theme); 8 | self.themes = ko.observableArray(['default', 'stoplight', 'list', 'lingo', 'lowres', 'minimal', 'twenty', 'twentyfive']); 9 | self.browserNotificationSupported = ko.observable(notification.isSupportedAndNotDenied()); 10 | self.browserNotificationEnabled = ko.observable(notification.isSupportedAndNotDenied() && settings.browserNotificationEnabled); 11 | self.soundNotificationEnabled = ko.observable(settings.soundNotificationEnabled); 12 | self.notificationFilterEnabled = ko.observable(settings.notificationFilterEnabled); 13 | self.notificationFilterValue = ko.observable(settings.notificationFilterValue); 14 | self.version = app.version; 15 | 16 | helper.detectGlobalInteraction( 17 | function() { 18 | self.isMenuButtonVisible(!self.isMenuVisible()); 19 | }, 20 | function() { 21 | self.isMenuButtonVisible(self.isMenuVisible()); 22 | }); 23 | 24 | self.changeTheme = function (theme) { 25 | settings.theme = theme; 26 | self.theme(theme); 27 | }; 28 | 29 | self.show = function () { 30 | self.isMenuVisible(true); 31 | self.isMenuButtonVisible(false); 32 | }; 33 | 34 | self.hide = function () { 35 | self.isMenuVisible(false); 36 | self.isMenuButtonVisible(true); 37 | }; 38 | 39 | self.browserNotificationEnabled.subscribe(function (enabled) { 40 | if (enabled) { 41 | notification.ensureGranted( 42 | function () { 43 | settings.browserNotificationEnabled = true; 44 | }, 45 | function () { 46 | self.browserNotificationEnabled(false); 47 | settings.browserNotificationEnabled = false; 48 | }); 49 | } else { 50 | settings.browserNotificationEnabled = false; 51 | } 52 | }); 53 | 54 | self.soundNotificationEnabled.subscribe(function (enabled) { 55 | settings.soundNotificationEnabled = enabled; 56 | }); 57 | 58 | self.notificationFilterEnabled.subscribe(function (enabled) { 59 | settings.notificationFilterEnabled = enabled; 60 | }); 61 | 62 | self.notificationFilterValue.subscribe(function (value) { 63 | settings.notificationFilterValue = value; 64 | }); 65 | }; 66 | 67 | return OptionsViewModel; 68 | }); 69 | -------------------------------------------------------------------------------- /app/public/scripts/helper.js: -------------------------------------------------------------------------------- 1 | define(function () { 2 | var getUrlParameter = function (parameter) { 3 | var pageUrl = window.location.search.substring(1), 4 | urlParameters = pageUrl.split('&'); 5 | 6 | for (var i = 0; i < urlParameters.length; i++) { 7 | var parameterName = urlParameters[i].split('='); 8 | if (parameterName[0] == parameter) { 9 | return parameterName[1]; 10 | } 11 | } 12 | }; 13 | 14 | var detectGlobalInteraction = function (show, hide) { 15 | detectInteraction(window, show, hide); 16 | }; 17 | 18 | var detectInteraction = function (elementName, show, hide) { 19 | var isShown = false; 20 | var nextTimeout = new Date(); 21 | var element = $(elementName) || $(window); 22 | 23 | setInterval(function() { 24 | if (isShown && (new Date() - nextTimeout > 2000)) { 25 | isShown = false; 26 | hide(); 27 | } 28 | 29 | }, 1000); 30 | 31 | function interactionDetected () { 32 | nextTimeout = new Date(); 33 | 34 | if(!isShown) { 35 | show(); 36 | isShown = true; 37 | } 38 | } 39 | 40 | element.keydown(function (event) { 41 | interactionDetected(); 42 | }); 43 | 44 | element.mousemove(function (event) { 45 | interactionDetected(); 46 | }); 47 | 48 | element.mousedown(function (event) { 49 | interactionDetected(); 50 | }); 51 | }; 52 | 53 | return { 54 | getUrlParameter: getUrlParameter, 55 | detectGlobalInteraction: detectGlobalInteraction, 56 | detectInteraction: detectInteraction 57 | }; 58 | }); -------------------------------------------------------------------------------- /app/public/scripts/knockoutExtensions.js: -------------------------------------------------------------------------------- 1 | define(['ko'], function (ko) { 2 | this.register = function () { 3 | ko.forcibleComputed = function(readFunc, context, options) { 4 | var trigger = ko.observable(), 5 | target = ko.computed(function() { 6 | trigger(); 7 | return readFunc.call(context); 8 | }, null, options); 9 | target.evaluateImmediate = function() { 10 | trigger.valueHasMutated(); 11 | }; 12 | return target; 13 | }; 14 | 15 | ko.bindingHandlers.animateCss = { 16 | update: function(element, valueAccessor) { 17 | var value = valueAccessor(); 18 | var unwrap = ko.unwrap(value); 19 | 20 | if(unwrap) { 21 | $(element).animate(unwrap, 1000); 22 | } 23 | } 24 | }; 25 | 26 | ko.bindingHandlers.fade = { 27 | update: function(element, valueAccessor, allBindings) { 28 | var value = valueAccessor(); 29 | var unwrap = ko.unwrap(value); 30 | 31 | var fadeInDuration = allBindings.get('fadeInDuration') || 300; 32 | var fadeOutDuration = allBindings.get('fadeOutDuration') || 1500; 33 | 34 | if(unwrap) { 35 | $(element).fadeIn(fadeInDuration); 36 | } else { 37 | $(element).fadeOut(fadeOutDuration); 38 | } 39 | } 40 | }; 41 | 42 | ko.bindingHandlers.hover = { 43 | init: function(element, valueAccessor) { 44 | var value = valueAccessor(); 45 | var timeout; 46 | 47 | $(element).mousemove(function (event) { 48 | value(true); 49 | 50 | window.clearTimeout(timeout); 51 | timeout = window.setTimeout(function () { 52 | value(false); 53 | }, 2000); 54 | }); 55 | 56 | $(element).mouseleave(function (event) { 57 | value(false); 58 | 59 | if (timeout) { 60 | window.clearTimeout(timeout); 61 | timeout = null; 62 | } 63 | }); 64 | } 65 | }; 66 | 67 | ko.bindingHandlers.changeFavicon = { 68 | update: function(element, valueAccessor, allBindings) { 69 | function changeFavicon(src) { 70 | var link = document.createElement('link'), 71 | oldLink = document.getElementById('dynamic-favicon'); 72 | link.id = 'dynamic-favicon'; 73 | link.rel = 'shortcut icon'; 74 | link.href = src + '?=' + Math.random(); 75 | 76 | if (oldLink) { 77 | document.head.removeChild(oldLink); 78 | } 79 | document.head.appendChild(link); 80 | } 81 | 82 | var value = valueAccessor(); 83 | var unwrap = ko.unwrap(value); 84 | 85 | changeFavicon(unwrap); 86 | } 87 | }; 88 | 89 | var namingConventionLoader = { 90 | getConfig: function(name, callback) { 91 | var templateConfig = { 92 | templateUrl: 'templates/themes/' + name + '.html', 93 | cssUrl: 'stylesheets/themes/' + name + '/style.css' 94 | }; 95 | 96 | callback({ template: templateConfig }); 97 | } 98 | }; 99 | 100 | ko.components.loaders.unshift(namingConventionLoader); 101 | 102 | var templateFromUrlLoader = { 103 | loadTemplate: function(name, templateConfig, callback) { 104 | function loadCss(filename) { 105 | var linkElement = document.createElement("link"); 106 | linkElement.setAttribute("rel", "stylesheet"); 107 | linkElement.setAttribute("type", "text/css"); 108 | linkElement.setAttribute("href", filename); 109 | 110 | $("head").append(linkElement); 111 | } 112 | 113 | if (templateConfig.templateUrl) { 114 | $.get(templateConfig.templateUrl, function(markupString) { 115 | ko.components.defaultLoader.loadTemplate(name, markupString, callback); 116 | loadCss(templateConfig.cssUrl); 117 | }); 118 | } else { 119 | callback(null); 120 | } 121 | } 122 | }; 123 | 124 | ko.components.loaders.unshift(templateFromUrlLoader); 125 | }; 126 | 127 | return this; 128 | }); 129 | -------------------------------------------------------------------------------- /app/public/scripts/libs/cookies.min.js: -------------------------------------------------------------------------------- 1 | (function(g,f){"use strict";var h=function(e){if("object"!==typeof e.document)throw Error("Cookies.js requires a `window` with a `document` object");var b=function(a,d,c){return 1===arguments.length?b.get(a):b.set(a,d,c)};b._document=e.document;b._cacheKeyPrefix="cookey.";b._maxExpireDate=new Date("Fri, 31 Dec 9999 23:59:59 UTC");b.defaults={path:"/",secure:!1};b.get=function(a){b._cachedDocumentCookie!==b._document.cookie&&b._renewCache();return b._cache[b._cacheKeyPrefix+a]};b.set=function(a,d,c){c=b._getExtendedOptions(c); c.expires=b._getExpiresDate(d===f?-1:c.expires);b._document.cookie=b._generateCookieString(a,d,c);return b};b.expire=function(a,d){return b.set(a,f,d)};b._getExtendedOptions=function(a){return{path:a&&a.path||b.defaults.path,domain:a&&a.domain||b.defaults.domain,expires:a&&a.expires||b.defaults.expires,secure:a&&a.secure!==f?a.secure:b.defaults.secure}};b._isValidDate=function(a){return"[object Date]"===Object.prototype.toString.call(a)&&!isNaN(a.getTime())};b._getExpiresDate=function(a,d){d=d||new Date; "number"===typeof a?a=Infinity===a?b._maxExpireDate:new Date(d.getTime()+1E3*a):"string"===typeof a&&(a=new Date(a));if(a&&!b._isValidDate(a))throw Error("`expires` parameter cannot be converted to a valid Date instance");return a};b._generateCookieString=function(a,b,c){a=a.replace(/[^#$&+\^`|]/g,encodeURIComponent);a=a.replace(/\(/g,"%28").replace(/\)/g,"%29");b=(b+"").replace(/[^!#$&-+\--:<-\[\]-~]/g,encodeURIComponent);c=c||{};a=a+"="+b+(c.path?";path="+c.path:"");a+=c.domain?";domain="+c.domain: "";a+=c.expires?";expires="+c.expires.toUTCString():"";return a+=c.secure?";secure":""};b._getCacheFromString=function(a){var d={};a=a?a.split("; "):[];for(var c=0;cb?a.length:b;return{key:decodeURIComponent(a.substr(0,b)),value:decodeURIComponent(a.substr(b+1))}};b._renewCache=function(){b._cache= b._getCacheFromString(b._document.cookie);b._cachedDocumentCookie=b._document.cookie};b._areEnabled=function(){var a="1"===b.set("cookies.js",1).get("cookies.js");b.expire("cookies.js");return a};b.enabled=b._areEnabled();return b},e="object"===typeof g.document?h(g):h;"function"===typeof define&&define.amd?define(function(){return e}):"object"===typeof exports?("object"===typeof module&&"object"===typeof module.exports&&(exports=module.exports=e),exports.Cookies=e):g.Cookies=e})("undefined"===typeof window? this:window); -------------------------------------------------------------------------------- /app/public/scripts/libs/countdown.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | countdown.js v2.3.4 http://countdownjs.org 3 | Copyright (c)2006-2012 Stephen M. McKamey. 4 | Licensed under The MIT License. 5 | */ 6 | var module,countdown=function(r){function v(a,b){var c=a.getTime();a.setUTCMonth(a.getUTCMonth()+b);return Math.round((a.getTime()-c)/864E5)}function t(a){var b=a.getTime(),c=new Date(b);c.setUTCMonth(a.getUTCMonth()+1);return Math.round((c.getTime()-b)/864E5)}function f(a,b){return a+" "+(1===a?p[b]:q[b])}function n(){}function l(a,b,c,g,x,d){0<=a[c]&&(b+=a[c],delete a[c]);b/=x;if(1>=b+1)return 0;if(0<=a[g]){a[g]=+(a[g]+b).toFixed(d);switch(g){case "seconds":if(60!==a.seconds||isNaN(a.minutes))break; 7 | a.minutes++;a.seconds=0;case "minutes":if(60!==a.minutes||isNaN(a.hours))break;a.hours++;a.minutes=0;case "hours":if(24!==a.hours||isNaN(a.days))break;a.days++;a.hours=0;case "days":if(7!==a.days||isNaN(a.weeks))break;a.weeks++;a.days=0;case "weeks":if(a.weeks!==t(a.refMonth)/7||isNaN(a.months))break;a.months++;a.weeks=0;case "months":if(12!==a.months||isNaN(a.years))break;a.years++;a.months=0;case "years":if(10!==a.years||isNaN(a.decades))break;a.decades++;a.years=0;case "decades":if(10!==a.decades|| 8 | isNaN(a.centuries))break;a.centuries++;a.decades=0;case "centuries":if(10!==a.centuries||isNaN(a.millennia))break;a.millennia++;a.centuries=0}return 0}return b}function w(a,b,c,g,d,k){a.start=b;a.end=c;a.units=g;a.value=c.getTime()-b.getTime();if(0>a.value){var f=c;c=b;b=f}a.refMonth=new Date(b.getFullYear(),b.getMonth(),15);try{a.millennia=0;a.centuries=0;a.decades=0;a.years=c.getUTCFullYear()-b.getUTCFullYear();a.months=c.getUTCMonth()-b.getUTCMonth();a.weeks=0;a.days=c.getUTCDate()-b.getUTCDate(); 9 | a.hours=c.getUTCHours()-b.getUTCHours();a.minutes=c.getUTCMinutes()-b.getUTCMinutes();a.seconds=c.getUTCSeconds()-b.getUTCSeconds();a.milliseconds=c.getUTCMilliseconds()-b.getUTCMilliseconds();var h;0>a.milliseconds?(h=s(-a.milliseconds/1E3),a.seconds-=h,a.milliseconds+=1E3*h):1E3<=a.milliseconds&&(a.seconds+=m(a.milliseconds/1E3),a.milliseconds%=1E3);0>a.seconds?(h=s(-a.seconds/60),a.minutes-=h,a.seconds+=60*h):60<=a.seconds&&(a.minutes+=m(a.seconds/60),a.seconds%=60);0>a.minutes?(h=s(-a.minutes/ 10 | 60),a.hours-=h,a.minutes+=60*h):60<=a.minutes&&(a.hours+=m(a.minutes/60),a.minutes%=60);0>a.hours?(h=s(-a.hours/24),a.days-=h,a.hours+=24*h):24<=a.hours&&(a.days+=m(a.hours/24),a.hours%=24);for(;0>a.days;)a.months--,a.days+=v(a.refMonth,1);7<=a.days&&(a.weeks+=m(a.days/7),a.days%=7);0>a.months?(h=s(-a.months/12),a.years-=h,a.months+=12*h):12<=a.months&&(a.years+=m(a.months/12),a.months%=12);10<=a.years&&(a.decades+=m(a.years/10),a.years%=10,10<=a.decades&&(a.centuries+=m(a.decades/10),a.decades%= 11 | 10,10<=a.centuries&&(a.millennia+=m(a.centuries/10),a.centuries%=10)));b=0;!(g&1024)||b>=d?(a.centuries+=10*a.millennia,delete a.millennia):a.millennia&&b++;!(g&512)||b>=d?(a.decades+=10*a.centuries,delete a.centuries):a.centuries&&b++;!(g&256)||b>=d?(a.years+=10*a.decades,delete a.decades):a.decades&&b++;!(g&128)||b>=d?(a.months+=12*a.years,delete a.years):a.years&&b++;!(g&64)||b>=d?(a.months&&(a.days+=v(a.refMonth,a.months)),delete a.months,7<=a.days&&(a.weeks+=m(a.days/7),a.days%=7)):a.months&& 12 | b++;!(g&32)||b>=d?(a.days+=7*a.weeks,delete a.weeks):a.weeks&&b++;!(g&16)||b>=d?(a.hours+=24*a.days,delete a.days):a.days&&b++;!(g&8)||b>=d?(a.minutes+=60*a.hours,delete a.hours):a.hours&&b++;!(g&4)||b>=d?(a.seconds+=60*a.minutes,delete a.minutes):a.minutes&&b++;!(g&2)||b>=d?(a.milliseconds+=1E3*a.seconds,delete a.seconds):a.seconds&&b++;if(!(g&1)||b>=d){var e=l(a,0,"milliseconds","seconds",1E3,k);if(e&&(e=l(a,e,"seconds","minutes",60,k))&&(e=l(a,e,"minutes","hours",60,k))&&(e=l(a,e,"hours","days", 13 | 24,k))&&(e=l(a,e,"days","weeks",7,k))&&(e=l(a,e,"weeks","months",t(a.refMonth)/7,k))){g=e;var n,p=a.refMonth,q=p.getTime(),r=new Date(q);r.setUTCFullYear(p.getUTCFullYear()+1);n=Math.round((r.getTime()-q)/864E5);if(e=l(a,g,"months","years",n/t(a.refMonth),k))if(e=l(a,e,"years","decades",10,k))if(e=l(a,e,"decades","centuries",10,k))if(e=l(a,e,"centuries","millennia",10,k))throw Error("Fractional unit overflow");}}}finally{delete a.refMonth}return a}function d(a,b,c,d,f){var k;c=+c||222;d=0f?Math.round(f):20:0;"function"===typeof a?(k=a,a=null):a instanceof Date||(a=null!==a&&isFinite(a)?new Date(a):null);"function"===typeof b?(k=b,b=null):b instanceof Date||(b=null!==b&&isFinite(b)?new Date(b):null);if(!a&&!b)return new n;if(!k)return w(new n,a||new Date,b||new Date,c,d,f);var l=c&1?1E3/30:c&2?1E3:c&4?6E4:c&8?36E5:c&16?864E5:6048E5,h,e=function(){k(w(new n,a||new Date,b||new Date,c,d,f),h)};e();return h=setInterval(e,l)}var s=Math.ceil,m=Math.floor,p,q,u;n.prototype.toString= 15 | function(){var a=u(this),b=a.length;if(!b)return"";1=c;c++)p[c]=a[c]||p[c],q[c]=b[c]||q[c]};(d.resetLabels=function(){p="millisecond second minute hour day week month year decade century millennium".split(" "); 17 | q="milliseconds seconds minutes hours days weeks months years decades centuries millennia".split(" ")})();r&&r.exports?r.exports=d:"function"===typeof window.define&&window.define.amd&&window.define("countdown",[],function(){return d});return d}(module); -------------------------------------------------------------------------------- /app/public/scripts/libs/jquery.color-2.1.2.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery Color v@2.1.2 http://github.com/jquery/jquery-color | jquery.org/license */ 2 | (function(a,b){function m(a,b,c){var d=h[b.type]||{};return a==null?c||!b.def?null:b.def:(a=d.floor?~~a:parseFloat(a),isNaN(a)?b.def:d.mod?(a+d.mod)%d.mod:0>a?0:d.max")[0],k,l=a.each;j.style.cssText="background-color:rgba(1,1,1,.5)",i.rgba=j.style.backgroundColor.indexOf("rgba")>-1,l(g,function(a,b){b.cache="_"+a,b.props.alpha={idx:3,type:"percent",def:1}}),f.fn=a.extend(f.prototype,{parse:function(c,d,e,h){if(c===b)return this._rgba=[null,null,null,null],this;if(c.jquery||c.nodeType)c=a(c).css(d),d=b;var i=this,j=a.type(c),o=this._rgba=[];d!==b&&(c=[c,d,e,h],j="array");if(j==="string")return this.parse(n(c)||k._default);if(j==="array")return l(g.rgba.props,function(a,b){o[b.idx]=m(c[b.idx],b)}),this;if(j==="object")return c instanceof f?l(g,function(a,b){c[b.cache]&&(i[b.cache]=c[b.cache].slice())}):l(g,function(b,d){var e=d.cache;l(d.props,function(a,b){if(!i[e]&&d.to){if(a==="alpha"||c[a]==null)return;i[e]=d.to(i._rgba)}i[e][b.idx]=m(c[a],b,!0)}),i[e]&&a.inArray(null,i[e].slice(0,3))<0&&(i[e][3]=1,d.from&&(i._rgba=d.from(i[e])))}),this},is:function(a){var b=f(a),c=!0,d=this;return l(g,function(a,e){var f,g=b[e.cache];return g&&(f=d[e.cache]||e.to&&e.to(d._rgba)||[],l(e.props,function(a,b){if(g[b.idx]!=null)return c=g[b.idx]===f[b.idx],c})),c}),c},_space:function(){var a=[],b=this;return l(g,function(c,d){b[d.cache]&&a.push(c)}),a.pop()},transition:function(a,b){var c=f(a),d=c._space(),e=g[d],i=this.alpha()===0?f("transparent"):this,j=i[e.cache]||e.to(i._rgba),k=j.slice();return c=c[e.cache],l(e.props,function(a,d){var e=d.idx,f=j[e],g=c[e],i=h[d.type]||{};if(g===null)return;f===null?k[e]=g:(i.mod&&(g-f>i.mod/2?f+=i.mod:f-g>i.mod/2&&(f-=i.mod)),k[e]=m((g-f)*b+f,d))}),this[d](k)},blend:function(b){if(this._rgba[3]===1)return this;var c=this._rgba.slice(),d=c.pop(),e=f(b)._rgba;return f(a.map(c,function(a,b){return(1-d)*e[b]+d*a}))},toRgbaString:function(){var b="rgba(",c=a.map(this._rgba,function(a,b){return a==null?b>2?1:0:a});return c[3]===1&&(c.pop(),b="rgb("),b+c.join()+")"},toHslaString:function(){var b="hsla(",c=a.map(this.hsla(),function(a,b){return a==null&&(a=b>2?1:0),b&&b<3&&(a=Math.round(a*100)+"%"),a});return c[3]===1&&(c.pop(),b="hsl("),b+c.join()+")"},toHexString:function(b){var c=this._rgba.slice(),d=c.pop();return b&&c.push(~~(d*255)),"#"+a.map(c,function(a){return a=(a||0).toString(16),a.length===1?"0"+a:a}).join("")},toString:function(){return this._rgba[3]===0?"transparent":this.toRgbaString()}}),f.fn.parse.prototype=f.fn,g.hsla.to=function(a){if(a[0]==null||a[1]==null||a[2]==null)return[null,null,null,a[3]];var b=a[0]/255,c=a[1]/255,d=a[2]/255,e=a[3],f=Math.max(b,c,d),g=Math.min(b,c,d),h=f-g,i=f+g,j=i*.5,k,l;return g===f?k=0:b===f?k=60*(c-d)/h+360:c===f?k=60*(d-b)/h+120:k=60*(b-c)/h+240,h===0?l=0:j<=.5?l=h/i:l=h/(2-i),[Math.round(k)%360,l,j,e==null?1:e]},g.hsla.from=function(a){if(a[0]==null||a[1]==null||a[2]==null)return[null,null,null,a[3]];var b=a[0]/360,c=a[1],d=a[2],e=a[3],f=d<=.5?d*(1+c):d+c-d*c,g=2*d-f;return[Math.round(o(g,f,b+1/3)*255),Math.round(o(g,f,b)*255),Math.round(o(g,f,b-1/3)*255),e]},l(g,function(c,e){var g=e.props,h=e.cache,i=e.to,j=e.from;f.fn[c]=function(c){i&&!this[h]&&(this[h]=i(this._rgba));if(c===b)return this[h].slice();var d,e=a.type(c),k=e==="array"||e==="object"?c:arguments,n=this[h].slice();return l(g,function(a,b){var c=k[e==="object"?a:b.idx];c==null&&(c=n[b.idx]),n[b.idx]=m(c,b)}),j?(d=f(j(n)),d[h]=n,d):f(n)},l(g,function(b,e){if(f.fn[b])return;f.fn[b]=function(f){var g=a.type(f),h=b==="alpha"?this._hsla?"hsla":"rgba":c,i=this[h](),j=i[e.idx],k;return g==="undefined"?j:(g==="function"&&(f=f.call(this,j),g=a.type(f)),f==null&&e.empty?this:(g==="string"&&(k=d.exec(f),k&&(f=j+parseFloat(k[2])*(k[1]==="+"?1:-1))),i[e.idx]=f,this[h](i)))}})}),f.hook=function(b){var c=b.split(" ");l(c,function(b,c){a.cssHooks[c]={set:function(b,d){var e,g,h="";if(d!=="transparent"&&(a.type(d)!=="string"||(e=n(d)))){d=f(e||d);if(!i.rgba&&d._rgba[3]!==1){g=c==="backgroundColor"?b.parentNode:b;while((h===""||h==="transparent")&&g&&g.style)try{h=a.css(g,"backgroundColor"),g=g.parentNode}catch(j){}d=d.blend(h&&h!=="transparent"?h:"_default")}d=d.toRgbaString()}try{b.style[c]=d}catch(j){}}},a.fx.step[c]=function(b){b.colorInit||(b.start=f(b.elem,c),b.end=f(b.end),b.colorInit=!0),a.cssHooks[c].set(b.elem,b.start.transition(b.end,b.pos))}})},f.hook(c),a.cssHooks.borderColor={expand:function(a){var b={};return l(["Top","Right","Bottom","Left"],function(c,d){b["border"+d+"Color"]=a}),b}},k=a.Color.names={aqua:"#00ffff",black:"#000000",blue:"#0000ff",fuchsia:"#ff00ff",gray:"#808080",green:"#008000",lime:"#00ff00",maroon:"#800000",navy:"#000080",olive:"#808000",purple:"#800080",red:"#ff0000",silver:"#c0c0c0",teal:"#008080",white:"#ffffff",yellow:"#ffff00",transparent:[null,null,null,0],_default:"#ffffff"}})(jQuery); -------------------------------------------------------------------------------- /app/public/scripts/main.js: -------------------------------------------------------------------------------- 1 | require.config({ 2 | paths: { 3 | io: '../socket.io/socket.io', 4 | ko: 'libs/knockout-3.2.0', 5 | moment: 'libs/moment.min', 6 | countdown: 'libs/countdown.min', 7 | cookies: 'libs/cookies.min' 8 | } 9 | }); 10 | 11 | define(['ko', 'knockoutExtensions', 'BuildMonitorServer', 'AppViewModel'], function (ko, knockoutExtensions, BuildMonitorServer, AppViewModel) { 12 | knockoutExtensions.register(); 13 | 14 | var app = new AppViewModel(); 15 | 16 | $(window).focus(function(e) { 17 | app.setHasUnreadBuilds(false); 18 | }); 19 | 20 | $(window).mousemove(function(e) { 21 | app.setHasUnreadBuilds(false); 22 | }); 23 | 24 | $(function() { 25 | ko.applyBindings(app); 26 | 27 | var buildMonitorServer = new BuildMonitorServer(); 28 | 29 | buildMonitorServer.onConnected = function() { 30 | app.setIsConnected(true); 31 | }; 32 | 33 | buildMonitorServer.onDisconnected = function() { 34 | app.setIsConnected(false); 35 | }; 36 | 37 | buildMonitorServer.onBuildsLoaded = function (builds) { 38 | app.loadBuilds(builds); 39 | app.setIsLoading(false); 40 | }; 41 | 42 | buildMonitorServer.onBuildsChanged = function (changes) { 43 | app.updateCurrentBuildsWithChanges(changes); 44 | app.setIsLoading(false); 45 | }; 46 | 47 | buildMonitorServer.onVersionChanged = function () { 48 | window.location.reload(true); 49 | }; 50 | 51 | buildMonitorServer.onSettingsLoaded = function (settings) { 52 | app.version(settings.version); 53 | }; 54 | 55 | buildMonitorServer.connect(); 56 | 57 | setInterval(app.updateBuildTimes, 1000); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /app/public/scripts/notification.js: -------------------------------------------------------------------------------- 1 | define([], function () { 2 | var notificationsAreSupported = 'Notification' in window; 3 | 4 | var permissionIsGranted = function () { 5 | return Notification.permission === 'granted'; 6 | }; 7 | 8 | var permissionIsNotDenied = function () { 9 | return Notification.permission !== 'denied'; 10 | }; 11 | 12 | var isSupportedAndNotDenied = function () { 13 | return notificationsAreSupported && permissionIsNotDenied(); 14 | }; 15 | 16 | var ensureGranted = function (granted, notAvailable) { 17 | if(!notificationsAreSupported) { 18 | notAvailable(); 19 | } else if (permissionIsGranted()) { 20 | granted(); 21 | } else if (permissionIsNotDenied()) { 22 | Notification.requestPermission(function (permission) { 23 | if (permissionIsGranted()) { 24 | granted(); 25 | } else { 26 | notAvailable(); 27 | } 28 | }); 29 | } else { 30 | notAvailable(); 31 | } 32 | }; 33 | 34 | var show = function (build) { 35 | if (permissionIsGranted()) { 36 | var notification = new Notification('Build ' + build.number + ' failed!', { 37 | body: 'The build ' + build.number + ' of project ' + build.project + ', which was triggered as ' + build.reason + ' by ' + build.requestedFor + ', failed.', 38 | icon: '/images/notification.png' 39 | }); 40 | 41 | notification.onclick = function() { 42 | window.focus(); 43 | this.cancel(); 44 | }; 45 | } 46 | }; 47 | 48 | return { 49 | isSupportedAndNotDenied: isSupportedAndNotDenied, 50 | ensureGranted: ensureGranted, 51 | show: show 52 | }; 53 | }); -------------------------------------------------------------------------------- /app/public/scripts/settings.js: -------------------------------------------------------------------------------- 1 | define(['cookies'], function (cookies) { 2 | var parseBool = function (boolAsString) { 3 | return boolAsString === 'true'; 4 | }; 5 | 6 | var settings = { }; 7 | 8 | Object.defineProperty(settings, 'theme', { 9 | get: function() { return cookies.get('theme') || 'default'; }, 10 | set: function(value) { cookies.set('theme', value); }, 11 | enumerable: true 12 | }); 13 | 14 | Object.defineProperty(settings, 'browserNotificationEnabled', { 15 | get: function() { return parseBool(cookies.get('browserNotificationEnabled')) || false; }, 16 | set: function(value) { cookies.set('browserNotificationEnabled', value); }, 17 | enumerable: true 18 | }); 19 | 20 | Object.defineProperty(settings, 'soundNotificationEnabled', { 21 | get: function() { return parseBool(cookies.get('soundNotificationEnabled')) || false; }, 22 | set: function(value) { cookies.set('soundNotificationEnabled', value); }, 23 | enumerable: true 24 | }); 25 | 26 | Object.defineProperty(settings, 'notificationFilterEnabled', { 27 | get: function() { return parseBool(cookies.get('notificationFilterEnabled')) || false; }, 28 | set: function(value) { cookies.set('notificationFilterEnabled', value); }, 29 | enumerable: true 30 | }); 31 | 32 | Object.defineProperty(settings, 'notificationFilterValue', { 33 | get: function() { return cookies.get('notificationFilterValue') || ''; }, 34 | set: function(value) { cookies.set('notificationFilterValue', value); }, 35 | enumerable: true 36 | }); 37 | 38 | return settings; 39 | }); 40 | -------------------------------------------------------------------------------- /app/public/stylesheets/base/font-awesome-4.2.0/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcells/node-build-monitor/b13c156f7ac4df2559808642c9a408e4e0357f16/app/public/stylesheets/base/font-awesome-4.2.0/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /app/public/stylesheets/base/font-awesome-4.2.0/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcells/node-build-monitor/b13c156f7ac4df2559808642c9a408e4e0357f16/app/public/stylesheets/base/font-awesome-4.2.0/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /app/public/stylesheets/base/font-awesome-4.2.0/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcells/node-build-monitor/b13c156f7ac4df2559808642c9a408e4e0357f16/app/public/stylesheets/base/font-awesome-4.2.0/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /app/public/stylesheets/base/font-awesome-4.2.0/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcells/node-build-monitor/b13c156f7ac4df2559808642c9a408e4e0357f16/app/public/stylesheets/base/font-awesome-4.2.0/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /app/public/stylesheets/base/fonts.css: -------------------------------------------------------------------------------- 1 | @import url("font-awesome-4.2.0/css/font-awesome.min.css"); 2 | @import url("opensans-condbold/css/opensans-condbold.css"); 3 | @import url("opensans-condlight/css/opensans-condlight.css"); -------------------------------------------------------------------------------- /app/public/stylesheets/base/opensans-condbold/css/opensans-condbold.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'open_sans_condensedbold'; 3 | src: url('../fonts/OpenSans-CondBold-webfont.eot'); 4 | src: url('../fonts/OpenSans-CondBold-webfont.eot?#iefix') format('embedded-opentype'), 5 | url('../fonts/OpenSans-CondBold-webfont.woff') format('woff'), 6 | url('../fonts/OpenSans-CondBold-webfont.ttf') format('truetype'), 7 | url('../fonts/OpenSans-CondBold-webfont.svg#open_sans_condensedbold') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } -------------------------------------------------------------------------------- /app/public/stylesheets/base/opensans-condbold/fonts/OpenSans-CondBold-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcells/node-build-monitor/b13c156f7ac4df2559808642c9a408e4e0357f16/app/public/stylesheets/base/opensans-condbold/fonts/OpenSans-CondBold-webfont.eot -------------------------------------------------------------------------------- /app/public/stylesheets/base/opensans-condbold/fonts/OpenSans-CondBold-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcells/node-build-monitor/b13c156f7ac4df2559808642c9a408e4e0357f16/app/public/stylesheets/base/opensans-condbold/fonts/OpenSans-CondBold-webfont.ttf -------------------------------------------------------------------------------- /app/public/stylesheets/base/opensans-condbold/fonts/OpenSans-CondBold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcells/node-build-monitor/b13c156f7ac4df2559808642c9a408e4e0357f16/app/public/stylesheets/base/opensans-condbold/fonts/OpenSans-CondBold-webfont.woff -------------------------------------------------------------------------------- /app/public/stylesheets/base/opensans-condlight/css/opensans-condlight.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'open_sanscondensed_light'; 3 | src: url('../fonts/OpenSans-CondLight-webfont.eot'); 4 | src: url('../fonts/OpenSans-CondLight-webfont.eot?#iefix') format('embedded-opentype'), 5 | url('../fonts/OpenSans-CondLight-webfont.woff') format('woff'), 6 | url('../fonts/OpenSans-CondLight-webfont.ttf') format('truetype'), 7 | url('../fonts/OpenSans-CondLight-webfont.svg#open_sanscondensed_light') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } -------------------------------------------------------------------------------- /app/public/stylesheets/base/opensans-condlight/fonts/OpenSans-CondLight-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcells/node-build-monitor/b13c156f7ac4df2559808642c9a408e4e0357f16/app/public/stylesheets/base/opensans-condlight/fonts/OpenSans-CondLight-webfont.eot -------------------------------------------------------------------------------- /app/public/stylesheets/base/opensans-condlight/fonts/OpenSans-CondLight-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcells/node-build-monitor/b13c156f7ac4df2559808642c9a408e4e0357f16/app/public/stylesheets/base/opensans-condlight/fonts/OpenSans-CondLight-webfont.ttf -------------------------------------------------------------------------------- /app/public/stylesheets/base/opensans-condlight/fonts/OpenSans-CondLight-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcells/node-build-monitor/b13c156f7ac4df2559808642c9a408e4e0357f16/app/public/stylesheets/base/opensans-condlight/fonts/OpenSans-CondLight-webfont.woff -------------------------------------------------------------------------------- /app/public/stylesheets/base/style.css: -------------------------------------------------------------------------------- 1 | @import url("fonts.css"); 2 | 3 | html { 4 | height: 100%; 5 | } 6 | 7 | body { 8 | padding: 0px; 9 | margin: 0px; 10 | font: 14px 'open_sanscondensed_light', Helvetica, Arial, sans-serif; 11 | height: 100%; 12 | } 13 | 14 | .full-size { 15 | height: 100%; 16 | width: 100%; 17 | } 18 | 19 | .carbon-background { 20 | background-image: url(../../images/pattern-b8gl.png); 21 | background-repeat: repeat; 22 | } 23 | 24 | .clear { 25 | clear: both; 26 | } 27 | 28 | /******************************* 29 | Overlay 30 | ********************************/ 31 | 32 | .connection > .overlay-background { 33 | width: 100%; 34 | height: 100%; 35 | position: fixed; 36 | top: 0px; 37 | left: 0px; 38 | background-color: black; 39 | opacity: 0.8; 40 | } 41 | 42 | .connection > .overlay-icon { 43 | width: 100%; 44 | height: 100%; 45 | position: fixed; 46 | top: 45%; 47 | left: 0px; 48 | color: white; 49 | text-align: center; 50 | } 51 | 52 | /******************************* 53 | Options 54 | ********************************/ 55 | 56 | .options-button { 57 | font-size: 40pt; 58 | top: 0px; 59 | left: 0px; 60 | width: 80px; 61 | max-height: 80px; 62 | position: fixed; 63 | background-color: white; 64 | text-align: center; 65 | vertical-align: middle; 66 | opacity: 0.5; 67 | overflow-y: auto; 68 | } 69 | 70 | .options-button > a { 71 | color: black; 72 | text-decoration: none; 73 | } 74 | 75 | .options > .background { 76 | background-color: white; 77 | opacity: 0.9; 78 | } 79 | 80 | .options > .content { 81 | color: black; 82 | overflow: auto; 83 | } 84 | 85 | .options > .background, 86 | .options > .content { 87 | position: fixed; 88 | top: 0px; 89 | left: 0px; 90 | width: 220px; 91 | overflow-y: auto; 92 | padding-left: 30px; 93 | padding-right: 30px; 94 | } 95 | 96 | .options > .background { 97 | height: 100%; 98 | } 99 | 100 | .options > .content { 101 | bottom: 30px; 102 | } 103 | 104 | .options > .footer { 105 | position: fixed; 106 | left: 0px; 107 | bottom: 0px; 108 | padding-left: 30px; 109 | padding-right: 30px; 110 | } 111 | 112 | .options > .footer a:link, 113 | .options > .footer a:visited, 114 | .options > .footer a:active, 115 | .options > .footer a:hover { 116 | color: black; 117 | } 118 | 119 | 120 | .options > .content > .header { 121 | font-size: 40pt; 122 | } 123 | 124 | .options > .content a:link, 125 | .options > .content a:visited, 126 | .options > .content a:active, 127 | .options > .content a:hover { 128 | color: black; 129 | } 130 | 131 | .options > .content > .header a:link, 132 | .options > .content > .header a:visited, 133 | .options > .content > .header a:active, 134 | .options > .content > .header a:hover { 135 | text-decoration: none; 136 | color: black; 137 | } 138 | 139 | .options > .content > .header a:active, 140 | .options > .content > .header a:hover { 141 | color: #303030; 142 | } 143 | 144 | .options > .content > .header > .back { 145 | float: right; 146 | } 147 | 148 | .options > .content section { 149 | padding-top: 12px; 150 | } 151 | 152 | .options > .content section > h1 { 153 | margin-bottom: 5px; 154 | } 155 | 156 | .options > .content section > div.description { 157 | padding-bottom: 8px; 158 | } 159 | 160 | .options > .content .checkbox { 161 | display: block; 162 | } 163 | 164 | .options > .content .selection-box ul { 165 | } 166 | 167 | .options > .content .selection-box ul > li { 168 | float: left; 169 | display: table; 170 | list-style: none; 171 | margin: 5px; 172 | } 173 | 174 | .options > .content .selection-box ul > li > a { 175 | display: table-cell; 176 | min-width: 80px; 177 | height: 50px; 178 | color: black; 179 | text-decoration: none; 180 | cursor: pointer; 181 | text-align: center; 182 | vertical-align: middle; 183 | border: 2px solid black; 184 | } 185 | 186 | .options > .content .selection-box ul > li > a:hover { 187 | background-color: lightgray; 188 | color: black; 189 | } 190 | 191 | .options > .content .selection-box ul > li.selected > a { 192 | background-color: #303030; 193 | color: white; 194 | } 195 | -------------------------------------------------------------------------------- /app/public/stylesheets/themes/default/style.css: -------------------------------------------------------------------------------- 1 | .default-theme .clear { 2 | clear: both; 3 | } 4 | 5 | .default-theme .nowrap { 6 | overflow: hidden; 7 | white-space: nowrap; 8 | } 9 | 10 | .default-theme .box { 11 | border: 1px solid black; 12 | } 13 | 14 | .default-theme .middle { 15 | vertical-align: middle; 16 | } 17 | 18 | .default-theme .medium-height { 19 | line-height: 40px; 20 | height: 40px; 21 | } 22 | 23 | .default-theme .reset-height { 24 | display: inline-block; 25 | line-height: normal; 26 | } 27 | 28 | .default-theme .flex-container { 29 | padding: 0px; 30 | margin: 0 auto 0 auto; 31 | height: 100%; 32 | width: 100%; 33 | } 34 | 35 | .default-theme .flex-item { 36 | float: left; 37 | width: 33%; 38 | height: 25%; 39 | color: transparent; 40 | position: relative; 41 | } 42 | 43 | .default-theme .flex-item-inner { 44 | background: transparent; 45 | margin: 25px; 46 | padding: 25px; 47 | box-shadow: 0px 0px 25px 8px rgba(0,0,0,0.8); 48 | } 49 | 50 | .flex-item .overlay { 51 | background-color: transparent; 52 | position: absolute; 53 | margin: 25px; 54 | top: 0px; 55 | right: 0px; 56 | } 57 | 58 | .flex-item .overlay a { 59 | float: left; 60 | color: white; 61 | font-size: 20pt; 62 | background-color: black; 63 | display: block; 64 | width: 50px; 65 | height: 50px; 66 | line-height: 50px; 67 | text-align: center; 68 | vertical-align: middle; 69 | opacity: 0.8; 70 | } 71 | 72 | .default-theme .flex-item .info-block { 73 | float: left; 74 | font-size: 15pt; 75 | } 76 | 77 | .default-theme .flex-item .project { 78 | font-weight: bold; 79 | } 80 | 81 | .default-theme .flex-item .project-definition-separator { 82 | border-left: 1px solid; 83 | padding-left: 10px; 84 | margin-left: 10px 85 | } 86 | 87 | .default-theme .flex-item .definition { 88 | } 89 | 90 | .default-theme .flex-item .number { 91 | font-size: 25pt; 92 | } 93 | 94 | .default-theme .flex-item .duration-block { 95 | float: right; 96 | font-size: 10pt; 97 | } 98 | 99 | .default-theme .flex-item .duration-icon { 100 | font-size: 35px; 101 | float: left; 102 | margin-top: 3px; 103 | } 104 | 105 | .default-theme .flex-item .time { 106 | margin-left: 10px; 107 | } 108 | 109 | .default-theme .flex-item .status { 110 | float: left; 111 | font-family: 'open_sans_condensedbold'; 112 | font-weight: bold; 113 | font-size: 25pt; 114 | } 115 | 116 | .default-theme .flex-item .reason { 117 | margin-left: 10px; 118 | font-size: 15pt; 119 | } 120 | 121 | .default-theme .flex-item .reason-icon { 122 | font-size: 25px; 123 | } 124 | 125 | .default-theme .flex-item .reason-block { 126 | float: right; 127 | } 128 | 129 | .default-theme .flex-item .for { 130 | margin-left: 15px; 131 | font-size: 20px; 132 | } 133 | 134 | .default-theme .requestedFor-block { 135 | font-size: 15pt; 136 | float: left; 137 | vertical-align: middle; 138 | } 139 | 140 | .default-theme .flex-item .warnings { 141 | font-weight: bold; 142 | font-size: 20pt; 143 | float: right; 144 | vertical-align: middle; 145 | } 146 | 147 | @media (max-height: 600px) { 148 | .default-theme .prio-vertical-one { 149 | display: none; 150 | } 151 | } 152 | 153 | @media (max-height: 760px) { 154 | .default-theme .prio-vertical-two { 155 | display: none; 156 | } 157 | } 158 | 159 | @media (max-height: 900px) { 160 | .default-theme .prio-vertical-three { 161 | display: none; 162 | } 163 | } 164 | 165 | @media (max-width: 870px) { 166 | .default-theme .prio-horizontal-one { 167 | display: none; 168 | } 169 | } 170 | 171 | @media (max-width: 1000px) { 172 | .default-theme .prio-horizontal-two { 173 | display: none; 174 | } 175 | } 176 | 177 | @media (max-width: 1300px) { 178 | .default-theme .prio-horizontal-three { 179 | display: none; 180 | } 181 | } -------------------------------------------------------------------------------- /app/public/stylesheets/themes/lingo/marck-script/css/marck-script.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Marck-Script'; 3 | src: url('../fonts/Marck-Script.ttf.eot'); 4 | src: url('../fonts/Marck-Script.ttf.eot?#iefix') format('embedded-opentype'), 5 | url('../fonts/Marck-Script.ttf.woff') format('woff'), 6 | url('../fonts/Marck-Script.ttf') format('truetype'), 7 | url('../fonts/Marck-Script.ttf.svg#Marck-Script') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } -------------------------------------------------------------------------------- /app/public/stylesheets/themes/lingo/marck-script/fonts/Marck-Script.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcells/node-build-monitor/b13c156f7ac4df2559808642c9a408e4e0357f16/app/public/stylesheets/themes/lingo/marck-script/fonts/Marck-Script.ttf -------------------------------------------------------------------------------- /app/public/stylesheets/themes/lingo/marck-script/fonts/Marck-Script.ttf.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcells/node-build-monitor/b13c156f7ac4df2559808642c9a408e4e0357f16/app/public/stylesheets/themes/lingo/marck-script/fonts/Marck-Script.ttf.eot -------------------------------------------------------------------------------- /app/public/stylesheets/themes/lingo/marck-script/fonts/Marck-Script.ttf.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcells/node-build-monitor/b13c156f7ac4df2559808642c9a408e4e0357f16/app/public/stylesheets/themes/lingo/marck-script/fonts/Marck-Script.ttf.woff -------------------------------------------------------------------------------- /app/public/stylesheets/themes/lingo/style.css: -------------------------------------------------------------------------------- 1 | @import url("marck-script/css/marck-script.css"); 2 | 3 | .lingo-theme .list-container { 4 | font-family: 'Marck-Script'; 5 | font-size: 20pt; 6 | padding: 30px; 7 | color: white; 8 | } 9 | 10 | .lingo-theme .list-item { 11 | margin-top: 15px; 12 | padding: 10px; 13 | } 14 | 15 | .lingo-theme .emphasize { 16 | color: #f7ffad; 17 | } -------------------------------------------------------------------------------- /app/public/stylesheets/themes/list/style.css: -------------------------------------------------------------------------------- 1 | .list-theme .list-container { 2 | font-size: 20pt; 3 | padding: 30px; 4 | color: white; 5 | } 6 | 7 | .list-theme .list-item { 8 | margin-top: 15px; 9 | padding: 10px; 10 | } 11 | 12 | .list-theme .clear { 13 | clear: both; 14 | } 15 | 16 | @media (min-width: 1400px) { 17 | .list-theme .list-item { 18 | right: 0; 19 | text-align: center; 20 | } 21 | 22 | .list-theme .part-1, .list-theme .part-2, .list-theme .part-3 { 23 | display: inline-block; 24 | white-space: nowrap; 25 | overflow: hidden; 26 | text-overflow: ellipsis; 27 | } 28 | 29 | .list-theme .part-1 { 30 | text-align: left; 31 | float: left; 32 | width: 35%; 33 | } 34 | 35 | .list-theme .part-2 { 36 | display: inline-block; 37 | text-align: center; 38 | margin: 0 auto; 39 | width: 30%; 40 | } 41 | 42 | .list-theme .part-3 { 43 | text-align: right; 44 | float: right; 45 | width: 35%; 46 | } 47 | } -------------------------------------------------------------------------------- /app/public/stylesheets/themes/lowres/style.css: -------------------------------------------------------------------------------- 1 | .lowres .clear { 2 | clear: both; 3 | } 4 | 5 | .lowres .nowrap { 6 | overflow: hidden; 7 | white-space: nowrap; 8 | } 9 | 10 | .lowres .box { 11 | border: 1px solid black; 12 | } 13 | 14 | .lowres .middle { 15 | vertical-align: middle; 16 | } 17 | 18 | .lowres .medium-height { 19 | line-height: 40px; 20 | height: 40px; 21 | } 22 | 23 | .lowres .reset-height { 24 | display: inline-block; 25 | line-height: normal; 26 | } 27 | 28 | .lowres .flex-container { 29 | padding: 0px; 30 | margin: 0 auto 0 auto; 31 | height: 100%; 32 | width: 100%; 33 | } 34 | 35 | .lowres .flex-item { 36 | float: left; 37 | width: 25%; 38 | height: 33%; 39 | color: transparent; 40 | position: relative; 41 | } 42 | 43 | .lowres .flex-item-inner { 44 | background: transparent; 45 | margin: 25px; 46 | padding: 25px; 47 | box-shadow: 0px 0px 25px 8px rgba(0,0,0,0.8); 48 | } 49 | 50 | .flex-item .overlay { 51 | background-color: transparent; 52 | position: absolute; 53 | margin: 25px; 54 | top: 0px; 55 | right: 0px; 56 | } 57 | 58 | .flex-item .overlay a { 59 | float: left; 60 | color: white; 61 | font-size: 20pt; 62 | background-color: black; 63 | display: block; 64 | width: 50px; 65 | height: 50px; 66 | line-height: 50px; 67 | text-align: center; 68 | vertical-align: middle; 69 | opacity: 0.8; 70 | } 71 | 72 | .lowres .flex-item .info-block { 73 | float: left; 74 | font-size: 15pt; 75 | } 76 | 77 | .lowres .flex-item .project { 78 | font-weight: bold; 79 | } 80 | 81 | .lowres .flex-item .project-definition-separator { 82 | border-left: 1px solid; 83 | padding-left: 10px; 84 | margin-left: 10px 85 | } 86 | 87 | .lowres .flex-item .definition { 88 | } 89 | 90 | .lowres .flex-item .number { 91 | font-size: 25pt; 92 | } 93 | 94 | .lowres .flex-item .duration-block { 95 | float: right; 96 | font-size: 10pt; 97 | } 98 | 99 | .lowres .flex-item .duration-icon { 100 | font-size: 35px; 101 | float: left; 102 | margin-top: 3px; 103 | } 104 | 105 | .lowres .flex-item .time { 106 | margin-left: 10px; 107 | } 108 | 109 | .lowres .flex-item .status { 110 | float: left; 111 | font-family: 'open_sans_condensedbold'; 112 | font-weight: bold; 113 | font-size: 25pt; 114 | } 115 | 116 | .lowres .flex-item .reason { 117 | margin-left: 10px; 118 | font-size: 15pt; 119 | } 120 | 121 | .lowres .flex-item .reason-icon { 122 | font-size: 25px; 123 | } 124 | 125 | .lowres .flex-item .reason-block { 126 | float: right; 127 | } 128 | 129 | .lowres .flex-item .for { 130 | margin-left: 15px; 131 | font-size: 20px; 132 | } 133 | 134 | .lowres .requestedFor-block { 135 | font-size: 15pt; 136 | float: left; 137 | vertical-align: middle; 138 | } 139 | 140 | .lowres .flex-item .warnings { 141 | font-weight: bold; 142 | font-size: 20pt; 143 | float: right; 144 | vertical-align: middle; 145 | } 146 | 147 | @media (max-height: 600px) { 148 | .lowres .prio-vertical-two { 149 | display: none; 150 | } 151 | } 152 | 153 | @media (max-height: 760px) { 154 | .lowres .prio-vertical-three { 155 | display: none; 156 | } 157 | } 158 | 159 | @media (max-width: 870px) { 160 | .lowres .prio-horizontal-one { 161 | display: none; 162 | } 163 | } 164 | 165 | @media (max-width: 1000px) { 166 | .lowres .prio-horizontal-two { 167 | display: none; 168 | } 169 | } 170 | 171 | @media (max-width: 1300px) { 172 | .lowres .prio-horizontal-three { 173 | display: none; 174 | } 175 | } -------------------------------------------------------------------------------- /app/public/stylesheets/themes/minimal/style.css: -------------------------------------------------------------------------------- 1 | #pluginWrap { 2 | display: flex; 3 | height: 100vh; 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | } 8 | .default-theme a { 9 | text-decoration: none; 10 | } 11 | 12 | .default-theme .full-size { 13 | 14 | } 15 | 16 | .default-theme .clear { 17 | clear: both; 18 | } 19 | 20 | .default-theme .nowrap { 21 | overflow: hidden; 22 | white-space: nowrap; 23 | } 24 | 25 | .default-theme .box { 26 | border: 1px solid black; 27 | } 28 | 29 | .default-theme .middle { 30 | vertical-align: middle; 31 | } 32 | 33 | .default-theme .medium-height { 34 | line-height: 40px; 35 | height: 40px; 36 | } 37 | 38 | .default-theme .reset-height { 39 | display: inline-block; 40 | line-height: normal; 41 | } 42 | 43 | .default-theme .flex-container { 44 | padding: 0px; 45 | margin: 0 auto 0 auto; 46 | height: 100%; 47 | width: 100%; 48 | align-items: center; 49 | justify-content: center; 50 | 51 | } 52 | 53 | .default-theme .flex-item { 54 | float: left; 55 | width: 25%; 56 | height: auto; 57 | color: transparent; 58 | position: relative; 59 | } 60 | 61 | .default-theme .flex-item-inner { 62 | background: transparent; 63 | margin: 15px; 64 | padding: 15px; 65 | box-shadow: 0px 0px 25px 8px rgba(0, 0, 0, 0.8); 66 | } 67 | 68 | .flex-item .overlay { 69 | background-color: transparent; 70 | position: absolute; 71 | margin: 25px; 72 | top: 0px; 73 | right: 0px; 74 | } 75 | 76 | .flex-item .overlay a { 77 | float: left; 78 | color: white; 79 | font-size: 20pt; 80 | background-color: black; 81 | display: block; 82 | width: 50px; 83 | height: 50px; 84 | line-height: 50px; 85 | text-align: center; 86 | vertical-align: middle; 87 | opacity: 0.8; 88 | } 89 | 90 | .default-theme .flex-item .info-block { 91 | float: left; 92 | font-size: 15pt; 93 | } 94 | 95 | .default-theme .flex-item .project { 96 | font-weight: bold; 97 | } 98 | 99 | .default-theme .flex-item .project-definition-separator { 100 | border-left: 1px solid; 101 | padding-left: 10px; 102 | margin-left: 10px; 103 | } 104 | 105 | .default-theme .flex-item .projectName { 106 | text-align: center; 107 | font-size: 25pt; 108 | } 109 | 110 | .default-theme .flex-item .duration-block { 111 | float: right; 112 | font-size: 10pt; 113 | } 114 | 115 | .default-theme .flex-item .duration-icon { 116 | font-size: 35px; 117 | float: left; 118 | margin-top: 3px; 119 | } 120 | 121 | .default-theme .flex-item .time { 122 | margin-left: 10px; 123 | } 124 | 125 | .default-theme .flex-item .status { 126 | float: left; 127 | font-family: "open_sans_condensedbold"; 128 | font-weight: bold; 129 | font-size: 25pt; 130 | } 131 | 132 | .default-theme .flex-item .reason { 133 | margin-left: 10px; 134 | font-size: 15pt; 135 | } 136 | 137 | .default-theme .flex-item .reason-icon { 138 | font-size: 25px; 139 | } 140 | 141 | .default-theme .flex-item .reason-block { 142 | float: right; 143 | } 144 | 145 | .default-theme .flex-item .for { 146 | margin-left: 15px; 147 | font-size: 20px; 148 | } 149 | 150 | .default-theme .requestedFor-block { 151 | font-size: 15pt; 152 | float: right; 153 | vertical-align: middle; 154 | } 155 | 156 | .default-theme .flex-item .warnings { 157 | font-weight: bold; 158 | font-size: 20pt; 159 | float: right; 160 | vertical-align: middle; 161 | } 162 | -------------------------------------------------------------------------------- /app/public/stylesheets/themes/stoplight/style.css: -------------------------------------------------------------------------------- 1 | .stoplight .clear { 2 | clear: both; 3 | } 4 | 5 | .stoplight .nowrap { 6 | overflow: hidden; 7 | white-space: nowrap; 8 | } 9 | 10 | .stoplight .box { 11 | border: 1px solid black; 12 | } 13 | 14 | .stoplight .middle { 15 | vertical-align: middle; 16 | } 17 | 18 | .stoplight .medium-height { 19 | line-height: 40px; 20 | height: 40px; 21 | } 22 | 23 | .stoplight .reset-height { 24 | display: inline-block; 25 | line-height: normal; 26 | } 27 | 28 | .stoplight .flex-container { 29 | padding: 0px; 30 | margin: 0 auto 0 auto; 31 | height: 100%; 32 | width: 100%; 33 | } 34 | 35 | .stoplight .flex-item { 36 | float: left; 37 | width: 33%; 38 | color: transparent; 39 | position: relative; 40 | } 41 | 42 | .stoplight .flex-item-inner { 43 | background: transparent; 44 | margin: 20px; 45 | padding: 20px; 46 | box-shadow: 0px 0px 25px 8px rgba(0,0,0,0.8); 47 | text-align: center; 48 | } 49 | 50 | .flex-item .overlay { 51 | background-color: transparent; 52 | position: absolute; 53 | margin: 20px; 54 | top: 0px; 55 | right: 0px; 56 | } 57 | 58 | .flex-item .overlay a { 59 | float: left; 60 | color: white; 61 | font-size: 20pt; 62 | background-color: black; 63 | display: block; 64 | width: 50px; 65 | height: 50px; 66 | line-height: 50px; 67 | text-align: center; 68 | vertical-align: middle; 69 | opacity: 0.8; 70 | } 71 | 72 | .stoplight .flex-item .info-block { 73 | float: left; 74 | font-size: 15pt; 75 | } 76 | 77 | .stoplight .flex-item .project { 78 | color: #DDDDDD; 79 | font-size: 16pt; 80 | font-weight: normal; 81 | } 82 | 83 | .stoplight .flex-item .project-definition-separator { 84 | border-left: 1px solid; 85 | padding-left: 10px; 86 | margin-left: 10px 87 | } 88 | 89 | .stoplight .flex-item .definition { 90 | color: #FFFFFF; 91 | font-family: 'open_sans_condensedbold'; 92 | font-size: 22pt; 93 | line-height: 34pt; 94 | } 95 | 96 | .stoplight .flex-item .number { 97 | color: #DDDDDD; 98 | font-size: 16pt; 99 | line-height: 28pt; 100 | } 101 | 102 | .stoplight .flex-item .duration-block { 103 | color: #999999; 104 | float: right; 105 | font-size: 10pt; 106 | } 107 | 108 | .stoplight .flex-item .duration-icon { 109 | display: none; 110 | } 111 | 112 | .stoplight .flex-item .time { 113 | color: #AAAAAA; 114 | margin-left: 10px; 115 | } 116 | 117 | .stoplight .flex-item .status { 118 | color: #DDDDDD; 119 | float: left; 120 | font-family: Helvetica, Arial, sans-serif; 121 | font-size: 14pt; 122 | } 123 | 124 | .stoplight .flex-item .reason { 125 | display: none; 126 | } 127 | 128 | .stoplight .flex-item .reason-icon { 129 | font-size: 25px; 130 | } 131 | 132 | .stoplight .flex-item .reason-block { 133 | float: right; 134 | } 135 | 136 | .stoplight .flex-item .for { 137 | margin-left: 15px; 138 | font-size: 20px; 139 | } 140 | 141 | .stoplight .requestedFor-block { 142 | font-size: 15pt; 143 | float: left; 144 | vertical-align: middle; 145 | } 146 | 147 | .stoplight .flex-item .warnings { 148 | font-weight: bold; 149 | font-size: 20pt; 150 | float: right; 151 | vertical-align: middle; 152 | } 153 | 154 | .stoplight .prio-vertical-one { 155 | margin-top: 15px; 156 | } 157 | 158 | @media (max-height: 600px) { 159 | .stoplight .prio-vertical-one { 160 | display: none; 161 | } 162 | } 163 | 164 | @media (max-height: 760px) { 165 | .stoplight .prio-vertical-two { 166 | display: none; 167 | } 168 | } 169 | 170 | @media (max-height: 900px) { 171 | .stoplight .prio-vertical-three { 172 | display: none; 173 | } 174 | } 175 | 176 | @media (max-width: 870px) { 177 | .stoplight .prio-horizontal-one { 178 | display: none; 179 | } 180 | } 181 | 182 | @media (max-width: 1000px) { 183 | .stoplight .prio-horizontal-two { 184 | display: none; 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /app/public/stylesheets/themes/twenty/style.css: -------------------------------------------------------------------------------- 1 | .twenty-theme .clear { 2 | clear: both; 3 | } 4 | 5 | .twenty-theme .nowrap { 6 | overflow: hidden; 7 | white-space: nowrap; 8 | } 9 | 10 | .twenty-theme .box { 11 | border: 1px solid black; 12 | } 13 | 14 | .twenty-theme .middle { 15 | vertical-align: middle; 16 | } 17 | 18 | .twenty-theme .medium-height { 19 | line-height: 40px; 20 | height: 40px; 21 | } 22 | 23 | .twenty-theme .reset-height { 24 | display: inline-block; 25 | line-height: normal; 26 | } 27 | 28 | .twenty-theme .flex-container { 29 | padding: 0px; 30 | margin: 0 auto 0 auto; 31 | height: 100%; 32 | width: 100%; 33 | } 34 | 35 | .twenty-theme .flex-item { 36 | float: left; 37 | width: 25%; 38 | height: 20%; 39 | color: transparent; 40 | position: relative; 41 | } 42 | 43 | .twenty-theme .flex-item-inner { 44 | background: transparent; 45 | margin: 15px; 46 | padding: 10px; 47 | box-shadow: 0px 0px 25px 8px rgba(0,0,0,0.8); 48 | } 49 | 50 | .flex-item .overlay { 51 | background-color: transparent; 52 | position: absolute; 53 | margin: 25px; 54 | top: 0px; 55 | right: 0px; 56 | } 57 | 58 | .flex-item .overlay a { 59 | float: left; 60 | color: white; 61 | font-size: 20pt; 62 | background-color: black; 63 | display: block; 64 | width: 50px; 65 | height: 50px; 66 | line-height: 50px; 67 | text-align: center; 68 | vertical-align: middle; 69 | opacity: 0.8; 70 | } 71 | 72 | .twenty-theme .flex-item .info-block { 73 | float: left; 74 | font-size: 15pt; 75 | } 76 | 77 | .twenty-theme .flex-item .project { 78 | font-weight: bold; 79 | } 80 | 81 | .twenty-theme .flex-item .project-definition-separator { 82 | border-left: 1px solid; 83 | padding-left: 10px; 84 | margin-left: 10px 85 | } 86 | 87 | .twenty-theme .flex-item .definition { 88 | } 89 | 90 | .twenty-theme .flex-item .number { 91 | font-size: 20pt; 92 | } 93 | 94 | .twenty-theme .flex-item .duration-block { 95 | float: right; 96 | font-size: 10pt; 97 | } 98 | 99 | .twenty-theme .flex-item .duration-icon { 100 | font-size: 35px; 101 | float: left; 102 | margin-top: 3px; 103 | } 104 | 105 | .twenty-theme .flex-item .time { 106 | margin-left: 10px; 107 | } 108 | 109 | .twenty-theme .flex-item .status { 110 | float: left; 111 | font-family: 'open_sans_condensedbold'; 112 | font-weight: bold; 113 | font-size: 25pt; 114 | } 115 | 116 | .twenty-theme .flex-item .reason { 117 | margin-left: 10px; 118 | font-size: 15pt; 119 | } 120 | 121 | .twenty-theme .flex-item .reason-icon { 122 | font-size: 25px; 123 | } 124 | 125 | .twenty-theme .flex-item .reason-block { 126 | float: right; 127 | } 128 | 129 | .twenty-theme .flex-item .for { 130 | margin-left: 15px; 131 | font-size: 20px; 132 | } 133 | 134 | .twenty-theme .requestedFor-block { 135 | font-size: 15pt; 136 | float: left; 137 | vertical-align: middle; 138 | } 139 | 140 | .twenty-theme .flex-item .warnings { 141 | font-weight: bold; 142 | font-size: 20pt; 143 | float: right; 144 | vertical-align: middle; 145 | } 146 | 147 | @media (max-height: 600px) { 148 | .twenty-theme .prio-vertical-one { 149 | display: none; 150 | } 151 | } 152 | 153 | @media (max-height: 760px) { 154 | .twenty-theme .prio-vertical-two { 155 | display: none; 156 | } 157 | } 158 | 159 | @media (max-height: 900px) { 160 | .twenty-theme .prio-vertical-three { 161 | display: none; 162 | } 163 | } 164 | 165 | @media (max-width: 870px) { 166 | .twenty-theme .prio-horizontal-one { 167 | display: none; 168 | } 169 | } 170 | 171 | @media (max-width: 1000px) { 172 | .twenty-theme .prio-horizontal-two { 173 | display: none; 174 | } 175 | } 176 | 177 | @media (max-width: 1300px) { 178 | .twenty-theme .prio-horizontal-three { 179 | display: none; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /app/public/stylesheets/themes/twentyfive/style.css: -------------------------------------------------------------------------------- 1 | .twentyfive-theme .clear { 2 | clear: both; 3 | } 4 | 5 | .twentyfive-theme .nowrap { 6 | overflow: hidden; 7 | white-space: nowrap; 8 | } 9 | 10 | .twentyfive-theme .box { 11 | border: 1px solid black; 12 | } 13 | 14 | .twentyfive-theme .middle { 15 | vertical-align: middle; 16 | } 17 | 18 | .twentyfive-theme .medium-height { 19 | line-height: 40px; 20 | height: 40px; 21 | } 22 | 23 | .twentyfive-theme .reset-height { 24 | display: inline-block; 25 | line-height: normal; 26 | } 27 | 28 | .twentyfive-theme .flex-container { 29 | padding: 0px; 30 | margin: 0 auto 0 auto; 31 | height: 100%; 32 | width: 100%; 33 | } 34 | 35 | .twentyfive-theme .flex-item { 36 | float: left; 37 | width: 20%; 38 | height: 20%; 39 | color: transparent; 40 | position: relative; 41 | } 42 | 43 | .twentyfive-theme .flex-item-inner { 44 | background: transparent; 45 | margin: 15px; 46 | padding: 10px; 47 | box-shadow: 0px 0px 25px 8px rgba(0,0,0,0.8); 48 | } 49 | 50 | .twentyfive-theme .flex-item .overlay { 51 | background-color: transparent; 52 | position: absolute; 53 | margin: 25px; 54 | top: 0px; 55 | right: 0px; 56 | } 57 | 58 | .twentyfive-theme .flex-item .overlay a { 59 | float: left; 60 | color: white; 61 | font-size: 20pt; 62 | background-color: black; 63 | display: block; 64 | width: 50px; 65 | height: 50px; 66 | line-height: 50px; 67 | text-align: center; 68 | vertical-align: middle; 69 | opacity: 0.8; 70 | } 71 | 72 | .twentyfive-theme .flex-item .info-block { 73 | float: left; 74 | font-size: 15pt; 75 | } 76 | 77 | .twentyfive-theme .flex-item .project { 78 | font-weight: bold; 79 | } 80 | 81 | .twentyfive-theme .flex-item .project-definition-separator { 82 | border-left: 1px solid; 83 | padding-left: 10px; 84 | margin-left: 10px 85 | } 86 | 87 | .twentyfive-theme .flex-item .definition { 88 | } 89 | 90 | .twentyfive-theme .flex-item .number { 91 | font-size: 20pt; 92 | } 93 | 94 | .twentyfive-theme .flex-item .duration-block { 95 | float: right; 96 | font-size: 10pt; 97 | } 98 | 99 | .twentyfive-theme .flex-item .duration-icon { 100 | font-size: 35px; 101 | float: left; 102 | margin-top: 3px; 103 | } 104 | 105 | .twentyfive-theme .flex-item .time { 106 | margin-left: 10px; 107 | } 108 | 109 | .twentyfive-theme .flex-item .status { 110 | float: left; 111 | font-family: 'open_sans_condensedbold'; 112 | font-weight: bold; 113 | font-size: 25pt; 114 | } 115 | 116 | .twentyfive-theme .flex-item .reason { 117 | margin-left: 10px; 118 | font-size: 15pt; 119 | } 120 | 121 | .twentyfive-theme .flex-item .reason-icon { 122 | font-size: 25px; 123 | } 124 | 125 | .twentyfive-theme .flex-item .reason-block { 126 | float: right; 127 | } 128 | 129 | .twentyfive-theme .flex-item .for { 130 | margin-left: 15px; 131 | font-size: 20px; 132 | } 133 | 134 | .twentyfive-theme .requestedFor-block { 135 | font-size: 15pt; 136 | float: left; 137 | vertical-align: middle; 138 | } 139 | 140 | .twentyfive-theme .flex-item .warnings { 141 | font-weight: bold; 142 | font-size: 20pt; 143 | float: right; 144 | vertical-align: middle; 145 | } 146 | 147 | @media (max-height: 600px) { 148 | .twentyfive-theme .prio-vertical-one { 149 | display: none; 150 | } 151 | } 152 | 153 | @media (max-height: 760px) { 154 | .twentyfive-theme .prio-vertical-two { 155 | display: none; 156 | } 157 | } 158 | 159 | @media (max-height: 900px) { 160 | .twentyfive-theme .prio-vertical-three { 161 | display: none; 162 | } 163 | } 164 | 165 | @media (max-width: 870px) { 166 | .twentyfive-theme .prio-horizontal-one { 167 | display: none; 168 | } 169 | } 170 | 171 | @media (max-width: 1000px) { 172 | .twentyfive-theme .prio-horizontal-two { 173 | display: none; 174 | } 175 | } 176 | 177 | @media (max-width: 1300px) { 178 | .twentyfive-theme .prio-horizontal-three { 179 | display: none; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /app/public/templates/themes/default.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 |
7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |
19 | 20 | 21 | 22 | 23 |
24 | 25 |
26 |
27 |
28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 |
39 |
40 | 41 | 46 |
47 |
48 |
-------------------------------------------------------------------------------- /app/public/templates/themes/lingo.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | The build 5 | 6 | 7 | 8 | of 9 | 10 | as part 11 | 12 | 13 | of project 14 | 15 | 16 | was triggered as 17 | 18 | 19 | by 20 | , 21 | 22 | 23 | and 24 | 25 | 26 | with the status 27 | . 28 | 29 | 30 | and some 31 | warnings 32 | or 33 | errors. 34 | 35 |
36 |
37 |
-------------------------------------------------------------------------------- /app/public/templates/themes/list.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 | | 7 | 8 | 9 | | 10 | 11 | 12 | | 13 | 14 |
15 |
16 | 17 | 18 | 19 | | 20 | 21 | 22 | 23 |
24 |
25 | 26 | 27 | 28 | and 29 | 30 | 31 | | 32 | 33 | 34 | 35 | 36 |
37 |
38 |
39 |
40 |
-------------------------------------------------------------------------------- /app/public/templates/themes/lowres.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 |
7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 |
30 |
31 |
32 |
33 | 34 | 35 | 36 | 37 | 38 |
39 |
40 |
41 | 42 | 47 |
48 |
49 |
-------------------------------------------------------------------------------- /app/public/templates/themes/minimal.html: -------------------------------------------------------------------------------- 1 | 31 | -------------------------------------------------------------------------------- /app/public/templates/themes/stoplight.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 |
7 | 8 |
9 | 10 |
11 |
12 | 13 | 14 | 15 |
16 | 17 |
18 |
19 |
20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 |
31 |
32 | 33 | 38 |
39 |
40 |
41 | -------------------------------------------------------------------------------- /app/public/templates/themes/twenty.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 |
7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |
19 |
20 | 21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 | 36 |
37 |
38 |
39 |
40 | 41 | 46 |
47 |
48 |
49 | -------------------------------------------------------------------------------- /app/public/templates/themes/twentyfive.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 | 7 | 8 | 9 | 10 |
11 | 12 |
13 |
14 |
15 |
16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 |
24 | 25 |
26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 |
39 |
40 | 41 | 46 |
47 |
48 |
49 | -------------------------------------------------------------------------------- /app/requests.js: -------------------------------------------------------------------------------- 1 | var request = require('request'), 2 | ntlm = require('httpntlm'); 3 | http = require('http'); 4 | 5 | module.exports = { 6 | makeRequest: function (opts, callback) { 7 | if (opts.authentication && opts.authentication.trim() === 'ntlm') { 8 | ntlm.get({ 9 | url: opts.url, 10 | username: opts.username, 11 | password: opts.password, 12 | headers: opts.headers || {} 13 | }, function (error, response) { 14 | callback(error, JSON.parse(response.body)); 15 | }); 16 | } else { 17 | request({ 18 | url: opts.url, 19 | rejectUnauthorized: false, // Don't validate SSL certs 20 | headers: opts.headers || {}, 21 | json: true, 22 | agent: false, 23 | timeout: 0, // avoid timeouts 24 | agentOptions: { 25 | keepAlive: false, // "http.Agent: idle sockets throw unhandled ECONNRESET" 26 | maxSockets: 200 // Infinity switches globalAgent on. 27 | } 28 | }, 29 | function (error, response, body) { 30 | try { 31 | if (response != undefined && response.statusCode === 200) { 32 | callback(error, body); 33 | } else { 34 | if (response != undefined) 35 | { 36 | let httpErrRes = 'HTTP Reponse: '+response.statusCode+' '+http.STATUS_CODES[response.statusCode]; 37 | if (error) { 38 | error.message += ' ('+httpErrRes+')'; 39 | callback(error); 40 | } else { 41 | // If the request never reached the server, then chances are the error object is null, so lets return a status code error instead 42 | callback(new Error(httpErrRes), body); 43 | } 44 | } 45 | else{ 46 | callback(error, body); 47 | } 48 | } 49 | } 50 | catch (err) // some exception cannot be handled, like ECONNRESET 51 | { 52 | callback(error, body); 53 | } 54 | }); 55 | } 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /app/services/Bamboo.js: -------------------------------------------------------------------------------- 1 | var request = require('request'), 2 | async = require('async'), 3 | striptags = require('striptags'); 4 | 5 | module.exports = function () { 6 | var self = this, 7 | queryBuilds = function (callback) { 8 | requestBuilds(function (error, body) { 9 | if (error) { 10 | callback(error); 11 | return; 12 | } 13 | 14 | async.map(body, requestBuild, function (error, results) { 15 | callback(error, results); 16 | }); 17 | }); 18 | }, 19 | getUrlParams = function() { 20 | var params = { 21 | "os_authType": "basic" 22 | }; 23 | 24 | if(self.configuration.includeAllStates && 25 | self.configuration.includeAllStates.toString().toLowerCase() === "true") { 26 | params.includeAllStates = "1"; 27 | } 28 | 29 | if (self.configuration.latestBuildPerBuildPlanOnly === true) { 30 | params["max-results"] = "1"; 31 | } 32 | 33 | return params; 34 | }, 35 | requestBuilds = function (callback) { 36 | var planUri = self.configuration.url + "/rest/api/latest/result/" + self.configuration.planKey + ".json"; 37 | var urlParams = getUrlParams(); 38 | 39 | request({ uri: planUri, qs: urlParams }, function(error, response, body) { 40 | try { 41 | var bodyJson = JSON.parse(body); 42 | callback(error, bodyJson.results.result); 43 | } catch (parseError) { 44 | callback(parseError, null); 45 | } 46 | }); 47 | }, 48 | requestBuild = function (build, callback) { 49 | var planUri = self.configuration.url + "/rest/api/latest/result/" + self.configuration.planKey + "/" + build.number + ".json"; 50 | var urlParams = getUrlParams(); 51 | 52 | request({ uri: planUri, qs: urlParams }, function(error, response, body) { 53 | if (error) { 54 | callback(error); 55 | return; 56 | } 57 | try { 58 | var bodyJson = JSON.parse(body); 59 | callback(error, simplifyBuild(bodyJson)); 60 | } catch (parseError) { 61 | callback(parseError, null); 62 | } 63 | }); 64 | }, 65 | simplifyBuild = function (res) { 66 | return { 67 | id: self.configuration.slug + '|' + res.number, 68 | project: res.plan.shortName, 69 | number: res.number, 70 | isRunning: !res.buildCompletedTime, 71 | startedAt: res.buildStartedTime, 72 | finishedAt: res.buildCompletedTime, 73 | requestedFor: getAuthors(res.buildReason), 74 | status: getStatus(res.state, res.lifeCycleState), 75 | statusText: getStatusText(res.state, res.lifeCycleState), 76 | reason: striptags(res.buildReason), 77 | hasErrors: !res.successful, 78 | hasWarnings: !res.successful, 79 | url: self.configuration.url + '/browse/' + res.buildResultKey 80 | }; 81 | }, 82 | getAuthors = function(reason) { 83 | var urlRegex = /]*>([\s\S]*?)<\/a>/g; 84 | var links = reason.match(urlRegex); 85 | if (links !== null) { 86 | return links.map( 87 | function(url) { 88 | return striptags(url); 89 | } 90 | ).join(', '); 91 | } 92 | return 'System'; 93 | }, 94 | getStatus = function(state, lifeCycleState) { 95 | if (state === 'started') return "Blue"; 96 | if (state === 'created') return "Blue"; 97 | if (state === 'canceled') return "Gray"; 98 | if (state === 'Failed') return "Red"; 99 | if (state === 'Unknown') { 100 | if (lifeCycleState === 'NotBuilt') return 'Gray'; 101 | if (lifeCycleState === 'InProgress') return '#FF8C00'; // dark orange 102 | } 103 | return "Green"; 104 | }, 105 | getStatusText = function(state, lifeCycleState) { 106 | if (state === 'Unknown') { 107 | if (lifeCycleState === 'NotBuilt') return 'Stopped'; 108 | if (lifeCycleState === 'InProgress') return 'In Progress'; 109 | } 110 | 111 | return state; 112 | }; 113 | 114 | self.cache = { 115 | expires: 0, 116 | projects: {} 117 | }; 118 | 119 | self.configure = function (config) { 120 | self.configuration = config; 121 | 122 | if (config.username && config.password) { 123 | var protocol = config.url.match(/(^|\s)(https?:\/\/)/i); 124 | if (Array.isArray(protocol)) { 125 | protocol = protocol[0]; 126 | var url = config.url.substr(protocol.length); 127 | host = protocol + config.username + ":" + config.password + "@" + url; 128 | } 129 | } 130 | self.configuration.url = host || config.url; 131 | }; 132 | 133 | self.check = function (callback) { 134 | queryBuilds(callback); 135 | }; 136 | }; 137 | -------------------------------------------------------------------------------- /app/services/BambooDeploy.js: -------------------------------------------------------------------------------- 1 | var request = require('request'), 2 | async = require('async'), 3 | striptags = require('striptags'), 4 | _ = require('lodash'); 5 | 6 | 7 | module.exports = function () { 8 | var self = this, 9 | queryBuilds = function (callback) { 10 | fetchDeployments(function (error, body) { 11 | if (error) { 12 | callback(error); 13 | return; 14 | } 15 | 16 | async.map(body, fetchDeployment, function (error, results) { 17 | callback(error, results); 18 | }); 19 | }); 20 | }, 21 | fetchDeployments = function (callback) { 22 | const url = self.configuration.url+ "/rest/api/latest/deploy/dashboard/"+self.configuration.projectId; 23 | const requestParam = { 24 | uri: url, 25 | qs: { 26 | 'os_authType': 'basic' 27 | }, 28 | headers: { 'cache-control': 'no-cache' }, 29 | json: true 30 | }; 31 | request(requestParam, function (error, response, body) { 32 | try { 33 | callback(error, body); 34 | } catch (parseError) { 35 | callback(parseError, null); 36 | } 37 | }); 38 | }, 39 | fetchDeployment = function (build, callback) { 40 | const url = self.configuration.url+ "/rest/api/latest/deploy/dashboard/"+self.configuration.projectId; 41 | const requestParam = { 42 | uri: url, 43 | qs: { 44 | 'os_authType': 'basic' 45 | }, 46 | headers: { 'cache-control': 'no-cache' }, 47 | json: true 48 | }; 49 | request(requestParam, function (error, response, body) { 50 | if (error) { 51 | callback(error); 52 | return; 53 | } 54 | try { 55 | let result = parseDeployResponse(body); 56 | callback(error, result.filter(r => r.environmentName === self.configuration.environmentName)[0]); 57 | } catch (parseError) { 58 | callback(parseError, null); 59 | } 60 | }); 61 | }, 62 | parseDeployResponse = function(res){ 63 | return _.flatMap(res, ({ environmentStatuses, deploymentProject }) => 64 | _.map(environmentStatuses, ({ environment, deploymentResult }) => 65 | ({ 66 | id: self.configuration.slug + '|' + deploymentResult.id, 67 | project: environment.name + ' ➜ ' + deploymentProject.name + ' [' + deploymentResult.deploymentVersionName + ']', 68 | number: deploymentResult.key.resultNumber, 69 | isRunning: !deploymentResult.finishedDate, 70 | startedAt: deploymentResult.startedDate, 71 | finishedAt: deploymentResult.finishedDate, 72 | requestedFor: getAuthors(deploymentResult.reasonSummary), 73 | status: getStatus(deploymentResult.deploymentState, deploymentResult.lifeCycleState), 74 | statusText: deploymentResult.deploymentState, 75 | reason: striptags(deploymentResult.reasonSummary), 76 | hasErrors: 'SUCCESS' !== deploymentResult.deploymentState, 77 | hasWarnings: 'SUCCESS' !== deploymentResult.deploymentState, 78 | url: self.configuration.url + "/deploy/viewDeploymentResult.action?deploymentResultId="+deploymentResult.id, 79 | environmentName: environment.name 80 | }) 81 | ) 82 | ); 83 | }, 84 | getAuthors = function (reason) { 85 | var urlRegex = /]*>([\s\S]*?)<\/a>/g; 86 | var links = reason.match(urlRegex); 87 | if (links !== null) { 88 | return links.map( 89 | function (url) { 90 | return striptags(url); 91 | } 92 | ).join(', '); 93 | } 94 | return 'System'; 95 | }, 96 | getStatus = function (state, lifeCycleState) { 97 | if (state === 'STARTED') return "Blue"; 98 | if (state === 'FINISHED') return "Blue"; 99 | if (state === 'CANCELLED') return "Gray"; 100 | if (state === 'FAILED') return "Red"; 101 | if (state === 'UNKNOWN') { 102 | if (lifeCycleState === 'NOT_BUILT') return 'Gray'; 103 | if (lifeCycleState === 'IN_PROGRESS') return '#FF8C00'; // dark orange 104 | } 105 | return "Green"; 106 | }; 107 | 108 | self.cache = { 109 | expires: 0, 110 | projects: {} 111 | }; 112 | 113 | self.configure = function (config) { 114 | self.configuration = config; 115 | 116 | if (config.username && config.password) { 117 | var protocol = config.url.match(/(^|\s)(https?:\/\/)/i); 118 | if (Array.isArray(protocol)) { 119 | protocol = protocol[0]; 120 | var url = config.url.substr(protocol.length); 121 | host = protocol + config.username + ":" + config.password + "@" + url; 122 | } 123 | } 124 | self.configuration.url = host || config.url; 125 | }; 126 | 127 | self.check = function (callback) { 128 | queryBuilds(callback); 129 | }; 130 | }; 131 | -------------------------------------------------------------------------------- /app/services/BitbucketPipelines.js: -------------------------------------------------------------------------------- 1 | var request = require('../requests'); 2 | 3 | module.exports = function () { 4 | var self = this, 5 | makeUrl = function () { 6 | return 'https://api.bitbucket.org/2.0/repositories/' + (self.configuration.teamname || self.configuration.username) + '/' + self.configuration.slug + '/pipelines/?sort=-created_on&pagelen=1'; 7 | }, 8 | makeBasicAuthToken = function() { 9 | return Buffer.from(self.configuration.username + ':' + self.configuration.apiKey).toString('base64'); 10 | }, 11 | makeRequest = function (url, callback) { 12 | request.makeRequest({ 13 | url: url, 14 | headers: {Authorization: 'Basic ' + makeBasicAuthToken()} 15 | }, callback); 16 | }, 17 | parseDate = function (dateAsString) { 18 | return dateAsString ? new Date(dateAsString) : null; 19 | }, 20 | forEachResult = function (body, callback) { 21 | for (var i = 0; i < body.values.length; i++) { 22 | callback(body.values[i]); 23 | } 24 | }, 25 | getStatus = function (statusText, resultText, stageType) { 26 | if (statusText === "IN_PROGRESS" && stageType === "pipeline_state_in_progress_paused") return "Gray"; 27 | if (statusText === "COMPLETED" && resultText === "SUCCESSFUL") return "Green"; 28 | if (statusText === "COMPLETED" && resultText === "FAILED") return "Red"; 29 | if (statusText === "COMPLETED" && resultText === "STOPPED") return "Gray"; 30 | if (statusText === "PENDING") return "'#FFA500'"; 31 | if (statusText === "IN_PROGRESS") return "Blue"; 32 | }, 33 | getStatusText = function (statusText, resultText, stageType) { 34 | if (statusText === "IN_PROGRESS" && stageType === "pipeline_state_in_progress_paused") return "Paused"; 35 | if (statusText === "COMPLETED" && resultText === "SUCCESSFUL") return "Succeeded"; 36 | if (statusText === "COMPLETED" && resultText === "FAILED") return "Failed"; 37 | if (statusText === "COMPLETED" && resultText === "STOPPED") return "Stopped"; 38 | if (statusText === "PENDING") return "Pending"; 39 | if (statusText === "IN_PROGRESS") return "In Progress"; 40 | 41 | return statusText; 42 | }, 43 | simplifyBuild = function (res) { 44 | return { 45 | id: res.uuid, 46 | project: res.repository.name, 47 | number: res.build_number, 48 | isRunning: !res.completed_on, 49 | startedAt: parseDate(res.created_on), 50 | finishedAt: parseDate(res.completed_on), 51 | requestedFor: (res.creator || {}).display_name, 52 | statusText: getStatusText(res.state.name, (res.state.result || {}).name, (res.state.stage || {}).type), 53 | status: getStatus(res.state.name, (res.state.result || {}).name, (res.state.stage || {}).type), 54 | url: res.repository.links.self.href 55 | }; 56 | }, 57 | queryBuilds = function (callback) { 58 | makeRequest(makeUrl(), function (error, body) { 59 | if (error || body.type === 'error') { 60 | callback(error || body.error); 61 | return; 62 | } 63 | 64 | var builds = []; 65 | 66 | forEachResult(body, function (res) { 67 | builds.push(simplifyBuild(res)); 68 | }); 69 | 70 | callback(error, builds); 71 | }); 72 | }; 73 | 74 | self.configure = function (config) { 75 | self.configuration = config; 76 | }; 77 | 78 | self.check = function (callback) { 79 | queryBuilds(callback); 80 | }; 81 | }; 82 | -------------------------------------------------------------------------------- /app/services/Bitrise.js: -------------------------------------------------------------------------------- 1 | var request = require('request'), 2 | async = require('async'); 3 | 4 | module.exports = function () { 5 | var self = this, 6 | requestBuilds = function (callback) { 7 | bitriseRequest( 8 | '/apps/' + self.configuration.slug + '/builds', 9 | function(error, response, body) { 10 | callback(error, body.data); 11 | } 12 | ); 13 | }, 14 | requestBuild = function (build, callback) { 15 | bitriseRequest( 16 | '/apps/' + self.configuration.slug + '/builds/' + build.slug, 17 | function(error, response, body) { 18 | if (error) { 19 | callback(error); 20 | return; 21 | } 22 | 23 | callback(error, simplifyBuild(body.data)); 24 | } 25 | ); 26 | }, 27 | queryBuilds = function (callback) { 28 | requestBuilds(function (error, body) { 29 | if (error) { 30 | callback(error); 31 | return; 32 | } 33 | 34 | async.map(body, requestBuild, function (error, results) { 35 | callback(error, results); 36 | }); 37 | }); 38 | }, 39 | parseDate = function (dateAsString) { 40 | return new Date(dateAsString); 41 | }, 42 | getStatus = function (status) { 43 | var statuses = [ "Blue", "Green", "Red", "Gray" ]; 44 | return statuses[status]; 45 | }, 46 | getStatusText = function(text) { 47 | var map = { 48 | success: 'finished' 49 | }; 50 | 51 | return map[text] || text; 52 | }, 53 | simplifyBuild = function (res) { 54 | return { 55 | id: res.slug, 56 | project: self.configuration.appName, 57 | number: res.build_number, 58 | isRunning: res.status === 0, 59 | startedAt: parseDate(res.triggered_at), 60 | finishedAt: parseDate(res.finished_at), 61 | requestedFor: (res.original_build_params.pull_request_author || res.triggered_by || '') + " building " + res.branch, 62 | status: getStatus(res.status), 63 | statusText: getStatusText(res.status_text), 64 | reason: res.triggered_workflow, 65 | hasErrors: false, 66 | hasWarnings: false, 67 | url: 'https://www.' + self.configuration.url + '/build/' + res.slug 68 | }; 69 | }, 70 | bitriseRequest = function (path, callback) { 71 | var options = { 72 | 'url': 'https://api.' + self.configuration.apiUrl + path, 73 | 'json' : true, 74 | 'headers': { Authorization: 'token ' + self.configuration.token } 75 | }; 76 | 77 | request(options, callback); 78 | }; 79 | 80 | self.configure = function (config) { 81 | self.configuration = config; 82 | 83 | self.configuration.apiVersion = self.configuration.apiVersion || 'v0.1'; 84 | self.configuration.url = self.configuration.url || 'bitrise.io'; 85 | self.configuration.apiUrl = self.configuration.url + '/' + self.configuration.apiVersion; 86 | self.configuration.token = self.configuration.token || ''; 87 | 88 | bitriseRequest( 89 | '/apps/' + self.configuration.slug, 90 | function (error, response, body) { 91 | self.configuration.appName = body.data.repo_owner + '/' + body.data.title; 92 | } 93 | ); 94 | }; 95 | 96 | self.check = function (callback) { 97 | queryBuilds(callback); 98 | }; 99 | }; 100 | -------------------------------------------------------------------------------- /app/services/BuddyBuild.js: -------------------------------------------------------------------------------- 1 | var request = require('request'), 2 | async = require('async'); 3 | 4 | module.exports = function () { 5 | var self = this, 6 | getRequestHeaders = function () { // build request header 7 | if (typeof process.env.BUILDBUDDY_ACCESS_TOKEN !== 'undefined') { 8 | self.configuration.access_token = process.env.BUILDBUDDY_ACCESS_TOKEN; 9 | } 10 | return { 11 | 'ACCESS-TOKEN': self.configuration.access_token, 12 | 'Authorization': 'Bearer ' + self.configuration.access_token, 13 | 'accept-encoding': 'application/json' 14 | }; 15 | }, 16 | makeUrl = function (app_id, build_id, branch, baseUrl) { //assemble url with designated branch id 17 | if (build_id) { // to query one build provide a build_id 18 | baseUrl += '/' + build_id; 19 | } else if (app_id) { // to get lastest build provide app_id but no build_id 20 | baseUrl += "/" + app_id + "/build/latest?branch=" + branch; 21 | } 22 | 23 | return baseUrl; 24 | }, 25 | makeRequest = function (url, callback) { //make http request to BuildBuddy API 26 | request({ 27 | headers: getRequestHeaders(), 28 | url: url, 29 | json: true 30 | }, 31 | function (error, response, body) { 32 | if (error) { 33 | callback(error); 34 | return; 35 | } 36 | 37 | if (response.statusCode === 500) { 38 | callback(new Error(response.statusMessage)); 39 | return; 40 | } 41 | 42 | callback(error, body); 43 | }); 44 | }, 45 | parseDate = function (dateAsString) { 46 | return new Date(dateAsString); 47 | }, 48 | forEachResult = function (body, callback) { 49 | 50 | callback(body); 51 | 52 | }, 53 | forEachApp = function (body, callback) { 54 | for (var i = 0; i < body.length; i++) { 55 | callback(body[i]); 56 | } 57 | 58 | }, 59 | isNullOrWhiteSpace = function (string) { 60 | if (!string) { 61 | return true; 62 | } 63 | 64 | return string === null || string.match(/^ *$/) !== null; 65 | }, 66 | getStatus = function (statusText) { 67 | switch (statusText) { 68 | case "success": 69 | return "Green"; 70 | case "failed": 71 | return "Red"; 72 | case "running": 73 | return "Blue"; 74 | case "stopped": 75 | return "Red"; 76 | case "queued": 77 | return "Blue"; 78 | case "canceled": 79 | return "#FFA500"; 80 | default: 81 | return "Gray"; 82 | } 83 | }, 84 | parseLink = function (appId, buildId) { // point to the build 85 | return 'https://dashboard.buddybuild.com/apps/' + appId + '/build/' + buildId; 86 | }, 87 | simplifyBuild = function (app, res) { 88 | return { 89 | id: res._id, 90 | platform: app ? app.platform : self.configuration.project_name, 91 | project: app ? app.app_name : self.configuration.project_name + ': ' + res.commit_info.branch, 92 | number: 'Build: ' + res.build_number, 93 | isRunning: res.BuildFinished, 94 | startedAt: parseDate(res.started_at), 95 | finishedAt: parseDate(res.finished_at), 96 | requestedFor: res.commit_info.author, 97 | statusText: res.build_status, 98 | status: getStatus(res.build_status), 99 | reason: res.commit_info.message, 100 | finished: res.finished, 101 | hasErrors: !isNullOrWhiteSpace(res.Errors), 102 | hasWarnings: !isNullOrWhiteSpace(res.Warnings), 103 | branch: res.commit_info.branch, 104 | url: parseLink(res.app_id, res._id) 105 | }; 106 | }, 107 | queryBuilds = function (callback) { // query the build 108 | makeRequest(makeUrl(self.configuration.app_id, self.configuration.build_id, self.configuration.branch, self.configuration.url), function (error, body) { 109 | if (error) { 110 | callback(error); 111 | return; 112 | } 113 | 114 | var builds = []; 115 | 116 | forEachResult(body, function (res) { 117 | builds.push(simplifyBuild(null, res)); 118 | }); 119 | 120 | callback(error, builds); 121 | }); 122 | }, 123 | queryBuildsAsync = function (app) { // query the build 124 | return new Promise(function (resolve, reject) { 125 | makeRequest(makeUrl(app._id, null, self.configuration.branch, self.configuration.url), function (error, body) { 126 | if (error) { 127 | reject(error); 128 | return; 129 | } 130 | resolve(simplifyBuild(app, body)); 131 | }); 132 | }); 133 | }, 134 | queryAllAppBuilds = function (callback) { // query the build 135 | makeRequest(makeUrl(null, null, self.configuration.branch, self.configuration.url), function (error, body) { 136 | if (error) { 137 | callback(error); 138 | return; 139 | } 140 | var asyncBuildsQuery = body.map(function (app) { 141 | return queryBuildsAsync(app); 142 | }); 143 | 144 | Promise.all(asyncBuildsQuery).then(function (builds) { 145 | callback(error, builds); 146 | }); 147 | }); 148 | }; 149 | 150 | self.configure = function (config) { 151 | self.configuration = config; 152 | }; 153 | 154 | self.check = function (callback) { 155 | if (self.configuration.app_id) { 156 | console.log('Fetching builds for app : ' + self.configuration.app_id); 157 | queryBuilds(callback); 158 | } else { 159 | console.log('app_id not specified, fetching builds for all apps ...'); 160 | queryAllAppBuilds(callback); 161 | } 162 | }; 163 | 164 | self.makeURL = function (app_id, build_id, branch, url) { 165 | return makeUrl(app_id, build_id, branch, url); 166 | }; 167 | 168 | self.getHeaders = function (access_token) { 169 | self.configuration.access_token = access_token; 170 | return getRequestHeaders(); 171 | }; 172 | 173 | self.getStatus = function (statusText) { 174 | "use strict"; 175 | return getStatus(statusText); 176 | }; 177 | }; 178 | -------------------------------------------------------------------------------- /app/services/Buildkite.js: -------------------------------------------------------------------------------- 1 | var request = require("request"); 2 | var graphql = require("graphql.js"); 3 | 4 | module.exports = function() { 5 | var configuration = {}; 6 | var graph; 7 | 8 | return { 9 | configure: function(options) { 10 | configuration = Object.assign(configuration, options); 11 | configuration.orgSlug = 12 | process.env.BUILDKITE_ORGANISATION_SLUG || configuration.orgSlug; 13 | configuration.teamSlug = 14 | process.env.BUILDKITE_TEAM_SLUG || configuration.teamSlug; 15 | 16 | if (!configuration.orgSlug) 17 | throw new Error( 18 | "Must configure the orgSlug property for the buildkite plugin" 19 | ); 20 | if (!configuration.teamSlug) 21 | throw new Error( 22 | "Must configure the teamSlug property for the buildkite plugin" 23 | ); 24 | if (!process.env.BUILDKITE_TOKEN) 25 | throw new Error( 26 | "Must configure the BUILDKITE_TOKEN environment variable with your bk token." 27 | ); 28 | 29 | graph = graphql("https://graphql.buildkite.com/v1", { 30 | asJSON: true, 31 | headers: { 32 | Authorization: `Bearer ${process.env.BUILDKITE_TOKEN}` 33 | } 34 | }); 35 | }, 36 | check: function(callback) { 37 | graph 38 | .query( 39 | ` 40 | SimpleQuery { 41 | organization(slug: "${configuration.orgSlug}") { 42 | name 43 | pipelines(first: 100, team: "${configuration.teamSlug}") { 44 | edges { 45 | node { 46 | name 47 | slug 48 | builds(first: 1) { 49 | edges { 50 | node { 51 | branch 52 | message 53 | number 54 | state 55 | startedAt 56 | finishedAt 57 | url 58 | createdBy { 59 | ... on User { 60 | name 61 | } 62 | ... on UnregisteredUser { 63 | name 64 | } 65 | } 66 | } 67 | } 68 | } 69 | } 70 | } 71 | } 72 | } 73 | } 74 | `, 75 | {} 76 | ) 77 | .then(function(response) { 78 | const result = response.organization.pipelines.edges.map(x => { 79 | const pipeline = x.node; 80 | const build = 81 | x.node.builds.edges.length > 0 ? 82 | x.node.builds.edges[0].node : { 83 | branch: "master", 84 | isRunning: false, 85 | createdBy: { 86 | name: "" 87 | }, 88 | state: "NOT_RUN", 89 | message: "Pipeline Created", 90 | number: "N/A" 91 | }; 92 | 93 | const buildStates = { 94 | SKIPPED: { desc: "The build was skipped", color: "#ffff00" }, 95 | SCHEDULED: { 96 | desc: "The build has yet to start running jobs", 97 | color: "#0000ff" 98 | }, 99 | RUNNING: { 100 | desc: " The build is currently running jobs", 101 | color: "#ffa500" 102 | }, 103 | PASSED: { desc: "The build passed", color: "#008000" }, 104 | FAILED: { desc: "The build failed", color: "#ff0000" }, 105 | CANCELING: { 106 | desc: "The build is currently being canceled", 107 | color: "#ffb3b3" 108 | }, 109 | CANCELED: { desc: "The build was canceled", color: "#ff4d4d" }, 110 | BLOCKED: { desc: "The build is blocked", color: "#003300" }, 111 | NOT_RUN: { desc: "The build wasn't run", color: "#808080" } 112 | }; 113 | 114 | return { 115 | id: pipeline.slug + "/" + build.number, 116 | project: pipeline.name, 117 | branch: build.branch, 118 | number: build.number, 119 | isRunning: build.state === "RUNNING", 120 | startedAt: new Date(build.startedAt), 121 | finishedAt: new Date(build.finishedAt), 122 | requestedFor: build.createdBy && build.createdBy.name, 123 | status: buildStates[build.state].color, 124 | statusText: build.state, 125 | reason: build.message, 126 | hasErrors: false, 127 | hasWarnings: false, 128 | url: build.url 129 | }; 130 | }); 131 | callback(null, result); 132 | }) 133 | .catch(function(error) { 134 | console.log(error); 135 | callback(error); 136 | }); 137 | } 138 | }; 139 | }; 140 | -------------------------------------------------------------------------------- /app/services/CCTray.js: -------------------------------------------------------------------------------- 1 | // CCTray format: https://github.com/erikdoe/ccmenu/wiki/Multiple-Project-Summary-Reporting-Standard 2 | // Sample CCTray data: https://api.travis-ci.org/repos/erikdoe/ccmenu/cc.xml 3 | 4 | var request = require('request'), 5 | xml2jsParser = require('xml2js').parseString; 6 | 7 | module.exports = function () { 8 | var self = this, 9 | getBuilds = function(callback) { 10 | makeRequest(self.config.url, function(err, result) { 11 | if (err) { 12 | callback(err); 13 | return; 14 | } 15 | callback(err, result.Projects.Project.map(function(project) { 16 | return simplifyBuild(project.$); 17 | })); 18 | }); 19 | }, 20 | makeRequest = function (url, callback) { 21 | request({ 22 | url: url 23 | }, 24 | function (error, response, body) { 25 | if (error) { 26 | callback(error); 27 | return; 28 | } 29 | 30 | xml2jsParser(body, function(err, result) { 31 | callback(err, result); 32 | }); 33 | }); 34 | }, 35 | simplifyBuild = function (res) { 36 | return { 37 | id: res.lastBuildLabel, 38 | project: res.name, 39 | number: res.lastBuildLabel, 40 | isRunning: isRunning(res.activity), 41 | startedAt: res.lastBuildTime, 42 | finishedAt: res.lastBuildTime, 43 | requestedFor: "", 44 | status: getBuildStatus(res), 45 | statusText: getBuildStatusText(res), 46 | reason: "", 47 | hasErrors: hasErrors(res.lastBuildStatus), 48 | hasWarnings: hasErrors(res.lastBuildStatus), 49 | url: res.webUrl 50 | }; 51 | }, 52 | isRunning = function(activity) { 53 | return activity === 'Building'; 54 | }, 55 | getBuildStatus = function(res) { 56 | if (res.activity === 'Building') { 57 | return 'Blue'; 58 | } else if (res.lastBuildStatus === 'Success') { 59 | return 'Green'; 60 | } else if (res.lastBuildStatus === 'Failure' || res.lastBuildStatus === 'Exception') { 61 | return 'Red'; 62 | } else { 63 | return 'Gray'; 64 | } 65 | }, 66 | getBuildStatusText = function(res) { 67 | if (res.activity === 'Building') { 68 | return res.activity; 69 | } else { 70 | return res.lastBuildStatus; 71 | } 72 | }, 73 | hasErrors = function(buildStatus) { 74 | return buildStatus === 'Failure' || buildStatus === 'Exception'; 75 | }; 76 | 77 | self.configure = function (config) { 78 | self.config = config; 79 | }; 80 | 81 | self.check = getBuilds; 82 | }; 83 | -------------------------------------------------------------------------------- /app/services/Drone.js: -------------------------------------------------------------------------------- 1 | var request = require("request"); 2 | 3 | module.exports = function () { 4 | var self = this, 5 | requestBuilds = function (callback) { 6 | const url = `${self.api_base}/repos/${self.configuration.slug}/builds`; 7 | if (self.configuration.debug) { 8 | console.info(`Requesting GET ${url}`); 9 | } 10 | 11 | request( 12 | { 13 | method: "GET", 14 | url: url, 15 | headers: { 16 | Authorization: self.configuration.token 17 | }, 18 | json: true 19 | }, 20 | function (error, response, body) { 21 | if (response.statusCode !== 200 || !body || !Array.isArray(body)) { 22 | error = `Invalid response for GET ${url}: ${response.statusCode} with body ${JSON.stringify(response)}`; 23 | } 24 | callback(error, body); 25 | } 26 | ); 27 | }, 28 | filterBuilds = function (build) { 29 | var matchesBranch = true, 30 | matchesEvent = true; 31 | 32 | if (self.configuration.branch) { 33 | matchesBranch = build.target === self.configuration.branch; 34 | } 35 | 36 | if (self.configuration.event) { 37 | matchesEvent = build.event === self.configuration.event; 38 | } 39 | return matchesBranch && matchesEvent; 40 | }, 41 | queryBuilds = function (callback) { 42 | requestBuilds(function (error, body) { 43 | if (error) { 44 | callback(error); 45 | return; 46 | } 47 | 48 | callback( 49 | error, 50 | body.filter(filterBuilds).map(build => simplifyBuild(build)) 51 | ); 52 | }); 53 | }, 54 | parseDate = function (unix_timestamp) { 55 | return new Date(unix_timestamp * 1000); 56 | }, 57 | getStatus = function (status) { 58 | // Statuses : https://github.com/drone/drone/blob/5b6a3d8ff4c37283cf37df20d871cc8dfe439565/core/status.go 59 | switch (status) { 60 | case "pending": 61 | case "running": 62 | case "waiting_on_dependencies": 63 | return "Blue"; 64 | 65 | case "skipped": 66 | case "blocked": 67 | case "killed": 68 | return "Gray"; 69 | 70 | case "declined": 71 | case "failure": 72 | case "error": 73 | return "Red"; 74 | 75 | case "success": 76 | return "Green"; 77 | 78 | default: 79 | return "Gray"; 80 | } 81 | }, 82 | simplifyBuild = function (res) { 83 | return { 84 | id: `drone|${self.configuration.slug}|${res.id}`, 85 | project: self.configuration.slug, 86 | number: res.number, 87 | isRunning: res.finished === 0, 88 | startedAt: parseDate(res.started), 89 | finishedAt: parseDate(res.finished), 90 | requestedFor: res.author_name || res.author_login || res.author_email, 91 | status: getStatus(res.status), 92 | statusText: res.status, 93 | reason: res.event, 94 | hasErrors: res.status === "error" || res.status === "failure", 95 | hasWarnings: res.status === "blocked", 96 | url: `https://${self.configuration.url}/${self.configuration.slug}/${ 97 | res.number 98 | }` 99 | }; 100 | }; 101 | 102 | self.configure = function (config) { 103 | self.configuration = config; 104 | 105 | if (!self.configuration.slug) { 106 | console.error(`[Drone] Please configure the [slug] parameter`); 107 | return; 108 | } 109 | 110 | if (!self.configuration.url) { 111 | console.error( 112 | `[Drone ${ 113 | self.configuration.slug 114 | }] Please configure the [url] parameter` 115 | ); 116 | return; 117 | } 118 | 119 | if (!self.configuration.token) { 120 | console.error( 121 | `[Drone ${ 122 | self.configuration.slug 123 | }] Please configure the [token] parameter` 124 | ); 125 | return; 126 | } 127 | 128 | self.configuration.url = self.configuration.url; 129 | self.configuration.token = self.configuration.token || ""; 130 | 131 | if (typeof self.configuration.caPath !== "undefined") { 132 | request = request.defaults({ 133 | agentOptions: { 134 | ca: require("fs") 135 | .readFileSync(self.configuration.caPath) 136 | .toString() 137 | .split("\n\n") 138 | } 139 | }); 140 | } 141 | 142 | self.api_base = `https://${self.configuration.url}/api`; 143 | }; 144 | 145 | self.check = function (callback) { 146 | queryBuilds(callback); 147 | }; 148 | }; 149 | -------------------------------------------------------------------------------- /app/services/PRTG.js: -------------------------------------------------------------------------------- 1 | // PRTG format: https://{prtgserver}/api/getsensordetails.json?id={Id}&username={myuser}&passhash={mypasshash} 2 | 3 | var request = require('../requests'); 4 | 5 | const status = Object.freeze({ 6 | ok: "3", 7 | error: "5", 8 | errorConfirmed: "13", 9 | warning: "4", 10 | paused: "7", 11 | unusual: "10", 12 | }); 13 | 14 | module.exports = function () { 15 | var self = this, 16 | getSensors = function(callback) { 17 | var url = self.configuration.url + 18 | '/api/getsensordetails.json?id=' + self.configuration.sensorId + 19 | '&username=' + self.configuration.username + 20 | '&passhash=' + self.configuration.passhash; 21 | 22 | request.makeRequest({ 23 | url: url 24 | }, (err, body) => { 25 | transformData(err, body, callback); 26 | }); 27 | }, 28 | transformData = function (err, body, callback) { 29 | if (err) { 30 | callback(err); 31 | return; 32 | } 33 | if (!(body && body.sensordata)) { 34 | callback('No sensor data found'); 35 | return; 36 | } 37 | 38 | var data = body.sensordata; 39 | 40 | var transformedData = { 41 | id: self.configuration.sensorId, 42 | number: data.name, 43 | project: data.parentgroupname, 44 | isRunning: false, 45 | // Errors first, then warnings, ok, paused and unknown should go to the end of the list. 46 | startedAt: hasErrors(data) || hasWarnings(data) ? new Date(Date.now() - getLastCheckedNumber(data.lastcheck)) : Date.parse('1970-01-01 00:00'), 47 | finishedAt: hasErrors(data) ? new Date() : (hasWarnings(data) ? new Date(Date.now() - 1) : Date.parse('1970-01-01 00:01')), 48 | status: getBuildStatus(data), 49 | statusText: getBuildStatusText(data), 50 | hasErrors: hasErrors(data), 51 | hasWarnings: hasWarnings(data), 52 | url: self.configuration.url + "/sensor.htm?id=" + self.configuration.sensorId, 53 | reason: "sensor", 54 | requestedFor: "" 55 | }; 56 | 57 | callback(null, [ transformedData ]); 58 | }, 59 | getLastCheckedNumber = function(str){ 60 | return str.substr(0, str.indexOf('.')); 61 | }, 62 | getBuildStatus = function(data) { 63 | if (data.statusid === status.ok) { 64 | return 'Green'; 65 | } 66 | else if (data.statusid === status.error) { 67 | return 'Red'; 68 | } 69 | else if (data.statusid === status.errorConfirmed) { 70 | return '#e67278'; 71 | } 72 | else if (data.statusid === status.warning) { 73 | return '#f5c500'; 74 | } 75 | else if (data.statusid === status.paused) { 76 | return '#477ec0'; 77 | } 78 | else if (data.statusid === status.unusual) { 79 | return '#f59c00'; 80 | } 81 | else { 82 | return 'Gray'; 83 | } 84 | }, 85 | getBuildStatusText = function(data) { 86 | if (data.statusid === status.ok) { 87 | return 'OK'; 88 | } 89 | else if (data.statusid === status.error) { 90 | return 'Error'; 91 | } 92 | else if (data.statusid === status.errorConfirmed) { 93 | return 'Error (Confirmed)'; 94 | } 95 | else if (data.statusid === status.warning) { 96 | return 'Warning'; 97 | } 98 | else if (data.statusid === status.paused) { 99 | return 'Paused'; 100 | } 101 | else if (data.statusid === status.unusual) { 102 | return 'Unusual'; 103 | } 104 | else { 105 | return 'Unknown'; 106 | } 107 | }, 108 | hasErrors = function(data) { 109 | return data.statusid === status.error || data.statusid === status.errorConfirmed; 110 | }; 111 | hasWarnings = function(data) { 112 | return data.statusid === status.warning || data.statusid === status.unusual; 113 | }; 114 | 115 | self.configure = function (config) { 116 | self.configuration = config; 117 | }; 118 | 119 | self.check = getSensors; 120 | }; 121 | -------------------------------------------------------------------------------- /app/services/Shippable.js: -------------------------------------------------------------------------------- 1 | var request = require('request'); 2 | 3 | module.exports = function () { 4 | var self = this, 5 | getRequestHeaders = function () { 6 | if (typeof process.env.SHIPPABLE_API_TOKEN !== 'undefined') { 7 | self.configuration.token = process.env.SHIPPABLE_API_TOKEN; 8 | } 9 | return { 10 | 'Accept': 'application/json', 11 | 'Authorization': 'apiToken ' + self.configuration.token 12 | }; 13 | }, 14 | makeUrl = function () { 15 | var url = self.configuration.url + '/runs?sortBy=createdAt&sortOrder=-1'; 16 | if (self.configuration.projects) { 17 | url += '&projectIds=' + self.configuration.projects; 18 | } 19 | if (self.configuration.branch) { 20 | url += '&branch=' + self.configuration.branch; 21 | } 22 | if (self.configuration.limit) { 23 | url += '&limit=' + self.configuration.limit; 24 | } 25 | 26 | return url; 27 | }, 28 | makeRequest = function (url, callback) { 29 | request({ 30 | headers: getRequestHeaders(), 31 | url: url, 32 | json: true 33 | }, 34 | function (error, response, body) { 35 | if (error) { 36 | callback(error); 37 | return; 38 | } 39 | 40 | callback(error, body); 41 | }); 42 | }, 43 | parseDate = function (dateAsString) { 44 | return new Date(dateAsString); 45 | }, 46 | forEachResult = function (body, callback) { 47 | for (var i = 0; i < body.length; i++) { 48 | callback(body[i]); 49 | } 50 | }, 51 | getStatus = function (statusCode) { 52 | switch (statusCode) { 53 | case 20: // Processing 54 | return 'Blue'; 55 | case 30: // Success 56 | return 'Green'; 57 | case 70: // Cancelled 58 | return '#FFA500'; 59 | case 60: // Timeout 60 | case 80: // Failed 61 | return 'Red'; 62 | default: 63 | return 'Gray'; 64 | } 65 | }, 66 | simplifyBuild = function (res) { 67 | return { 68 | id: res.id, 69 | project: res.projectName, 70 | number: res.runNumber, 71 | isRunning: res.endedAt === null, 72 | startedAt: parseDate(res.startedAt), 73 | finishedAt: parseDate(res.endedAt), 74 | requestedFor: res.triggeredBy.displayName, 75 | status: getStatus(res.statusCode), 76 | statusText: res.lastCommitShortDescription, 77 | reason: res.branchName, 78 | hasErrors: false, 79 | hasWarnings: false, 80 | branch: res.branchName, 81 | }; 82 | }, 83 | queryBuilds = function (callback) { 84 | makeRequest(makeUrl(), function (error, body) { 85 | if (error) { 86 | callback(error); 87 | return; 88 | } 89 | 90 | var builds = []; 91 | 92 | forEachResult(body, function (res) { 93 | builds.push(simplifyBuild(res)); 94 | }); 95 | 96 | callback(error, builds); 97 | }); 98 | }; 99 | 100 | self.configure = function (config) { 101 | self.configuration = config; 102 | self.configuration.url = self.configuration.url || 'https://api.shippable.com'; 103 | }; 104 | 105 | self.check = function (callback) { 106 | queryBuilds(callback); 107 | }; 108 | }; 109 | -------------------------------------------------------------------------------- /app/services/Tfs2015.js: -------------------------------------------------------------------------------- 1 | var request = require('../requests'); 2 | 3 | module.exports = function () { 4 | var self = this, 5 | makeUrl = function (url, odata) { 6 | var baseUrl = self.configuration.url + '/_apis/build' + url; 7 | 8 | if (odata) { 9 | baseUrl += '?' + odata; 10 | } 11 | 12 | return baseUrl; 13 | }, 14 | makeRequest = function (url, callback) { 15 | request.makeRequest({ 16 | authentication: self.configuration.authentication, 17 | url: url, 18 | username: self.configuration.username, 19 | password: self.configuration.password, 20 | headers: {Accept: 'application/json'} 21 | }, callback); 22 | }, 23 | parseDate = function (dateAsString) { 24 | return dateAsString ? new Date(dateAsString) : null; 25 | }, 26 | forEachResult = function (body, callback) { 27 | for (var i = 0; i < body.value.length; i++) { 28 | callback(body.value[i]); 29 | } 30 | }, 31 | isNullOrWhiteSpace = function (string) { 32 | if(!string) { 33 | return true; 34 | } 35 | 36 | return string === null || string.match(/^ *$/) !== null; 37 | }, 38 | getStatus = function (statusText, resultText) { 39 | if (statusText === "completed" && resultText === "succeeded") return "Green"; 40 | if (statusText === "completed" && resultText === "failed") return "Red"; 41 | if (statusText === "completed" && resultText === "canceled") return "Gray"; 42 | if (statusText === "inProgress") return "Blue"; 43 | if (statusText === "stopped") return "Gray"; 44 | 45 | return "'#FFA500'"; 46 | }, 47 | getStatusText = function (statusText, resultText) { 48 | if (statusText === "completed" && resultText === "succeeded") return "Succeeded"; 49 | if (statusText === "completed" && resultText === "failed") return "Failed"; 50 | if (statusText === "inProgress") return "In Progress"; 51 | if (statusText === "stopped") return "Stopped"; 52 | 53 | return statusText + "/" + resultText; 54 | }, 55 | simplifyBuild = function (res) { 56 | return { 57 | id: res.id, 58 | project: res.project.name, 59 | definition: res.definition.name, 60 | number: res.buildNumber, 61 | isRunning: !res.finishTime, 62 | startedAt: parseDate(res.startTime), 63 | finishedAt: parseDate(res.finishTime), 64 | requestedFor: res.requestedFor.displayName, 65 | statusText: getStatusText(res.status, res.result), 66 | status: getStatus(res.status, res.result), 67 | reason: res.reason, 68 | hasErrors: !isNullOrWhiteSpace(res.Errors), 69 | hasWarnings: !isNullOrWhiteSpace(res.Warnings), 70 | url: res._links.web.href 71 | }; 72 | }, 73 | queryBuilds = function (callback) { 74 | makeRequest(makeUrl('/Builds', '$top=30'), function (error, body) { 75 | if (error) { 76 | callback(error); 77 | return; 78 | } 79 | 80 | var builds = []; 81 | 82 | forEachResult(body, function (res) { 83 | builds.push(simplifyBuild(res)); 84 | }); 85 | 86 | callback(error, builds); 87 | }); 88 | }; 89 | 90 | self.configure = function (config) { 91 | self.configuration = config; 92 | }; 93 | 94 | self.check = function (callback) { 95 | queryBuilds(callback); 96 | }; 97 | }; 98 | -------------------------------------------------------------------------------- /app/services/TfsProxy.js: -------------------------------------------------------------------------------- 1 | var request = require('../requests'); 2 | 3 | module.exports = function () { 4 | var self = this, 5 | tryGetTfsProxyUrlOfDocker = function () { 6 | return 'http://' + 7 | process.env.TFS_PROXY_PORT_4567_TCP_ADDR + 8 | ':' + 9 | process.env.TFS_PROXY_PORT_4567_TCP_PORT + 10 | '/builds'; 11 | }, 12 | makeRequest = function (url, callback) { 13 | request.makeRequest({ 14 | authentication: self.configuration.authentication, 15 | url: self.configuration.tfsProxyUrl || tryGetTfsProxyUrlOfDocker(), 16 | username: self.configuration.username, 17 | password: self.configuration.password, 18 | headers:{ 19 | Accept: 'application/json', 20 | url: self.configuration.url, 21 | username: self.configuration.username, 22 | password: self.configuration.password 23 | } 24 | }, callback); 25 | }, 26 | parseDate = function (dateAsString) { 27 | return new Date(dateAsString); 28 | }, 29 | forEachResult = function (body, callback) { 30 | for (var i = 0; i < body.builds.length; i++) { 31 | callback(body.builds[i]); 32 | } 33 | }, 34 | getStatus = function (statusText) { 35 | if (statusText === "Succeeded") return "Green"; 36 | if (statusText === "Failed") return "Red"; 37 | if (statusText === "In Progress") return "Blue"; 38 | if (statusText === "Stopped") return "Gray"; 39 | if (statusText === "Partially Succeeded") return "'#FFA500'"; 40 | 41 | return null; 42 | }, 43 | simplifyBuild = function (res) { 44 | return { 45 | id: res.teamProjectDefinition + '|' + res.buildDefinition + '|' + res.buildNumber, 46 | project: res.teamProjectDefinition, 47 | definition: res.buildDefinition, 48 | number: res.buildNumber, 49 | isRunning: !res.isBuildFinished, 50 | startedAt: parseDate(res.startTime), 51 | finishedAt: parseDate(res.finishTime), 52 | requestedFor: res.requestedFor, 53 | statusText: res.buildStatusText, 54 | status: getStatus(res.buildStatusText), 55 | reason: res.buildReasonText, 56 | hasErrors: false, 57 | hasWarnings: false, 58 | url: self.configuration.url + '/' + res.teamProjectDefinition + '/_build#buildUri=' + res.uri + '&_a=summary' 59 | }; 60 | }, 61 | queryBuilds = function (callback) { 62 | makeRequest(function (error, body) { 63 | if (error) { 64 | callback(error); 65 | return; 66 | } 67 | 68 | var builds = []; 69 | 70 | forEachResult(body, function (res) { 71 | builds.push(simplifyBuild(res)); 72 | }); 73 | 74 | callback(error, builds); 75 | }); 76 | }; 77 | 78 | self.configure = function (config) { 79 | self.configuration = config; 80 | }; 81 | 82 | self.check = function (callback) { 83 | queryBuilds(callback); 84 | }; 85 | }; 86 | -------------------------------------------------------------------------------- /app/services/Travis.js: -------------------------------------------------------------------------------- 1 | var request = require('request'), 2 | async = require('async'); 3 | 4 | module.exports = function () { 5 | var self = this, 6 | requestBuilds = function (callback) { 7 | request({ 8 | 'url': self.api_base + '/builds?access_token=' + self.configuration.token, 9 | 'json' : true 10 | }, 11 | function(error, response, body) { 12 | callback(error, body); 13 | }); 14 | }, 15 | requestBuild = function (build, callback) { 16 | request({ 17 | 'url': self.api_base + '/builds/' + build.id + '?access_token=' + self.configuration.token, 18 | 'json' : true 19 | }, 20 | function(error, response, body) { 21 | if (error) { 22 | callback(error); 23 | return; 24 | } 25 | 26 | callback(error, simplifyBuild(body)); 27 | }); 28 | }, 29 | queryBuilds = function (callback) { 30 | requestBuilds(function (error, body) { 31 | if (error) { 32 | callback(error); 33 | return; 34 | } 35 | 36 | async.map(body, requestBuild, function (error, results) { 37 | callback(error, results); 38 | }); 39 | }); 40 | }, 41 | parseDate = function (dateAsString) { 42 | return new Date(dateAsString); 43 | }, 44 | getStatus = function (result, state) { 45 | if (state === 'started') return "Blue"; 46 | if (state === 'created') return "Blue"; 47 | if (state === 'canceled') return "Gray"; 48 | if (result === null || result === 1) return "Red"; 49 | if (result === 0) return "Green"; 50 | 51 | return null; 52 | }, 53 | simplifyBuild = function (res) { 54 | return { 55 | id: self.configuration.slug + '|' + res.number, 56 | project: self.configuration.slug, 57 | number: res.number, 58 | isRunning: res.state === 'started', 59 | startedAt: parseDate(res.started_at), 60 | finishedAt: parseDate(res.finished_at), 61 | requestedFor: res.author_name, 62 | status: getStatus(res.result, res.state), 63 | statusText: res.state, 64 | reason: res.event_type, 65 | hasErrors: false, 66 | hasWarnings: false, 67 | url: 'https://' + self.configuration.url + '/' + self.configuration.slug + '/builds/' + res.id 68 | }; 69 | }; 70 | 71 | self.configure = function (config) { 72 | self.configuration = config; 73 | 74 | self.configuration.url = self.configuration.url || 'travis-ci.org'; 75 | self.configuration.token = self.configuration.token || ''; 76 | self.configuration.is_enterprise = self.configuration.is_enterprise || false; 77 | 78 | if (typeof self.configuration.caPath !== 'undefined') { 79 | request = request.defaults({ 80 | agentOptions: { 81 | ca: require('fs').readFileSync(self.configuration.caPath).toString().split("\n\n") 82 | } 83 | }); 84 | } 85 | 86 | self.api_base = 'https://'; 87 | if (self.configuration.is_enterprise) 88 | self.api_base += self.configuration.url + '/api'; 89 | else 90 | self.api_base += 'api.' + self.configuration.url; 91 | self.api_base += '/repos/' + self.configuration.slug; 92 | }; 93 | 94 | self.check = function (callback) { 95 | queryBuilds(callback); 96 | }; 97 | }; 98 | -------------------------------------------------------------------------------- /app/views/health.pug: -------------------------------------------------------------------------------- 1 | | 200 2 | -------------------------------------------------------------------------------- /app/views/index.pug: -------------------------------------------------------------------------------- 1 | extends layout.pug 2 | 3 | block scripts 4 | script(src='scripts/libs/jquery-2.1.0.min.js') 5 | script(src='scripts/libs/jquery.color-2.1.2.min.js') 6 | script(data-main="scripts/main", src='scripts/libs/require-2.1.15.min.js') 7 | 8 | block content 9 | .full-size(data-bind='component: { name: options.theme(), params: { builds: builds } }, changeFavicon: faviconImageUrl') 10 | 11 | include options.pug 12 | include overlay.pug 13 | -------------------------------------------------------------------------------- /app/views/layout.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | meta(http-equiv='X-UA-Compatible', content='IE=9; IE=8; IE=EDGE') 5 | meta(name='mobile-web-app-capable', content='yes') 6 | meta(name='viewport', content='width=device-width, height=device-height, initial-scale=0.3') 7 | 8 | title= title 9 | link(rel='stylesheet', href='stylesheets/base/style.css') 10 | link#basic-favicon(href='images/favicon.png', rel='icon') 11 | 12 | block scripts 13 | 14 | body.carbon-background 15 | block content 16 | -------------------------------------------------------------------------------- /app/views/options.pug: -------------------------------------------------------------------------------- 1 | div(data-bind='with: options') 2 | .options-button(data-bind='fade: isMenuButtonVisible') 3 | a(href='#', data-bind='click: show') 4 | span.fa.fa-bars 5 | 6 | .options(data-bind='fade: isMenuVisible, fadeOutDuration: 300', style='display: none') 7 | .background 8 | .content 9 | .header 10 | span.title Options 11 | span.back 12 | a(href='#', data-bind='click: hide') 13 | span.fa.fa-arrow-left 14 | hr 15 | 16 | section 17 | h1 Theme 18 | .description Select your preferred default theme, when you open the monitor without the theme url parameter. 19 | 20 | .selection-box 21 | ul(data-bind='foreach: themes') 22 | li(data-bind='css: { selected: $parent.theme() === $data }') 23 | a(data-bind='click: $parent.changeTheme') 24 | span(data-bind='text: $data') 25 | 26 | .clear 27 | 28 | section 29 | h1 Notification on build failure 30 | .description With enabled notifications you can leave the build monitor open in the background and never miss a failed build again. 31 | 32 | label.checkbox 33 | input(type='checkbox', data-bind='checked: soundNotificationEnabled') 34 | span Play sound 35 | 36 | label.checkbox(data-bind='visible: browserNotificationSupported') 37 | input(type='checkbox', data-bind='checked: browserNotificationEnabled') 38 | span Show browser notification 39 | 40 | div(data-bind='visible: !browserNotificationSupported()') 41 | small 42 | | Browser Notifications are not supported in your browser or were disabled by you. 43 | a(href='http://caniuse.com/#feat=notifications', target='_blank') More Info 44 | 45 | section 46 | h1 Notification filter 47 | .description You only will get notified by your set up notifications, if the notification filter regular expression matches a build parameter. 48 | 49 | label.checkbox 50 | input(type='checkbox', data-bind='checked: notificationFilterEnabled') 51 | span Notification filter 52 | 53 | input(type='text', data-bind='value: notificationFilterValue, enable: notificationFilterEnabled') 54 | span 55 | small 56 | a(href='https://regex101.com/#javascript', target='_blank') More Info 57 | 58 | span.footer 59 | span node-build-monitor 60 | span(data-bind='text: version') 61 | span ( 62 | a(href='http://marcells.github.io/node-build-monitor', target='_blank') Project page 63 | span ) 64 | -------------------------------------------------------------------------------- /app/views/overlay.pug: -------------------------------------------------------------------------------- 1 | .connection(data-bind='fade: isIntercepted') 2 | .overlay-background 3 | .overlay-icon 4 | span(data-bind='visible: infoType() === "loading"') 5 | span.fa.fa-5x.fa-circle-o-notch.fa-spin 6 | span(data-bind='visible: infoType() === "connection"', style='display: none') 7 | span.fa.fa-5x.fa-plug -------------------------------------------------------------------------------- /charts/README.md: -------------------------------------------------------------------------------- 1 | | Parameter | Description | Default | 2 | |----------------------------------------|---------------------------------------------|-----------------------------------------------------| 3 | | `image.pullPolicy` | Container pull policy | `IfNotPresent` | 4 | | `image.repository` | Container image to use | `marcells/node-build-monitor` | 5 | | `image.tag` | Container image tag to deploy | `latest` | 6 | | `replicaCount` | k8s replicas | `1` | 7 | | `resources` | Container resource | `{}` | 8 | | `nodeSelector` | Map of node labels for pod assignment | `{}` | 9 | | `tolerations` | List of node taints to tolerate | `[]` | 10 | | `affinity` | Map of node/pod affinities | `{}` | 11 | | `ingress.enabled` | Enable ingress | `false` | 12 | | `ingress.annotations` | Ingress annotations | `{}` | 13 | | `ingress.hosts` | Ingress Hostnames | `["build-monitor.local"]` | 14 | | `ingress.path` | Path within the URL structure | `/` | 15 | | `ingress.tls` | Ingress TLS configuration | `[]` | 16 | | `service.type` | Kubernetes Service type | `ClusterIP` | 17 | | `service.port` | Port | `80` | 18 | | `config` | Json config for node-build-monitor | `{}` | 19 | | `port` | Port of application | `3000` | 20 | | `rejectTls` | reject tls envirnment var | `1` | 21 | -------------------------------------------------------------------------------- /charts/build-monitor/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | -------------------------------------------------------------------------------- /charts/build-monitor/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: "1.0" 3 | description: A Helm chart for Kubernetes 4 | name: build-monitor 5 | version: 0.1.0 6 | -------------------------------------------------------------------------------- /charts/build-monitor/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range .Values.ingress.hosts }} 4 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ . }}{{ $.Values.ingress.path }} 5 | {{- end }} 6 | {{- else if contains "NodePort" .Values.service.type }} 7 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "build-monitor.fullname" . }}) 8 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 9 | echo http://$NODE_IP:$NODE_PORT 10 | {{- else if contains "LoadBalancer" .Values.service.type }} 11 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 12 | You can watch the status of by running 'kubectl get svc -w {{ include "build-monitor.fullname" . }}' 13 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "build-monitor.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') 14 | echo http://$SERVICE_IP:{{ .Values.service.port }} 15 | {{- else if contains "ClusterIP" .Values.service.type }} 16 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "build-monitor.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 17 | echo "Visit http://127.0.0.1:8080 to use your application" 18 | kubectl port-forward $POD_NAME 8080:80 19 | {{- end }} 20 | -------------------------------------------------------------------------------- /charts/build-monitor/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "build-monitor.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "build-monitor.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "build-monitor.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 32 | {{- end -}} 33 | -------------------------------------------------------------------------------- /charts/build-monitor/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: config-file 5 | data: 6 | config.json: {{ .Values.config | quote }} -------------------------------------------------------------------------------- /charts/build-monitor/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1beta2 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "build-monitor.fullname" . }} 5 | labels: 6 | app.kubernetes.io/name: {{ include "build-monitor.name" . }} 7 | helm.sh/chart: {{ include "build-monitor.chart" . }} 8 | app.kubernetes.io/instance: {{ .Release.Name }} 9 | app.kubernetes.io/managed-by: {{ .Release.Service }} 10 | spec: 11 | replicas: {{ .Values.replicaCount }} 12 | selector: 13 | matchLabels: 14 | app.kubernetes.io/name: {{ include "build-monitor.name" . }} 15 | app.kubernetes.io/instance: {{ .Release.Name }} 16 | template: 17 | metadata: 18 | labels: 19 | app.kubernetes.io/name: {{ include "build-monitor.name" . }} 20 | app.kubernetes.io/instance: {{ .Release.Name }} 21 | spec: 22 | containers: 23 | - name: {{ .Chart.Name }} 24 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" 25 | imagePullPolicy: {{ .Values.image.pullPolicy }} 26 | volumeMounts: 27 | - mountPath: /build-mon/app/config.json 28 | subPath: config.json 29 | name: config-volume 30 | env: 31 | - name: "PORT" 32 | value: {{ .Values.port | quote }} 33 | - name: "NODE_TLS_REJECT_UNAUTHORIZED" 34 | value: {{ .Values.rejectTls | quote }} 35 | ports: 36 | - name: http 37 | containerPort: 3000 38 | protocol: TCP 39 | livenessProbe: 40 | httpGet: 41 | path: / 42 | port: http 43 | readinessProbe: 44 | httpGet: 45 | path: / 46 | port: http 47 | resources: 48 | {{ toYaml .Values.resources | indent 12 }} 49 | volumes: 50 | - name: config-volume 51 | configMap: 52 | name: config-file 53 | {{- with .Values.nodeSelector }} 54 | nodeSelector: 55 | {{ toYaml . | indent 8 }} 56 | {{- end }} 57 | {{- with .Values.affinity }} 58 | affinity: 59 | {{ toYaml . | indent 8 }} 60 | {{- end }} 61 | {{- with .Values.tolerations }} 62 | tolerations: 63 | {{ toYaml . | indent 8 }} 64 | {{- end }} 65 | -------------------------------------------------------------------------------- /charts/build-monitor/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "build-monitor.fullname" . -}} 3 | {{- $ingressPath := .Values.ingress.path -}} 4 | apiVersion: extensions/v1beta1 5 | kind: Ingress 6 | metadata: 7 | name: {{ $fullName }} 8 | labels: 9 | app.kubernetes.io/name: {{ include "build-monitor.name" . }} 10 | helm.sh/chart: {{ include "build-monitor.chart" . }} 11 | app.kubernetes.io/instance: {{ .Release.Name }} 12 | app.kubernetes.io/managed-by: {{ .Release.Service }} 13 | {{- with .Values.ingress.annotations }} 14 | annotations: 15 | {{ toYaml . | indent 4 }} 16 | {{- end }} 17 | spec: 18 | {{- if .Values.ingress.tls }} 19 | tls: 20 | {{- range .Values.ingress.tls }} 21 | - hosts: 22 | {{- range .hosts }} 23 | - {{ . | quote }} 24 | {{- end }} 25 | secretName: {{ .secretName }} 26 | {{- end }} 27 | {{- end }} 28 | rules: 29 | {{- range .Values.ingress.hosts }} 30 | - host: {{ . | quote }} 31 | http: 32 | paths: 33 | - path: {{ $ingressPath }} 34 | backend: 35 | serviceName: {{ $fullName }} 36 | servicePort: http 37 | {{- end }} 38 | {{- end }} 39 | -------------------------------------------------------------------------------- /charts/build-monitor/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "build-monitor.fullname" . }} 5 | labels: 6 | app.kubernetes.io/name: {{ include "build-monitor.name" . }} 7 | helm.sh/chart: {{ include "build-monitor.chart" . }} 8 | app.kubernetes.io/instance: {{ .Release.Name }} 9 | app.kubernetes.io/managed-by: {{ .Release.Service }} 10 | spec: 11 | type: {{ .Values.service.type }} 12 | ports: 13 | - port: {{ .Values.service.port }} 14 | targetPort: http 15 | protocol: TCP 16 | name: http 17 | selector: 18 | app.kubernetes.io/name: {{ include "build-monitor.name" . }} 19 | app.kubernetes.io/instance: {{ .Release.Name }} 20 | -------------------------------------------------------------------------------- /charts/build-monitor/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for build-monitor. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | image: 8 | repository: marcells/node-build-monitor 9 | tag: latest 10 | pullPolicy: IfNotPresent 11 | 12 | nameOverride: 13 | fullnameOverride: 14 | 15 | # env var for port 16 | port: 3000 17 | 18 | # env var to reject tls 19 | rejectTls: 1 20 | 21 | # json config (config.json in app) 22 | config: | 23 | { 24 | "monitor": { 25 | "interval": 30000, 26 | "numberOfBuilds": 12, 27 | "latestBuildOnly": false, 28 | "sortOrder": "date", 29 | "errorsFirst": false, 30 | "expandEnvironmentVariables": false, 31 | "debug": true 32 | }, 33 | "services": [ 34 | { 35 | "name": "Travis", 36 | "configuration": { 37 | "slug": "node-build-monitor" 38 | } 39 | }, 40 | { 41 | "name": "Travis", 42 | "configuration": { 43 | "slug": "marcells/bloggy", 44 | "latestBuildOnly": true 45 | } 46 | } 47 | ] 48 | } 49 | 50 | service: 51 | type: ClusterIP 52 | port: 80 53 | 54 | ingress: 55 | enabled: false 56 | annotations: {} 57 | # kubernetes.io/ingress.class: nginx 58 | # kubernetes.io/tls-acme: true 59 | path: / 60 | hosts: 61 | - build-monitor.local 62 | tls: [] 63 | # - secretName: build-monitor-tls 64 | # hosts: 65 | # - build-monitor.local 66 | 67 | resources: {} 68 | # We usually recommend not to specify default resources and to leave this as a conscious 69 | # choice for the user. This also increases chances charts run on environments with little 70 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 71 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 72 | # limits: 73 | # cpu: 100m 74 | # memory: 128Mi 75 | # requests: 76 | # cpu: 100m 77 | # memory: 128Mi 78 | 79 | nodeSelector: {} 80 | 81 | tolerations: [] 82 | 83 | affinity: {} 84 | -------------------------------------------------------------------------------- /docker-ci/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM marcells/node-build-monitor -------------------------------------------------------------------------------- /docker-ci/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "monitor": { 3 | "interval": 300000, 4 | "numberOfBuilds": 12, 5 | "latestBuildOnly": false, 6 | "sortOrder": "date", 7 | "debug": false 8 | }, 9 | "services": [ 10 | { 11 | "name": "Travis", 12 | "configuration": { 13 | "slug": "marcells/bloggy" 14 | } 15 | }, 16 | { 17 | "name": "Travis", 18 | "configuration": { 19 | "slug": "marcells/node-build-monitor" 20 | } 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /docker/.gitignore: -------------------------------------------------------------------------------- 1 | config.json -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM marcells/node-build-monitor -------------------------------------------------------------------------------- /docker/config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "monitor": { 3 | "interval": 300000, 4 | "numberOfBuilds": 12, 5 | "latestBuildOnly": false, 6 | "sortOrder": "date", 7 | "debug": false 8 | }, 9 | "services": [ 10 | { 11 | "name": "Travis", 12 | "configuration": { 13 | "slug": "marcells/bloggy" 14 | } 15 | }, 16 | { 17 | "name": "Travis", 18 | "configuration": { 19 | "slug": "marcells/node-build-monitor" 20 | } 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /docker/docker-compose.with-self-signed-certs.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | node-build-monitor: 5 | restart: unless-stopped 6 | image: marcells/node-build-monitor 7 | build: . 8 | environment: 9 | NODE_ENV: production 10 | NODE_TLS_REJECT_UNAUTHORIZED: 0 11 | ports: 12 | - 3000:3000 -------------------------------------------------------------------------------- /docker/docker-compose.with-tfs-proxy.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | node-build-monitor: 5 | restart: unless-stopped 6 | image: marcells/node-build-monitor 7 | build: . 8 | links: 9 | - tfs-proxy 10 | environment: 11 | NODE_ENV: production 12 | ports: 13 | - 3000:3000 14 | 15 | tfs-proxy: 16 | restart: unless-stopped 17 | image: marcells/tfs-proxy 18 | container_name: tfs-proxy 19 | build: . -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | node-build-monitor: 5 | restart: unless-stopped 6 | image: marcells/node-build-monitor 7 | build: . 8 | environment: 9 | NODE_ENV: production 10 | ports: 11 | - 3000:3000 -------------------------------------------------------------------------------- /docs/node-build-monitor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcells/node-build-monitor/b13c156f7ac4df2559808642c9a408e4e0357f16/docs/node-build-monitor.png -------------------------------------------------------------------------------- /node-build-monitor.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "path": ".", 6 | "folder_exclude_patterns": [ "node_modules" ] 7 | } 8 | ] 9 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-build-monitor", 3 | "version": "0.9.59", 4 | "description": "A Build Monitor written in Node.js, which supports several build services and can be extended easily.", 5 | "author": "Marcell Spies ", 6 | "contributors": [ 7 | { 8 | "name": "Marcell Spies", 9 | "email": "marcells@gmx.de" 10 | }, 11 | { 12 | "name": "Ken Toley", 13 | "email": "kenneth.toley@dictionary.com" 14 | } 15 | ], 16 | "dependencies": { 17 | "async": "2.6.0", 18 | "errorhandler": "1.5.0", 19 | "express": "4.16.2", 20 | "graphql.js": "^0.4.20", 21 | "httpntlm": "^1.7.5", 22 | "moment": "2.21.0", 23 | "morgan": "^1.9.1", 24 | "pug": "^2.0.3", 25 | "request": "^2.88.0", 26 | "socket.io": "2.0.4", 27 | "striptags": "3.1.1", 28 | "xml2js": "0.4.19" 29 | }, 30 | "devDependencies": { 31 | "chai": "4.1.2", 32 | "chai-shallow-deep-equal": "^1.4.6", 33 | "grunt": "^1.0.4", 34 | "grunt-bump": "^0.8.0", 35 | "grunt-contrib-jshint": "1.1.0", 36 | "grunt-contrib-watch": "^1.1.0", 37 | "grunt-mocha-test": "0.13.3", 38 | "mocha": "5.0.4", 39 | "nock": "9.2.3", 40 | "rewire": "3.0.2", 41 | "should": "13.2.1", 42 | "sinon": "4.4.2", 43 | "sinon-chai": "3.0.0" 44 | }, 45 | "keywords": [], 46 | "repository": "git://github.com/marcells/node-build-monitor", 47 | "scripts": { 48 | "start": "node app/app.js", 49 | "ci": "grunt ci", 50 | "test": "grunt mochaTest:test", 51 | "pkg": "pkg -c ./package.json -t node14-linux,node14-macos,node14-win --out-dir ./release ./app/app.js" 52 | }, 53 | "engines": { 54 | "node": ">= 6.10.0" 55 | }, 56 | "pkg": { 57 | "scripts": [ 58 | "app/services/**/*.js", 59 | "node_modules/pug/register.js", 60 | "node_modules/pug/lib/**/*.js" 61 | ], 62 | "assets": [ 63 | "app/public/**/*", 64 | "app/views/**/*", 65 | "app/config.json" 66 | ] 67 | }, 68 | "license": "MIT" 69 | } 70 | -------------------------------------------------------------------------------- /test/fake.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | var self = this, 3 | clone = function (obj) { 4 | return JSON.parse(JSON.stringify(obj)); 5 | }; 6 | 7 | self.builds = []; 8 | 9 | self.check = function (callback) { 10 | callback(null, clone(self.builds)); 11 | }; 12 | 13 | self.add = function () { 14 | var build = { 15 | id: 'project_' + (self.builds.length + 1), 16 | project: 'project', 17 | number: 'number', 18 | isRunning: true, 19 | startedAt: new Date(2000, 0, 1), 20 | finishedAt: new Date(2000, 0, 1), 21 | requestedFor: 'author', 22 | status: 'status', 23 | statusText: 'statusText', 24 | reason: 'reason', 25 | hasErrors: true, 26 | hasWarnings: true 27 | }; 28 | 29 | self.builds.push(build); 30 | return build; 31 | }; 32 | 33 | self.addLater = function () { 34 | var build = self.add(); 35 | 36 | build.startedAt = new Date(2001, 0, 1); 37 | build.finishedAt = new Date(2001, 0, 2); 38 | 39 | return build; 40 | }; 41 | 42 | self.update = function (index) { 43 | self.builds[0].status = 'newStatus'; 44 | }; 45 | 46 | self.add(); 47 | 48 | return self; 49 | }; 50 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require should -------------------------------------------------------------------------------- /test/monitor.js: -------------------------------------------------------------------------------- 1 | describe('monitor', function () { 2 | var monitor; 3 | 4 | beforeEach(function () { 5 | monitor = new (require('../app/monitor'))(); 6 | 7 | monitor.configure({ 8 | interval: 1, 9 | numberOfBuilds: 3 10 | }); 11 | }); 12 | 13 | describe('a new monitor', function () { 14 | it('should not have plugins', function () { 15 | monitor.plugins.should.be.empty; 16 | }); 17 | 18 | it('should not display any builds', function () { 19 | monitor.currentBuilds.should.be.empty; 20 | }); 21 | }); 22 | 23 | describe('watching a plugin', function () { 24 | var fake; 25 | 26 | beforeEach(function () { 27 | fake = new (require('./fake'))(); 28 | monitor.watchOn(fake); 29 | }); 30 | 31 | it('should have one plugin', function () { 32 | monitor.plugins.should.have.lengthOf(1); 33 | }); 34 | 35 | describe('a running monitor', function () { 36 | it('should invoke the buildsChanged event', function (done) { 37 | monitor.once('buildsChanged', function (changes) { 38 | changes.added.should.have.lengthOf(1); 39 | changes.order.should.have.lengthOf(1); 40 | 41 | done(); 42 | }); 43 | 44 | monitor.run(); 45 | }); 46 | 47 | it('should invoke the buildsChanged event and contain an etag', function (done) { 48 | monitor.once('buildsChanged', function (changes) { 49 | changes.added[0].should.have.ownProperty('etag'); 50 | 51 | done(); 52 | }); 53 | 54 | monitor.run(); 55 | }); 56 | 57 | it('should invoke the buildsChanged two times, when a build is updated', function (done) { 58 | monitor.once('buildsChanged', function (firstChanges) { 59 | monitor.once('buildsChanged', function (secondChanges) { 60 | firstChanges.added.should.have.lengthOf(1); 61 | secondChanges.updated.should.have.lengthOf(1); 62 | secondChanges.order.should.have.lengthOf(1); 63 | 64 | done(); 65 | }); 66 | 67 | fake.update(0); 68 | }); 69 | 70 | monitor.run(); 71 | }); 72 | 73 | it('should invoke the buildsChanged two times, when a build is added', function (done) { 74 | monitor.once('buildsChanged', function (firstChanges) { 75 | monitor.once('buildsChanged', function (secondChanges) { 76 | firstChanges.added.should.have.lengthOf(1); 77 | secondChanges.added.should.have.lengthOf(1); 78 | secondChanges.order.should.eql(['project_2', 'project_1']); 79 | 80 | done(); 81 | }); 82 | 83 | fake.addLater(); 84 | }); 85 | 86 | monitor.run(); 87 | }); 88 | }); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /test/services/BuddyBuild.js: -------------------------------------------------------------------------------- 1 | describe('BuddyBuild service', function () { 2 | var BB; // class instance 3 | 4 | function assert(expr, msg) { 5 | if (!expr) throw new Error(msg || 'failed'); 6 | } 7 | 8 | beforeEach(function () { 9 | BB = new ( require('../../app/services/BuddyBuild'))(); 10 | BB.configure({ 11 | interval: 1, 12 | numberOfBuilds: 3 13 | }); 14 | 15 | }); 16 | 17 | describe('getStatus', function (){ 18 | "use strict"; 19 | it('success should return color Green', function(){ 20 | var result = BB.getStatus('success'); 21 | assert(result == 'Green', "Expecting Green Got: " + result ); 22 | }); 23 | 24 | it('failed should return color Red', function(){ 25 | var result = BB.getStatus('failed'); 26 | assert(result == 'Red', "Expecting Red Got: " + result); 27 | }); 28 | 29 | it('running should return color Blue', function(){ 30 | var result = BB.getStatus('running'); 31 | assert(result == 'Blue', "Expecting Blue Got: " + result); 32 | }); 33 | 34 | it('canceled should return color Orange #FFA500', function(){ 35 | var result = BB.getStatus('canceled'); 36 | assert(result == '#FFA500', "Expecting FFA500 Got: " + result); 37 | }); 38 | 39 | it('queued should return color Blue', function(){ 40 | var result = BB.getStatus('queued'); 41 | assert(result == 'Blue', "Expecting Blue Got: " + result); 42 | }); 43 | 44 | it('should return default color Gray', function(){ 45 | var result = BB.getStatus('default'); 46 | assert(result == 'Gray', "Expecting Gray Got: " + result); 47 | }); 48 | }); 49 | 50 | describe('makeUrl', function () { 51 | it('should return valid url to the latest build of the specified branch', function () { 52 | var url = "https://buddybuild.com/APPID/build/latest?branch=develop"; 53 | var sampleParam = BB.makeURL('APPID', '', 'develop', "https://buddybuild.com"); 54 | assert(url === sampleParam, "Expecting Format: " + url + " Received: " + sampleParam); 55 | }); 56 | 57 | it('should return valid url to specified build', function () { 58 | var url = "https://buddybuild.com/BUILDID"; 59 | var sampleParam = BB.makeURL('', 'BUILDID', '', "https://buddybuild.com"); 60 | assert(url === sampleParam, "Expecting Format: " + url + " Received: " + sampleParam); 61 | }); 62 | 63 | it('should return valid url to all given token when app_is is not given', function () { 64 | var url = "https://api.buddybuild.com/v1/apps"; 65 | var sampleParam = BB.makeURL('', '', '', "https://api.buddybuild.com/v1/apps"); 66 | assert(url === sampleParam, "Expecting Format: " + url + " Received: " + sampleParam); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/services/Buildkite.js: -------------------------------------------------------------------------------- 1 | const chai = require("chai"), 2 | expect = chai.expect, 3 | sinon = require("sinon"), 4 | sinonChai = require("sinon-chai"), 5 | rewire = require("rewire"), 6 | path = require("path"); 7 | 8 | const graphql = require("graphql.js"); 9 | const Buildkite = rewire("../../app/services/Buildkite"); 10 | 11 | process.env.BUILDKITE_TOKEN = "FAKE_BUILDKITE_TOKEN"; 12 | chai.use(sinonChai); 13 | 14 | describe("Buildkite service", function() { 15 | const buildkiteReturnData = noCreatedByUserData(); 16 | let buildkite; 17 | 18 | // Scheduled builds no longer have a created by user set. 19 | context("Should process builds without a created by user", function() { 20 | this.beforeAll(function() { 21 | const graphqlStub = sinon.stub().callsFake(() => { 22 | return { 23 | query: () => new Promise(resolve => resolve(buildkiteReturnData)) 24 | }; 25 | }); 26 | 27 | Buildkite.__set__("graphql", graphqlStub); 28 | buildkite = Buildkite(); 29 | buildkite.configure({ 30 | orgSlug: "org_slug", 31 | teamSlug: "everyone" 32 | }); 33 | }); 34 | 35 | it("should return a null 'requestedFor' for the build without a createdBy User", function(done) { 36 | buildkite.check((_, results) => { 37 | try { 38 | expect(results[0].requestedFor).to.be.equal("John Doe"); 39 | expect(results[1].requestedFor).to.be.null; 40 | done(); 41 | } catch (e) { 42 | done(e); 43 | } 44 | }); 45 | }); 46 | }); 47 | }); 48 | 49 | //Some fake buildkite data to test builds that do not have a created by user 50 | function noCreatedByUserData() { 51 | return { 52 | organization: { 53 | name: "Org name", 54 | pipelines: { 55 | edges: [ 56 | { 57 | node: { 58 | id: "RANDOMONID_1", 59 | name: "pipeline name 1", 60 | slug: "pipeline_slug_1", 61 | builds: { 62 | edges: [ 63 | { 64 | node: { 65 | id: "RANDOMONID_2", 66 | branch: "master", 67 | message: ":sparkles: linting", 68 | number: 7, 69 | state: "PASSED", 70 | startedAt: "2019-10-29T22:30:43Z", 71 | finishedAt: "2019-10-29T22:44:34Z", 72 | url: 73 | "https://buildkite.com/org_slug/pipeline_slug_1/builds/7", 74 | createdBy: { 75 | name: "John Doe" 76 | } 77 | } 78 | } 79 | ] 80 | } 81 | } 82 | }, 83 | { 84 | node: { 85 | id: "RANDOMONID_3", 86 | name: "pipeline name 2", 87 | slug: "pipeline_slug_2", 88 | builds: { 89 | edges: [ 90 | { 91 | node: { 92 | id: "RANDOMONID_4", 93 | branch: "master", 94 | message: ":rocket: deployment sceripts", 95 | number: 170, 96 | state: "PASSED", 97 | startedAt: "2019-11-03T20:44:06Z", 98 | finishedAt: "2019-11-03T20:56:53Z", 99 | url: 100 | "https://buildkite.com/org_slug/trust-pipeline_slug_2/builds/170", 101 | createdBy: null 102 | } 103 | } 104 | ] 105 | } 106 | } 107 | } 108 | ] 109 | } 110 | } 111 | }; 112 | } 113 | -------------------------------------------------------------------------------- /test/services/GitLab.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'), 2 | expect = chai.expect, 3 | sinon = require('sinon'), 4 | sinonChai = require('sinon-chai'), 5 | rewire = require('rewire'), 6 | path = require('path'); 7 | 8 | chai.use(sinonChai); 9 | 10 | describe('GitLab service', function () { 11 | context('Custom root CA path', function () { 12 | 13 | var gitlab, requestStub; 14 | 15 | beforeEach(function () { 16 | // Set path to dummy CA cert 17 | this.caPath = path.resolve(__dirname, 'data', 'ca.pem'); 18 | 19 | // Stub out request module in GitLab module 20 | requestStub = { 21 | defaults: sinon.stub() 22 | }; 23 | var gitlabModule = rewire('../../app/services/GitLab'); 24 | gitlabModule.__set__('request', requestStub); 25 | 26 | gitlab = new gitlabModule(); 27 | }); 28 | 29 | it('should set the default CA in the request module', function () { 30 | var expectedCert = require('fs').readFileSync(this.caPath).toString().split("\n\n"); 31 | 32 | gitlab.configure({ 33 | caPath: this.caPath 34 | }); 35 | 36 | expect(requestStub.defaults).to.have.been.calledWithExactly({ 37 | agentOptions: { 38 | ca: expectedCert 39 | } 40 | }); 41 | }); 42 | }); 43 | 44 | context('Calculate state', function () { 45 | 46 | var gitlab; 47 | beforeEach(function () { 48 | var gitlabModule = rewire('../../app/services/GitLab'); 49 | gitlab = new gitlabModule(); 50 | }); 51 | 52 | it('Calculate state', function (done) { 53 | var nock = require('nock'); 54 | 55 | nock('http://foo') 56 | .get('/api/v4/projects?page=1&per_page=100').times(2) 57 | .reply('200', { 58 | "id": 4, 59 | "description": null, 60 | "default_branch": "master", 61 | "ssh_url_to_repo": "git@example.com:diaspora/diaspora-client.git", 62 | "http_url_to_repo": "http://example.com/diaspora/diaspora-client.git", 63 | "web_url": "http://example.com/diaspora/diaspora-client", 64 | "readme_url": "http://example.com/diaspora/diaspora-client/blob/master/README.md", 65 | "tag_list": [ 66 | "example", 67 | "disapora client" 68 | ], 69 | "namespace": { 70 | "id": 3, 71 | "name": "Diaspora", 72 | "path": "diaspora", 73 | "kind": "group", 74 | "full_path": "diaspora" 75 | }, 76 | "name": "Diaspora Client", 77 | "name_with_namespace": "Diaspora / Diaspora Client", 78 | "path": "diaspora-client", 79 | "path_with_namespace": "diaspora/diaspora-client", 80 | "created_at": "2013-09-30T13:46:02Z", 81 | "last_activity_at": "2013-09-30T13:46:02Z", 82 | "forks_count": 0, 83 | "avatar_url": "http://example.com/uploads/project/avatar/4/uploads/avatar.png", 84 | "star_count": 0, 85 | }, {"x-total-pages": "1"}); 86 | 87 | nock("http://foo").get("/api/v4/projects/4/pipelines").reply(200, [{ 88 | "id": 47, 89 | "status": "pending", 90 | "ref": "new-pipeline", 91 | "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a", 92 | "web_url": "https://example.com/foo/bar/pipelines/47", 93 | "created_at": "2016-08-11T11:28:34.085Z", 94 | "updated_at": "2016-08-11T11:32:35.169Z", 95 | }]); 96 | 97 | nock("http://foo").get("/api/v4/projects/4/pipelines/47").reply(200, { 98 | "id": 47, 99 | "status": "success", 100 | "ref": "new-pipeline", 101 | "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a", 102 | "web_url": "https://example.com/foo/bar/pipelines/47", 103 | "created_at": "2016-08-11T11:28:34.085Z", 104 | "updated_at": "2016-08-11T11:32:35.169Z", 105 | "detailed_status": { 106 | "icon": "status_warning", 107 | "text": "passed", 108 | "label": "passed with warnings", 109 | "group": "success-with-warnings", 110 | "tooltip": "passed", 111 | "has_details": false, 112 | "details_path": "/replaceWithFOooByMe/pipelines/1234567", 113 | "illustration": null, 114 | "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png" 115 | } 116 | }); 117 | 118 | nock("http://foo").get("/api/v4/projects/4/pipelines/47/jobs/").reply(200, { 119 | "id": 47, 120 | "status": "pending", 121 | "ref": "new-pipeline", 122 | "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a", 123 | "web_url": "https://example.com/foo/bar/pipelines/47", 124 | "created_at": "2016-08-11T11:28:34.085Z", 125 | "updated_at": "2016-08-11T11:32:35.169Z", 126 | }); 127 | 128 | gitlab.configure({"slugs": [{"project": "diaspora/diaspora-client"}], "url": "http://foo"}); 129 | 130 | gitlab.check(function (err, builds) { 131 | expect(builds.length).to.equal(1); 132 | expect(builds[0].hasWarnings).to.be.true; 133 | expect(builds[0].status).to.equal("#ffa500"); 134 | 135 | done(); 136 | }); 137 | }); 138 | }); 139 | }); 140 | -------------------------------------------------------------------------------- /test/services/Jenkins/Jenkins.js: -------------------------------------------------------------------------------- 1 | var should = require('should'); 2 | var scenario_1 = require('./scenario_1'); 3 | var scenario_2 = require('./scenario_2'); 4 | var scenario_3 = require('./scenario_3'); 5 | 6 | describe('Jenkins service', function () { 7 | var service, nock; 8 | 9 | beforeEach(function () { 10 | service = new (require('../../../app/services/Jenkins'))(); 11 | service.configure(scenario_1.configuration); 12 | }); 13 | 14 | afterEach(function () { 15 | nock.cleanAll(); 16 | }); 17 | 18 | describe('builds are checked successfully', function () { 19 | before(function() { 20 | nock = scenario_1.setup(); 21 | }); 22 | 23 | it('should return the valid builds', function (done) { 24 | service.check(function(error, builds) { 25 | if (error) { 26 | done(error); 27 | return; 28 | } 29 | 30 | builds.should.eql(scenario_1.expected); 31 | 32 | done(); 33 | }); 34 | }); 35 | }); 36 | 37 | describe('a problem occurs while checking the builds', function () { 38 | before(function() { 39 | nock = scenario_2.setup(); 40 | }); 41 | 42 | it('should handle the request error', function (done) { 43 | service.check(function(error, builds) { 44 | should.not.exist(builds); 45 | error.should.eql({ message: 'unexpected error', code: 'UNEXPECTED_ERROR' }); 46 | 47 | done(); 48 | }); 49 | }); 50 | }); 51 | 52 | describe('invalid JSON is returned in the request body', function () { 53 | before(function() { 54 | nock = scenario_3.setup(); 55 | }); 56 | 57 | it('should handle the jenkins-api error', function (done) { 58 | service.check(function(error, builds) { 59 | builds.should.eql([ undefined ]); 60 | error.should.eql(new SyntaxError('Unexpected token I in JSON at position 0')); 61 | 62 | done(); 63 | }); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /test/services/Jenkins/scenario_2.js: -------------------------------------------------------------------------------- 1 | exports.configuration = { 2 | "url": "http://dev.jazzteam.org:8080", 3 | "username": "x2sdemo", 4 | "password": "x2sdemo", 5 | "job": "xml2selenium-bestpractices" 6 | }; 7 | 8 | exports.setup = function () { 9 | var nock = require('nock'); 10 | 11 | nock('http://dev.jazzteam.org:8080') 12 | .get('/job/xml2selenium-bestpractices/api/json') 13 | .replyWithError({'message': 'unexpected error', 'code': 'UNEXPECTED_ERROR'}); 14 | 15 | return nock; 16 | } 17 | -------------------------------------------------------------------------------- /test/services/Jenkins/scenario_3.js: -------------------------------------------------------------------------------- 1 | var job_info_result = { actions: [ null, {}, {} ], 2 | description: 'This job run bestpractices testcases.
\r\n\r\nrepo [https://dev.jazzteam.org/projects/xml2selenium-bestpractices]
', 3 | displayName: 'xml2selenium-bestpractices', 4 | displayNameOrNull: null, 5 | name: 'xml2selenium-bestpractices', 6 | url: 'http://dev.jazzteam.org:8080/job/xml2selenium-bestpractices/', 7 | buildable: true, 8 | builds: 9 | [ { number: 338, 10 | url: 'http://dev.jazzteam.org:8080/job/xml2selenium-bestpractices/338/' }, 11 | { number: 337, 12 | url: 'http://dev.jazzteam.org:8080/job/xml2selenium-bestpractices/337/' } ], 13 | color: 'blue', 14 | firstBuild: 15 | { number: 319, 16 | url: 'http://dev.jazzteam.org:8080/job/xml2selenium-bestpractices/319/' }, 17 | healthReport: 18 | [ { description: 'Test Result: 0 tests failing out of a total of 101 tests.', 19 | iconClassName: 'icon-health-80plus', 20 | iconUrl: 'health-80plus.png', 21 | score: 100 }, 22 | { description: 'Build stability: No recent builds failed.', 23 | iconClassName: 'icon-health-80plus', 24 | iconUrl: 'health-80plus.png', 25 | score: 100 } ], 26 | inQueue: false, 27 | keepDependencies: false, 28 | lastBuild: { number: 338, url: 'http://dev.jazzteam.org:8080/job/xml2selenium-bestpractices/338/' }, 29 | lastCompletedBuild: { number: 338, url: 'http://dev.jazzteam.org:8080/job/xml2selenium-bestpractices/338/' }, 30 | lastFailedBuild: null, 31 | lastStableBuild: { number: 338, url: 'http://dev.jazzteam.org:8080/job/xml2selenium-bestpractices/338/' }, 32 | lastSuccessfulBuild: { number: 338, url: 'http://dev.jazzteam.org:8080/job/xml2selenium-bestpractices/338/' }, 33 | lastUnstableBuild: { number: 337, url: 'http://dev.jazzteam.org:8080/job/xml2selenium-bestpractices/337/' }, 34 | lastUnsuccessfulBuild: { number: 337, url: 'http://dev.jazzteam.org:8080/job/xml2selenium-bestpractices/337/' }, 35 | nextBuildNumber: 339, 36 | property: [ {} ], 37 | queueItem: null, 38 | concurrentBuild: false, 39 | downstreamProjects: [], 40 | scm: {}, 41 | upstreamProjects: [] }; 42 | 43 | var build_info_337_result = "Illegal JSON data is returned"; 44 | var build_info_338_result = "Illegal JSON data is returned"; 45 | 46 | exports.configuration = { 47 | "url": "http://dev.jazzteam.org:8080", 48 | "username": "x2sdemo", 49 | "password": "x2sdemo", 50 | "job": "xml2selenium-bestpractices" 51 | }; 52 | 53 | exports.setup = function () { 54 | var nock = require('nock'); 55 | 56 | nock('http://dev.jazzteam.org:8080') 57 | .get('/job/xml2selenium-bestpractices/api/json') 58 | .reply('200', job_info_result); 59 | 60 | nock('http://dev.jazzteam.org:8080') 61 | .get('/job/xml2selenium-bestpractices/338/api/json') 62 | .reply('200', build_info_338_result); 63 | 64 | nock('http://dev.jazzteam.org:8080') 65 | .get('/job/xml2selenium-bestpractices/337/api/json') 66 | .reply('200', build_info_337_result); 67 | 68 | return nock; 69 | } 70 | -------------------------------------------------------------------------------- /test/services/Travis.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'), 2 | expect = chai.expect, 3 | sinon = require('sinon'), 4 | sinonChai = require('sinon-chai'), 5 | rewire = require('rewire'), 6 | path = require('path'); 7 | 8 | chai.use(sinonChai); 9 | 10 | describe('Travis service', function () { 11 | context('Custom root CA path', function () { 12 | 13 | var travis, requestStub; 14 | 15 | beforeEach(function () { 16 | // Set path to dummy CA cert 17 | this.caPath = path.resolve(__dirname, 'data', 'ca.pem'); 18 | 19 | // Stub out request module in Travis module 20 | requestStub = { 21 | defaults: sinon.stub() 22 | }; 23 | var travisModule = rewire('../../app/services/Travis'); 24 | travisModule.__set__('request', requestStub); 25 | 26 | travis = new travisModule(); 27 | }); 28 | 29 | it('should set the default CA in the request module', function () { 30 | var expectedCert = require('fs').readFileSync(this.caPath).toString().split("\n\n"); 31 | 32 | travis.configure({ 33 | caPath: this.caPath 34 | }); 35 | 36 | expect(requestStub.defaults).to.have.been.calledWithExactly({ 37 | agentOptions: { 38 | ca: expectedCert 39 | } 40 | }); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /test/services/data/ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN TRUSTED CERTIFICATE----- 2 | MIICATCCAWoCCQDPjZNZljAK6jANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJB 3 | VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 4 | cyBQdHkgTHRkMB4XDTE2MDgxNzE0NDU1NVoXDTE3MDgxNzE0NDU1NVowRTELMAkG 5 | A1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0 6 | IFdpZGdpdHMgUHR5IEx0ZDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwfoo 7 | mwCHSzc+gE2Dfzq49rMZOUqZ8t3ddXXnGCYVjDiTPvkGt782ZrD1VpGFMFS0NOH8 8 | KXaDYkT0RMPS7/aNfBD180jgPWI8nZqfoEABcw1XFKO+B/xIkHMhh263QURVZXUr 9 | bJgqoBpH/qKvlqSYZuQ9rVb3WgT2PugLRQmhh1cCAwEAATANBgkqhkiG9w0BAQUF 10 | AAOBgQBM2qMDWQEZPuRY+mqyn1DlM1ABPWR+IS0eZt2izZqdXUNLU85DFk6VuUbJ 11 | KwjVyx5AwWNnPf/W0uUKA6avjlqYDTki9MEnjcHcF5fSh5QDL6R1rAHZZtp5OWW7 12 | fvV4kAfPp9oyJ6fcUbCIPI5sc78SDucNPP4Q4IZIIPqq6DsTkQ== 13 | -----END TRUSTED CERTIFICATE----- 14 | --------------------------------------------------------------------------------