├── db └── .gitkeep ├── .dockerignore ├── bin └── images │ ├── logo.png │ ├── webhook.png │ ├── frontend.png │ ├── build-frontend.png │ ├── build-scheduled.png │ ├── notification-ses.png │ ├── notification-github.png │ └── notification-slack.png ├── src ├── client │ ├── lib │ │ └── alt.js │ ├── index.html │ ├── components │ │ ├── header.jsx │ │ ├── buildHistory.jsx │ │ ├── project.jsx │ │ ├── projectsListContainer.jsx │ │ ├── buildOutput.jsx │ │ └── projectDetails.jsx │ ├── stores │ │ └── store.js │ ├── package.json │ ├── utils │ │ └── utils.js │ ├── styles │ │ └── App.scss │ ├── actions │ │ └── actions.js │ ├── webpack.config.js │ └── app.jsx ├── dispatcher.js ├── logger.js ├── router.js ├── notifications │ ├── github.js │ ├── slack.js │ └── emailSes.js ├── index.js ├── socket.js ├── config.js ├── matching.js ├── publisher.js ├── auth.js ├── tar.js ├── hooks.js ├── utils.js ├── notifications.js ├── storage.js ├── git.js ├── storage │ └── file.js ├── publisher │ └── s3.js ├── github.js ├── routes.js ├── docker.js └── builder.js ├── .gitignore ├── docker-compose.yml ├── config.example.yml ├── Dockerfile ├── config └── base.yml ├── examples └── auth │ └── local.js ├── .jshintrc ├── package.json └── README.md /db/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .git/ 3 | registry 4 | src/client/node_modules/ 5 | -------------------------------------------------------------------------------- /bin/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/namshi/roger/HEAD/bin/images/logo.png -------------------------------------------------------------------------------- /bin/images/webhook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/namshi/roger/HEAD/bin/images/webhook.png -------------------------------------------------------------------------------- /bin/images/frontend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/namshi/roger/HEAD/bin/images/frontend.png -------------------------------------------------------------------------------- /src/client/lib/alt.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Alt from 'alt'; 4 | export default new Alt(); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | db/* 2 | !db/.gitkeep 3 | node_modules 4 | registry 5 | .idea 6 | dist 7 | config.yml 8 | -------------------------------------------------------------------------------- /bin/images/build-frontend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/namshi/roger/HEAD/bin/images/build-frontend.png -------------------------------------------------------------------------------- /bin/images/build-scheduled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/namshi/roger/HEAD/bin/images/build-scheduled.png -------------------------------------------------------------------------------- /bin/images/notification-ses.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/namshi/roger/HEAD/bin/images/notification-ses.png -------------------------------------------------------------------------------- /bin/images/notification-github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/namshi/roger/HEAD/bin/images/notification-github.png -------------------------------------------------------------------------------- /bin/images/notification-slack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/namshi/roger/HEAD/bin/images/notification-slack.png -------------------------------------------------------------------------------- /src/dispatcher.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var EventEmitter = require('events').EventEmitter; 3 | var dispatcher = new EventEmitter; 4 | 5 | module.exports = dispatcher; -------------------------------------------------------------------------------- /src/logger.js: -------------------------------------------------------------------------------- 1 | var winston = require('winston'); 2 | 3 | /** 4 | * #dafuq 5 | */ 6 | winston.remove(winston.transports.Console); 7 | winston.add(winston.transports.Console, {timestamp: true}); 8 | 9 | module.exports = winston; 10 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | var config = require('./config'); 2 | var router = {}; 3 | 4 | /** 5 | * Generates a URL with the given options. 6 | */ 7 | router.generate = function(route, options, absolute) { 8 | return (absolute ? config.get('app.url') : '') + config.get('routes.' + route, options); 9 | }; 10 | 11 | module.exports = router; -------------------------------------------------------------------------------- /src/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Roger App 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | server: 2 | build: . 3 | ports: 4 | - "8080:8080" 5 | volumes: 6 | - .:/src 7 | - /var/run/docker.sock:/tmp/docker.sock 8 | - ./config.yml:/config.yml 9 | - ./examples/auth:/auth 10 | - ./db:/db 11 | - ./logs:/tmp/roger-builds/logs 12 | command: nodemon /src/src/index.js -- --config /config.yml 13 | environment: 14 | VIRTUAL_HOST: roger.swag 15 | registry: 16 | image: registry:2 17 | volumes: 18 | - ./registry:/tmp/registry 19 | ports: 20 | - "5000:5000" 21 | -------------------------------------------------------------------------------- /config.example.yml: -------------------------------------------------------------------------------- 1 | auth: 2 | dockerhub: # these credentials are only useful if you need to push to the dockerhub 3 | username: user # your username on the dockerhub 4 | email: someone@gmail.com # your...well, you get it 5 | password: YOUR_DOCKERHUB_PASSWORD --> see https://github.com/namshi/roger#sensitive-data 6 | github: YOUR_GITHUB_TOKEN # General token to be used to authenticate to clone any project or comment on PRs (https://github.com/settings/tokens/new) 7 | slack: 8 | url: 'https://slack-url-with-token.com' 9 | -------------------------------------------------------------------------------- /src/notifications/github.js: -------------------------------------------------------------------------------- 1 | var github = require('./../github'); 2 | var config = require('./../config'); 3 | 4 | /** 5 | * Creates a github status. 6 | */ 7 | module.exports = function(project, options) { 8 | options.logger.info('[%s] Creating github status for build %s', options.buildId, options.uuid); 9 | 10 | var parts = project.repo.split('/'); 11 | options.repo = parts.pop(); 12 | options.user = parts.pop(); 13 | options.token = config.get('notifications.github.token'); 14 | github.createStatus(options); 15 | }; 16 | -------------------------------------------------------------------------------- /src/client/components/header.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | 5 | class Header extends React.Component { 6 | render() { 7 | return ( 8 | 20 | ) 21 | } 22 | } 23 | 24 | export default Header; 25 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var logger = require('./logger'); 3 | var config = require('./config'); 4 | var routes = require('./routes'); 5 | var utils = require('./utils'); 6 | var socket = require('./socket'); 7 | var auth = require('./auth'); 8 | 9 | /** 10 | * Print the config while booting, 11 | * but omit the sensitive stuff. 12 | */ 13 | logger.info('using config:', JSON.stringify(utils.obfuscate(config.get()))); 14 | 15 | /** 16 | * Register the routes. 17 | */ 18 | var app = express(); 19 | 20 | if (config.get('app.auth')) { 21 | auth.enable(app); 22 | } 23 | 24 | routes.bind(app); 25 | 26 | /** 27 | * Start the fun! 28 | * 29 | * #swag 30 | */ 31 | var port = 8080; 32 | var server = app.listen(port); 33 | // enable websockets support 34 | socket(server); 35 | console.log('Roger running on port', port); 36 | -------------------------------------------------------------------------------- /src/client/stores/store.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import alt from '../lib/alt'; 4 | import actions from '../actions/actions'; 5 | import _ from 'lodash'; 6 | import Utility from '../utils/utils'; 7 | 8 | class Store { 9 | constructor() { 10 | this.projects = []; 11 | this.builds = []; 12 | this.bindAction(actions.updateData, this.updateData); 13 | this.bindAction(actions.updateBuildInProgress, this.updateBuildInProgress); 14 | } 15 | 16 | updateData(data) { 17 | this.projects = data.projects; 18 | this.builds = data.builds; 19 | } 20 | 21 | updateBuildInProgress(projectName) { 22 | let project = _.find(this.projects, function (project) { 23 | return Utility.getProjectUrl(project.name) === projectName; 24 | }); 25 | 26 | project && (project.buildInProgress = true); 27 | } 28 | 29 | } 30 | 31 | export default alt.createStore(Store); 32 | -------------------------------------------------------------------------------- /src/socket.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | var growingFile = require('growing-file'); 5 | var dispatcher = require('./dispatcher'); 6 | var utils = require('./utils'); 7 | var config = require('./config'); 8 | 9 | module.exports = function (server) { 10 | var io = require('socket.io')(server); 11 | var ss = require('socket.io-stream'); 12 | 13 | io.on('connection', function (socket) { 14 | console.log('websocket connected'); 15 | 16 | io.origins((origin, callback) => { 17 | if (origin !== config.get('app.url') + '/') { 18 | return callback('origin not allowed', false); 19 | } 20 | callback(null, true); 21 | }); 22 | 23 | /* When the projects db updated*/ 24 | dispatcher.on('storage-updated', function() { 25 | socket.emit('fetch-data'); 26 | }); 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | var reconfig = require('reconfig'); 2 | var yaml = require('js-yaml'); 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var argv = require('yargs').argv; 6 | var _ = require('lodash'); 7 | var url = require('url'); 8 | var logger = require('./logger'); 9 | var userConfig = {}; 10 | 11 | try { 12 | var userConfig = yaml.safeLoad(fs.readFileSync(path.join(argv.config), 'utf8')); 13 | } catch (err) { 14 | logger.info('Unable to find config file, proceeding with a bare minimum configuration'); 15 | logger.info('You might want to fix this unless you are passing config values through environment variables (ROGER_CONFIG_...)'); 16 | } 17 | 18 | var config = yaml.safeLoad(fs.readFileSync(path.join(__dirname, '..', 'config', 'base.yml'), 'utf8')); 19 | 20 | _.assign(config, userConfig); 21 | 22 | module.exports = new reconfig(config, 'ROGER_CONFIG'); 23 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8 2 | 3 | MAINTAINER Alessandro Nadalin "alessandro.nadalin@gmail.com" 4 | 5 | # dev deps 6 | RUN npm install -g nodemon \ 7 | apt-get update && \ 8 | apt-get install -y git && \ 9 | apt-get clean && \ 10 | rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \ 11 | find /var/log -type f | while read f; do echo -ne '' > $f; done; \ 12 | mkdir /tmp/roger-builds \ 13 | && mkdir /tmp/roger-builds/logs \ 14 | && mkdir /tmp/roger-builds/tars \ 15 | && mkdir /tmp/roger-builds/sources 16 | 17 | COPY ./src/client/package.json /src/src/client/ 18 | COPY ./src/client/package-lock.json /src/src/client/ 19 | WORKDIR /src/src/client 20 | RUN npm install 21 | 22 | COPY ./package.json /src/ 23 | COPY ./package-lock.json /src/ 24 | WORKDIR /src 25 | RUN npm install 26 | 27 | COPY . /src 28 | 29 | WORKDIR /src/src/client 30 | RUN npm run build 31 | 32 | COPY ./db /db 33 | 34 | WORKDIR /src 35 | 36 | EXPOSE 8080 37 | CMD ["node", "src/index.js", "--config", "/config.yml"] 38 | -------------------------------------------------------------------------------- /config/base.yml: -------------------------------------------------------------------------------- 1 | app: 2 | url: 'http://localhost:8080' 3 | storage: file 4 | builds: 5 | concurrent: 5 # max number of builds to run in parallel 6 | retry-after: 20 # interval, in seconds, for Roger to check whether it can start queued builds 7 | routes: 8 | config: '/api/config' 9 | projects: '/api/projects' 10 | build-project: '/api/build' 11 | builds: '/api/builds' 12 | build: '/api/builds/:build' 13 | build-log: '/api/builds/:build/log' 14 | build-link: '/#/projects/:projectName/:build' 15 | github-hooks: '/public/api/hooks/github' 16 | paths: 17 | builds: '/tmp/roger-builds/' 18 | sources: '{{ paths.builds }}/sources' 19 | tars: '{{ paths.builds }}/tars' 20 | logs: '{{ paths.builds }}/logs' 21 | docker: 22 | client: 23 | socketPath: '/tmp/docker.sock' 24 | notifications: 25 | github: 26 | global: 'false' 27 | token: '{{ auth.github }}' 28 | slack: 29 | global: 'true' 30 | channel: '#builds' 31 | icon_emoji: ':robot_face:' 32 | username: 'Roger' 33 | -------------------------------------------------------------------------------- /src/notifications/slack.js: -------------------------------------------------------------------------------- 1 | var config = require('./../config'); 2 | var slack = require('slack-notify')(config.get('auth.slack.url')); 3 | 4 | /** 5 | * Triggers a slack notification 6 | */ 7 | 8 | module.exports = function (project, options) { 9 | options.logger.info('[%s] Notifying slack of build %s', options.buildId, options.uuid); 10 | 11 | var parts = project.repo.split('/'); 12 | options.repo = parts.pop(); 13 | options.user = parts.pop(); 14 | 15 | if (options.result instanceof Error) { 16 | var color = '#d00000'; 17 | } else { 18 | var color = '#36a64f'; 19 | } 20 | 21 | slack.send({ 22 | icon_emoji: config.get('notifications.slack.icon_emoji'), 23 | username: config.get('notifications.slack.username'), 24 | channel: config.get('notifications.slack.channel'), 25 | attachments: [{ 26 | color: color, 27 | author_name: options.user, 28 | text: options.comments.join("\n\n") 29 | }] 30 | }); 31 | }; 32 | 33 | slack.onError = function (err) { 34 | console.log('SLACK API error:', err); 35 | }; 36 | -------------------------------------------------------------------------------- /src/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "roger-client", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "NODE_ENV=production webpack --progress --profile --colors", 8 | "dev": "webpack-dev-server --host 0.0.0.0 --progress --profile --colors --hot" 9 | }, 10 | "license": "ISC", 11 | "dependencies": { 12 | "alt": "^0.16.10", 13 | "axios": "^0.5.4", 14 | "lodash": "^3.9.3", 15 | "moment": "^2.10.3", 16 | "react": "^0.13.3", 17 | "react-router": "^0.13.3", 18 | "socket.io-stream": "^0.8.0" 19 | }, 20 | "devDependencies": { 21 | "babel-core": "^5.5.8", 22 | "babel-loader": "^5.1.4", 23 | "css-loader": "^0.15.1", 24 | "extract-text-webpack-plugin": "^0.8.2", 25 | "file-loader": "^0.8.4", 26 | "node-libs-browser": "^0.5.2", 27 | "node-sass": "^3.2.0", 28 | "react-hot-loader": "^1.2.7", 29 | "sass-loader": "^1.0.2", 30 | "style-loader": "^0.12.3", 31 | "webpack": "^1.9.11", 32 | "webpack-config-merger": "0.0.3", 33 | "webpack-dev-server": "^1.9.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/auth/local.js: -------------------------------------------------------------------------------- 1 | var passport = require('/src/node_modules/passport'); 2 | var session = require('/src/node_modules/express-session'); 3 | 4 | module.exports = function(app) { 5 | app.use(session({secret: '1234'})); 6 | app.use(passport.initialize()); 7 | app.use(passport.session()); 8 | 9 | var LocalStrategy = require('/src/node_modules/passport-local').Strategy; 10 | 11 | /** 12 | * Let's authenticate all users by default. 13 | */ 14 | passport.use(new LocalStrategy( 15 | function(username, password, done) { 16 | done(null, {username: username, password: password}); 17 | } 18 | )); 19 | 20 | /** 21 | * Define a route to authenticate your users, 22 | * then call next() to serve the response. 23 | */ 24 | app.get('/login', passport.authenticate('local'), function(req, res, next){ 25 | res.status(200).body = {result: "login successful"}; 26 | next() 27 | }); 28 | 29 | passport.serializeUser(function(user, done) { 30 | return done(null, JSON.stringify(user)); 31 | }); 32 | 33 | passport.deserializeUser(function(user, done) { 34 | return done(null, user); 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /src/client/components/buildHistory.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import Utils from '../utils/utils'; 5 | import {Link} from 'react-router'; 6 | 7 | class BuildHistory extends React.Component { 8 | render() { 9 | let getBuildStatusClassNames = (build)=> { 10 | return `label ${Utils.getBuildStatusClassName(build.status)}` 11 | }; 12 | 13 | return ( 14 |
15 |

Build History

16 |
17 | {this.props.builds.map((build, index)=> { 18 | let projectName = Utils.getProjectShortName(build.project); 19 | return ( 20 | 21 |
22 | {build.branch} 23 |
24 |
25 | {build.status} 26 |
27 | 28 | ); 29 | })} 30 |
31 |
32 | ); 33 | } 34 | } 35 | 36 | export default BuildHistory; -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "bitwise": true, 4 | "browser": true, 5 | "camelcase": true, 6 | "curly": true, 7 | "eqeqeq": true, 8 | "esnext": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "smarttabs": true, 17 | "strict": false, 18 | "sub": true, 19 | "trailing": true, 20 | "undef": true, 21 | "unused": true, 22 | "jquery": true, 23 | "globalstrict": true, 24 | "laxcomma": true, 25 | "predef": { 26 | "__dirname": true, 27 | "include": true, 28 | "inspect": true, 29 | "module": true, 30 | "describe":true, 31 | "before":true, 32 | "after":true, 33 | "beforeEach":true, 34 | "it": true, 35 | "angular": true, 36 | "Reconfig": true, 37 | "vpo": true, 38 | "_": true, 39 | "alert": true, 40 | "confirm": true, 41 | "maxConcurrentBuilds": true 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/client/utils/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import _ from 'lodash'; 3 | 4 | class Utils { 5 | static getProjectShortName(projectName = '') { 6 | let parts = projectName.split('__'); 7 | return parts[1] || parts[0]; 8 | } 9 | 10 | static getProjectUrl(projectName = '') { 11 | let parts = projectName.split('__'); 12 | return parts[0]; 13 | } 14 | 15 | static getUniqProjects(projects) { 16 | return _.chain(projects) 17 | .sortByOrder( p => { 18 | return p.latest_build.updated_by; 19 | }, 'desc') 20 | .uniq(project => { 21 | return this.getProjectShortName(project.name); 22 | }).value() 23 | } 24 | 25 | static filterBuildsByProject(builds, projectName){ 26 | return _.filter(builds, build => { 27 | return this.getProjectShortName(build.project) === this.getProjectShortName(projectName); 28 | }) 29 | } 30 | 31 | static getBuildStatusClassName(status) { 32 | let classNamesMap = { 33 | 'passed': 'label-success', 34 | 'failed': 'label-danger', 35 | 'started': 'label-warning', 36 | 'queued': 'label-info', 37 | }; 38 | 39 | return classNamesMap[status] || ''; 40 | } 41 | } 42 | 43 | export default Utils; 44 | -------------------------------------------------------------------------------- /src/matching.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const git = require('./git'); 3 | 4 | const matching = {}; 5 | 6 | /** 7 | * Check branch name against settings match rules 8 | * @param {object} settings - The global settings key from build.yml 9 | * @param {string} name - The name of the branch to be built 10 | * @param {string} path - The path to the Git repository 11 | * @return {boolean} - The result of the check, true if match is found 12 | */ 13 | matching.checkNameRules = async function(settings, name, path) { 14 | // Default settings match all branches 15 | if (!settings || !settings.matching) { 16 | return true; 17 | } 18 | 19 | const { branches, patterns, tags } = settings.matching; 20 | 21 | // Allow exact branch names 22 | if (branches && branches.includes(name)) { 23 | return true; 24 | } 25 | 26 | // Allow tags when enabled 27 | if (tags === true && await git.getRefType(path, name) === 'tag') { 28 | return true; 29 | } 30 | 31 | // Allow any name that matches a regex pattern 32 | if (patterns && _.some(patterns, function(pattern) { 33 | const regex = new RegExp(pattern); 34 | return regex.test(name); 35 | })) { 36 | return true; 37 | } 38 | 39 | // Disallow if no sub-keys are defined 40 | return false; 41 | } 42 | 43 | module.exports = matching; 44 | -------------------------------------------------------------------------------- /src/publisher.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var Q = require('q'); 3 | var config = require('./config'); 4 | 5 | var publisher = {}; 6 | 7 | /** 8 | * Publishes artifacts. 9 | * 10 | * This general publisher will loop through 11 | * to all the "publish" blocks of a project 12 | * configuration and invoke the specific 13 | * publisher (ie. s3). 14 | * 15 | * We will wait untill all publishers are done 16 | * to declare ourselves done :) 17 | */ 18 | publisher.publish = function(dockerClient, buildId, project, logger, options) { 19 | if (project.publish) { 20 | logger.info("[%s] Publishing artifacts...", buildId); 21 | var publishers = []; 22 | 23 | _.each(project.publish, function(target){ 24 | logger.info("[%s] Publishing to %s", buildId, target.to); 25 | var publisher_config = {} 26 | if (config.config.publisher && config.config.publisher.hasOwnProperty(target.to)) { 27 | publisher_config = config.config.publisher[target.to]; 28 | } 29 | _.extend(target, publisher_config) 30 | publishers.push(require('./publisher/' + target.to)(dockerClient, buildId, project, logger, target)); 31 | }); 32 | 33 | return Q.all(publishers); 34 | } else { 35 | logger.info("[%s] Nothing to publish for this build", buildId); 36 | return Q.when(); 37 | } 38 | } 39 | 40 | module.exports = publisher; 41 | -------------------------------------------------------------------------------- /src/notifications/emailSes.js: -------------------------------------------------------------------------------- 1 | var aws = require('aws-sdk'); 2 | var _ = require('lodash'); 3 | 4 | /** 5 | * Triggers an email notification 6 | * through AWS SES. 7 | */ 8 | module.exports = function(project, options, notificationOptions) { 9 | aws.config.accessKeyId = notificationOptions['accessKey']; 10 | aws.config.secretAccessKey = notificationOptions.secret; 11 | aws.config.region = notificationOptions.region; 12 | var ses = new aws.SES({apiVersion: '2010-12-01'}); 13 | var recipients = []; 14 | 15 | _.each(notificationOptions.to, function(recipient){ 16 | if (recipient == 'committer' && options.author) { 17 | recipients.push(options.author) 18 | } else { 19 | recipients.push(recipient) 20 | } 21 | }) 22 | 23 | ses.sendEmail( { 24 | Source: notificationOptions.from, 25 | Destination: { ToAddresses: recipients }, 26 | Message: { 27 | Subject: { 28 | Data: options.comments.shift() 29 | }, 30 | Body: { 31 | Text: { 32 | Data: options.comments.join("\n"), 33 | } 34 | } 35 | } 36 | } 37 | , function(err, data) { 38 | if(err) { 39 | options.logger.error(err); 40 | return; 41 | } 42 | 43 | options.logger.info('[%s] Sent email to %s', options.buildId, notificationOptions.to.join(',')); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /src/client/components/project.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import moment from 'moment'; 5 | import {Link} from 'react-router'; 6 | import Utils from '../utils/utils' 7 | 8 | class Project extends React.Component { 9 | 10 | render() { 11 | let data = this.props.data; 12 | let build = data.latest_build; 13 | 14 | let getStatusClassName = function (status) { 15 | let classNamesMap = { 16 | 'started': 'project__build--started', 17 | 'passed': 'project__build--passed', 18 | 'failed': 'project__build--failed' 19 | }; 20 | 21 | return classNamesMap[status] || ''; 22 | }; 23 | 24 | let projectName = Utils.getProjectShortName(data.name); 25 | let activeClass = this.props.selected ? 'active' : ''; 26 | 27 | return ( 28 |
29 | 30 |

31 | {projectName} : {build.branch} 32 |

33 |
tag: {build.tag}
34 |
created: {moment(build.created_at).fromNow()}
35 |
updated: {moment(build.updated_at).fromNow()}
36 | 37 |
38 | ); 39 | } 40 | } 41 | export default Project; -------------------------------------------------------------------------------- /src/client/styles/App.scss: -------------------------------------------------------------------------------- 1 | html,body{ 2 | height: 100%; 3 | padding: 0; 4 | } 5 | .full-width { 6 | width: 100%; 7 | } 8 | .content-wrapper { 9 | min-height: 100vh; 10 | overflow: hidden; 11 | padding-bottom: 30px; 12 | } 13 | #sideBar, #mainContent { 14 | padding-bottom: 9999px; 15 | margin-bottom: -9999px; 16 | } 17 | #mainContent { 18 | border-left: 5px solid #dedede; 19 | } 20 | .build-header { 21 | padding-bottom: 15px; 22 | } 23 | .projects-list { 24 | margin-top: 20px; 25 | } 26 | 27 | /* Project component */ 28 | .project { 29 | margin-top: 10px; 30 | } 31 | .project__build { 32 | border: none; 33 | border-left: 10px solid transparent; 34 | box-shadow: -1px 1px 5px #D2D2D2; 35 | 36 | .list-group-item { 37 | border-radius: 0; 38 | } 39 | 40 | &--started { 41 | border-left-color: #F0AD4E; 42 | } 43 | &--passed { 44 | border-left-color: #3FA75F; 45 | } 46 | &--failed { 47 | border-left-color: #DB423C; 48 | } 49 | } 50 | 51 | /*Project details component*/ 52 | 53 | .project-details__build-branch, 54 | .project-details__build-status { 55 | margin-left: 10px; 56 | } 57 | 58 | .build-output, 59 | { 60 | background-color: #000; 61 | position: relative; 62 | } 63 | .build-output__header { 64 | color: #21D4F4; 65 | .title { 66 | color: lightblue; 67 | font-weight: bold; 68 | } 69 | .auto-update { 70 | float: right; 71 | } 72 | } 73 | .build-output__log { 74 | width: 100%; 75 | height: 75vh; 76 | border: 2px solid limegreen; 77 | background-color: #fff; 78 | } 79 | -------------------------------------------------------------------------------- /src/auth.js: -------------------------------------------------------------------------------- 1 | var config = require('./config') 2 | var auth = {}; 3 | 4 | /** 5 | * This is the base authorization function 6 | * that will be called before every route. 7 | * 8 | * By default, it just calls next, without 9 | * verifying that the user is authenticated. 10 | * 11 | * When you turn on authentication, this will 12 | * be overriden and will actually check that 13 | * the user is authenticated. 14 | * 15 | * @param {object} req 16 | * @param {object} res 17 | * @param {Function} next 18 | * @return void 19 | */ 20 | auth.authorize = function(req, res, next){ 21 | next() 22 | } 23 | 24 | /** 25 | * Enable authentication for the app. 26 | * 27 | * This method will dynamically load 28 | * a module provided by the user and 29 | * replace the authorize function so that 30 | * it will actually check that the user 31 | * is authenticated before letting it 32 | * hit routes in roger. 33 | * 34 | * @param {[type]} app [description] 35 | * @return {[type]} [description] 36 | */ 37 | auth.enable = function(app) { 38 | auth.authorize = function(req, res, next) { 39 | if (!req.isAuthenticated()) { 40 | throw new Error("You didnt say the magic word!") 41 | } 42 | 43 | next() 44 | }; 45 | 46 | /** 47 | * This bit is what creates the magic: it loads 48 | * a module dynamically and lets you register your 49 | * own strategy based on passport 50 | * 51 | * @see http://passportjs.org/docs/ 52 | */ 53 | require(config.get('app.auth.provider'))(app); 54 | } 55 | 56 | module.exports = auth; 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "roger", 3 | "version": "1.4.1", 4 | "description": "A build system for dockerized apps. From the ones who tried Jenkins and had their eyes bleed for too long.", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "mocha" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/namshi/roger" 12 | }, 13 | "keywords": [ 14 | "docker", 15 | "build", 16 | "jenkins", 17 | "ci", 18 | "continuos", 19 | "integration", 20 | "server", 21 | "swag" 22 | ], 23 | "author": "Alex Nadalin ", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/namshi/roger/issues" 27 | }, 28 | "homepage": "https://github.com/namshi/roger", 29 | "dependencies": { 30 | "aws-sdk": "^2.6.4", 31 | "body-parser": "^1.10.2", 32 | "dockerode": "2.4.3", 33 | "express": "^4.11.1", 34 | "express-session": "^1.11.3", 35 | "github": "^9.2.0", 36 | "growing-file": "^0.1.3", 37 | "js-yaml": "^3.2.5", 38 | "lodash": "^2.4.1", 39 | "moment": "^2.9.0", 40 | "netroute": "^1.0.2", 41 | "node-dir": "^0.1.15", 42 | "node-uuid": "^1.4.7", 43 | "nodegit": "^0.20.1", 44 | "passport": "^0.2.2", 45 | "passport-github2": "^0.1.10", 46 | "passport-local": "^1.0.0", 47 | "q": "^1.1.2", 48 | "reconfig": "^1.5.1", 49 | "s3": "^4.4.0", 50 | "slack-notify": "^0.1.6", 51 | "socket.io": "^1.4.0", 52 | "socket.io-stream": "^0.8.0", 53 | "tar-fs": "^1.4.2", 54 | "winston": "^0.8.3", 55 | "yargs": "^1.3.3" 56 | }, 57 | "devDependencies": {} 58 | } 59 | -------------------------------------------------------------------------------- /src/client/actions/actions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import axios from 'axios'; 3 | import alt from '../lib/alt'; 4 | import Utils from '../utils/utils'; 5 | 6 | class Actions { 7 | startBuild(projectName, branch) { 8 | let buildUrl = `/api/build?repo=${projectName}`; 9 | 10 | if(branch) { 11 | branch = branch === 'latest' ? 'master' : branch; 12 | buildUrl += `&branch=${branch}`; 13 | } 14 | 15 | axios.get(buildUrl).then(res=> { 16 | alert(res.data.message); 17 | this.actions.updateBuildInProgress(projectName); 18 | }); 19 | } 20 | 21 | updateBuildInProgress(projectName) { 22 | this.dispatch(projectName); 23 | } 24 | 25 | loadProjects() { 26 | return axios.get('/api/projects?limit=10') 27 | .then(res=> { 28 | return res.data.projects || []; 29 | }) 30 | .catch(err=> { 31 | console.log('Error loading projects', err); 32 | }); 33 | } 34 | 35 | loadBuilds() { 36 | return axios.get('/api/builds?limit=100') 37 | .then(res=> { 38 | return res.data.builds || []; 39 | }) 40 | .catch(err=> { 41 | console.log('Error loading builds', err); 42 | }); 43 | } 44 | 45 | loadData() { 46 | axios.all([this.actions.loadProjects(), this.actions.loadBuilds()]).then((data)=> { 47 | let builds = data[1]; 48 | let projects = Utils.getUniqProjects(data[0]); 49 | this.actions.updateData({ 50 | 'projects': projects, 51 | 'builds': builds 52 | }); 53 | }); 54 | } 55 | 56 | updateData(data) { 57 | this.dispatch(data); 58 | } 59 | } 60 | export default alt.createActions(Actions); 61 | -------------------------------------------------------------------------------- /src/client/components/projectsListContainer.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import Project from '../components/project'; 5 | import Utils from '../utils/utils' 6 | import _ from 'lodash'; 7 | 8 | class ProjectsListContainer extends React.Component { 9 | constructor(...props) { 10 | super(...props); 11 | this.state = { 12 | searchText: '' 13 | }; 14 | this.filterProjects = this.filterProjects.bind(this); 15 | } 16 | 17 | filterProjects(e) { 18 | this.setState({ 19 | searchText: e.target.value 20 | }); 21 | } 22 | 23 | render(){ 24 | 25 | let isSelectedProject = (project)=> { 26 | let params = this.props.params; 27 | return params.project === Utils.getProjectShortName(project.name); 28 | }; 29 | 30 | return ( 31 | 47 | ); 48 | } 49 | } 50 | 51 | export default ProjectsListContainer; -------------------------------------------------------------------------------- /src/tar.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'); 2 | var spawn = require('child_process').spawn; 3 | var baseTar = require('tar-fs') 4 | 5 | var tar = {}; 6 | 7 | tar.createFromStream = function(path, stream) { 8 | var extract = baseTar.extract(path, {strict: false}); 9 | stream.pipe(extract); 10 | 11 | return Q.Promise(function(resolve, reject){ 12 | stream.on('error', function(err){ 13 | reject(err); 14 | }) 15 | 16 | extract.on('finish', function() { 17 | resolve(); 18 | }); 19 | }); 20 | } 21 | 22 | /** 23 | * Creates a tarball at path with the contents 24 | * of the sourceDirectory. 25 | * 26 | * After the tarball is created, the callback 27 | * gets invoked. 28 | * 29 | * @return promise 30 | */ 31 | tar.create = function(path, sourceDirectory, buildLogger, buildOptions){ 32 | var deferred = Q.defer(); 33 | var tar = spawn('tar', ['-C', sourceDirectory, '-czvf', path, '.']); 34 | 35 | buildLogger.info('[%s] Creating tarball', buildOptions.buildId); 36 | 37 | tar.stderr.on('data', function(data) { 38 | buildLogger.error('[%s] Error creating tarball', buildOptions.buildId); 39 | deferred.reject(data) 40 | }); 41 | 42 | tar.stdout.on('data', function(data) { 43 | buildLogger.error('[%s] %s', buildOptions.buildId, data.toString()); 44 | }); 45 | 46 | tar.on('close', function(code) { 47 | if (code === 0) { 48 | buildLogger.info('[%s] Tarball created', buildOptions.buildId); 49 | deferred.resolve() 50 | } else { 51 | buildLogger.info('[%s] Error creating tarball', buildOptions.buildId); 52 | deferred.reject(new Error("Unable to tar -- exit code " + code)) 53 | } 54 | }); 55 | 56 | return deferred.promise; 57 | }; 58 | 59 | module.exports = tar; 60 | -------------------------------------------------------------------------------- /src/hooks.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'); 2 | var _ = require('lodash'); 3 | var hooks = {}; 4 | 5 | /** 6 | * Utility function that runs the hook 7 | * in a container and resolves / rejects 8 | * based on the hooks exit code. 9 | */ 10 | function promisifyHook(dockerClient, buildId, command, logger) { 11 | var deferred = Q.defer(); 12 | 13 | dockerClient.run(buildId, command.split(' '), process.stdout, function (err, data, container) { 14 | if (err) { 15 | deferred.reject(err); 16 | } else if (data.StatusCode === 0) { 17 | deferred.resolve(); 18 | } else { 19 | logger.error(data); 20 | deferred.reject(command + ' failed, exited with status code ' + data.StatusCode); 21 | } 22 | }); 23 | 24 | return deferred.promise; 25 | } 26 | 27 | /** 28 | * Run all hooks for a specified event on a container 29 | * built from the buildId, ie. my_node_app:master 30 | * 31 | * If any of the hooks fails, the returning promise 32 | * will be rejected. 33 | * 34 | * @return promise 35 | */ 36 | hooks.run = function(event, buildId, project, dockerClient, logger) { 37 | var deferred = Q.defer(); 38 | var hooks = project[event]; 39 | var promises = []; 40 | 41 | if (_.isArray(hooks)) { 42 | logger.info('[%s] Running %s hooks', buildId, event); 43 | 44 | _.each(hooks, function(command) { 45 | logger.info('[%s] Running %s hook "%s"', buildId, event, command); 46 | 47 | promises.push(promisifyHook(dockerClient, buildId, command, logger)); 48 | }); 49 | 50 | return Q.all(promises); 51 | } else { 52 | logger.info('[%s] No %s hooks to run', buildId, event); 53 | deferred.resolve(); 54 | } 55 | 56 | return deferred.promise; 57 | }; 58 | 59 | module.exports = hooks; 60 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | var url = require('url'); 2 | var _ = require('lodash'); 3 | var config = require('./config'); 4 | var utils = {}; 5 | 6 | /** 7 | * Gets a roger path 8 | * 9 | * @param {string} to 10 | * @return {string} 11 | */ 12 | utils.path = function(to) { 13 | return config.get('paths.' + to); 14 | } 15 | 16 | /** 17 | * Throw a simple exception with the 18 | * given name and message. 19 | * 20 | * @param {string} name 21 | * @param {string} message 22 | * @throws {Error} 23 | */ 24 | utils.throw = function(name, message){ 25 | var e = new Error(message) 26 | e.name = name 27 | 28 | throw e 29 | } 30 | 31 | /** 32 | * Utility function that takes an 33 | * object and recursively loops 34 | * through it replacing all "sensitive" 35 | * informations with *****. 36 | * 37 | * This is done to print out objects 38 | * without exposing passwords and so 39 | * on. 40 | */ 41 | utils.obfuscate = function(object) { 42 | object = _.clone(object); 43 | var stopWords = ['password', 'github', 'github-token', 'token', 'access-key', 'secret'] 44 | 45 | _.each(object, function(value, key){ 46 | if (typeof value === 'object') { 47 | object[key] = utils.obfuscate(value); 48 | } 49 | 50 | if (_.isString(value)) { 51 | object[key] = utils.obfuscateString(value); 52 | 53 | if (_.contains(stopWords, key)) { 54 | object[key] = '*****' 55 | } 56 | } 57 | }) 58 | 59 | return object; 60 | }; 61 | 62 | /** 63 | * Takes a string and remove all sensitive 64 | * values from it. 65 | * 66 | * For example, if the string is a URL, it 67 | * will remove the auth, if present. 68 | */ 69 | utils.obfuscateString = function(string) { 70 | var parts = url.parse(string); 71 | 72 | if (parts.host && parts.auth) { 73 | parts.auth = null; 74 | 75 | return url.format(parts); 76 | } 77 | 78 | return string; 79 | } 80 | 81 | module.exports = utils; 82 | -------------------------------------------------------------------------------- /src/client/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 4 | var mergeWebpackConfig = require('webpack-config-merger'); 5 | 6 | // Default configurations 7 | var config = { 8 | entry: [ 9 | './app.jsx', // App entry point 10 | './index.html', 11 | './styles/App.scss' 12 | ], 13 | output: { 14 | path: path.join(__dirname, 'dist'), 15 | filename: 'bundle.js' 16 | }, 17 | resolve: { 18 | extensions: ['', '.js', '.jsx'] 19 | }, 20 | module: { 21 | loaders: [{ 22 | test: /\.jsx?$/, 23 | exclude: /(node_modules|bower_components)/, 24 | loaders: ['babel'] 25 | }, 26 | { 27 | test: /\.html$/, 28 | loader: "file?name=[name].[ext]" 29 | }, 30 | { 31 | test: /\.scss$/, 32 | loader: ExtractTextPlugin.extract( 33 | 'css?sourceMap!' + 34 | 'sass?sourceMap' 35 | ) 36 | }] 37 | }, 38 | plugins: [ 39 | new webpack.NoErrorsPlugin(), 40 | new ExtractTextPlugin('app.css') 41 | ] 42 | }; 43 | 44 | // Development specific configurations 45 | var devConfig = { 46 | entry: [ 47 | 'webpack-dev-server/client?http://0.0.0.0:8080', // WebpackDevServer host and port 48 | 'webpack/hot/only-dev-server' 49 | ], 50 | devtool: process.env.WEBPACK_DEVTOOL || 'source-map', 51 | module: { 52 | loaders: [{ 53 | test: /\.jsx?$/, 54 | exclude: /(node_modules|bower_components)/, 55 | loaders: ['react-hot', 'babel'] 56 | }] 57 | }, 58 | devServer: { 59 | contentBase: "./dist", 60 | noInfo: true, // --no-info option 61 | hot: true, 62 | inline: true 63 | }, 64 | plugins: [ 65 | new webpack.HotModuleReplacementPlugin(), 66 | ] 67 | }; 68 | 69 | var isDev = process.env.NODE_ENV !== 'production'; 70 | if(isDev) { 71 | mergeWebpackConfig(config, devConfig); 72 | } 73 | 74 | module.exports = config; -------------------------------------------------------------------------------- /src/notifications.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var config = require('./config'); 3 | var router = require('./router'); 4 | var notifications = {}; 5 | 6 | /** 7 | * Trigger notifications for a specific project. 8 | * 9 | * @param project 10 | * @param branch 11 | * @param options {buildId, uuid, logger} 12 | */ 13 | notifications.trigger = function(project, branch, options){ 14 | options.logger.info('[%s] Sending notifications for %s', options.buildId, options.uuid); 15 | var comments = ['[' + options.project.name + ':' + branch + '] BUILD PASSED']; 16 | var buildLink = router.generate('build-link', {build: options.uuid, projectName: project.name}, true); 17 | var status = { 18 | state: 'success', 19 | target_url: buildLink, 20 | description: 'The build succeeded!' 21 | }; 22 | 23 | if (options.result instanceof Error) { 24 | comments = ['[' + options.project.name + ':' + branch + '] BUILD FAILED: ' + options.result.message]; 25 | status.state = 'failure'; 26 | status.description = 'The build failed!'; 27 | } 28 | 29 | comments.push("You can check the build output at " + buildLink); 30 | 31 | _.each(config.get('notifications'), function(val, key){ 32 | if (val['global'] === 'true') { 33 | project.notify.push(key); 34 | } 35 | }); 36 | 37 | if (_.isArray(project.notify)) { 38 | _.each(project.notify, function(handler){ 39 | options.branch = branch; 40 | options.comments = _.clone(comments); 41 | options.status = status; 42 | 43 | var notificationOptions = (project.notify && project.notify[handler]) || config.get('notifications.' + handler); 44 | 45 | try { 46 | require('./notifications/' + handler)(project, _.clone(options), notificationOptions); 47 | } catch(err) { 48 | options.logger.info('[%s] Error with notifying %s: %s', options.buildId, handler, err.toString()); 49 | } 50 | }); 51 | } 52 | }; 53 | 54 | module.exports = notifications; 55 | -------------------------------------------------------------------------------- /src/storage.js: -------------------------------------------------------------------------------- 1 | var dispatcher = require('./dispatcher'); 2 | var config = require('./config'); 3 | var adapter = require('./storage/' + config.get('app.storage')); 4 | 5 | /** 6 | * The storage object is simply 7 | * responsible for proxying a 8 | * storage adapter (ie. mysql) 9 | * and defining the interface 10 | * that the adapter needs to 11 | * implement. 12 | * 13 | * If you wish to implement your own 14 | * storage solution, you will only 15 | * need to implement the methods 16 | * exported here. 17 | * 18 | * For an exaple implementation, 19 | * see storage/file.js, our super-dummy 20 | * file-based storage system. 21 | * 22 | * @type {Object} 23 | */ 24 | module.exports = { 25 | /** 26 | * Saves build information. 27 | */ 28 | saveBuild: function(id, tag, project, branch, status) { 29 | return adapter.saveBuild(id, tag, project, branch, status).then(function(result) { 30 | dispatcher.emit('storage-updated'); 31 | 32 | return result; 33 | }); 34 | }, 35 | /** 36 | * Returns all builds of a project, 37 | * DESC sorted. 38 | */ 39 | getBuilds: function(limit) { 40 | return adapter.getBuilds(limit); 41 | }, 42 | /** 43 | * Returns all started jobs. 44 | */ 45 | getStartedBuilds: function() { 46 | return this.getBuildsByStatus(['started']); 47 | }, 48 | /** 49 | * Returns all jobs that are either started 50 | * or queued. 51 | */ 52 | getPendingBuilds: function() { 53 | return this.getBuildsByStatus(['started', 'queued']); 54 | }, 55 | /** 56 | * Returns a list of builds in the given 57 | * statuses. 58 | * 59 | * @param {list} statuses 60 | * @return {list} 61 | */ 62 | getBuildsByStatus: function(statuses) { 63 | return adapter.getBuildsByStatus(statuses); 64 | }, 65 | /** 66 | * Returns all projects, 67 | * DESC sorted by latest build. 68 | */ 69 | getProjects: function(limit) { 70 | return adapter.getProjects(limit); 71 | }, 72 | /** 73 | * Returns a particular build. 74 | */ 75 | getBuild: function(id) { 76 | return adapter.getBuild(id); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/client/components/buildOutput.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | 5 | class BuildOutput extends React.Component { 6 | constructor(...props) { 7 | super(...props); 8 | this.state = { autoUpdate: true }; 9 | this._numberOfTries = 0; 10 | this._lastScrollHeight = 0; 11 | this.stopTimer = this.stopTimer.bind(this); 12 | } 13 | 14 | componentDidMount() { 15 | this.updateBuildOutput(); 16 | } 17 | 18 | componentWillReceiveProps(props) { 19 | this.updateBuildOutput(); 20 | } 21 | 22 | getBuildOutputWindow(){ 23 | let buildOutputFrame = this.refs.buildOutputFrame.getDOMNode(); 24 | return buildOutputFrame.contentWindow; 25 | } 26 | 27 | scrollLogView(){ 28 | try{ 29 | let _win = this.getBuildOutputWindow(); 30 | let scrollHeight = _win.document.body.scrollHeight; 31 | 32 | if(scrollHeight === this._lastScrollHeight ){ 33 | this._numberOfTries++; 34 | } else { 35 | this._numberOfTries = 0; 36 | this._lastScrollHeight = scrollHeight; 37 | } 38 | 39 | if( this._numberOfTries > 100 && this.props.buildStatus !== 'started' ){ 40 | this.stopTimer(); 41 | return; 42 | } 43 | 44 | if(this.state.autoUpdate){ 45 | _win.scrollTo(0, scrollHeight); 46 | } 47 | } catch(e){} 48 | } 49 | 50 | updateBuildOutput(){ 51 | this.stopTimer(); 52 | this._numberOfTries = 0; 53 | 54 | let runTimer = () => { 55 | this.raf = requestAnimationFrame(this.scrollLogView.bind(this)); 56 | this.tmr = setTimeout( runTimer, 1); 57 | }; 58 | 59 | runTimer(); 60 | } 61 | 62 | stopTimer(){ 63 | clearTimeout(this.tmr); 64 | cancelAnimationFrame(this.raf); 65 | } 66 | 67 | toggleAutoUpdate(){ 68 | this.setState({ 69 | autoUpdate: !this.state.autoUpdate 70 | }); 71 | 72 | if(this.state.autoUpdate){ 73 | this.updateBuildOutput(); 74 | } 75 | } 76 | 77 | render() { 78 | return
79 |
80 | Build Id : {this.props.buildId} 81 | 82 | Auto scroll: 83 |

84 |
85 |