├── .dockerignore ├── .gitignore ├── views └── jobs.pug ├── docker-compose.yml ├── lib ├── handler.js ├── build-app.js └── worker.js ├── package.json ├── index.js ├── Dockerfile └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | tmp/* 3 | -------------------------------------------------------------------------------- /views/jobs.pug: -------------------------------------------------------------------------------- 1 | h1 Jobs 2 | 3 | table 4 | thead 5 | tr 6 | th JobID 7 | th Start at 8 | th End at 9 | th Commit 10 | tbody 11 | each job in jobs 12 | tr 13 | td= job.id 14 | td= job.startAt 15 | td= job.endAt 16 | td= job.headCommit -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | app: 4 | build: . 5 | volumes: 6 | - .:/code 7 | ports: 8 | - 4567:4567 9 | environment: 10 | - GITHUB_BRANCH=master 11 | - REDIS_URL=redis://redis 12 | links: 13 | - redis 14 | redis: 15 | image: redis 16 | -------------------------------------------------------------------------------- /lib/handler.js: -------------------------------------------------------------------------------- 1 | var worker = require('./worker'); 2 | 3 | module.exports = function (repo, data) { 4 | if (repo === "decidim" && data.ref === `refs/heads/${process.env.GITHUB_BRANCH}`) { 5 | var githubUrl = data.repository.clone_url; 6 | var headCommit = data.head_commit.id; 7 | 8 | worker.enqueue(githubUrl, headCommit); 9 | } 10 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "decidim-app-builder", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon index.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "body-parser": "^1.15.2", 14 | "express": "^4.14.0", 15 | "express-github-webhook": "^1.0.5", 16 | "moment": "^2.15.1", 17 | "node-uuid": "^1.4.7", 18 | "pug": "^2.0.0-beta6", 19 | "redis": "^2.6.2" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/build-app.js: -------------------------------------------------------------------------------- 1 | var util = require('util'), 2 | spawn = require('child_process').spawn; 3 | 4 | module.exports = function (githubUrl, headCommit, cb) { 5 | var cmd = spawn('./build-app.sh', [githubUrl, headCommit]); 6 | 7 | cmd.stdout.on('data', function (data) { 8 | console.log('stdout: ' + data.toString()); 9 | }); 10 | 11 | cmd.stderr.on('data', function (data) { 12 | console.log('stderr: ' + data.toString()); 13 | }); 14 | 15 | cmd.on('exit', function (code) { 16 | console.log('child process exited with code ' + code.toString()); 17 | cb(); 18 | }); 19 | }; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var GithubWebHook = require('express-github-webhook'); 3 | var webhookHandler = GithubWebHook({ path: '/', secret: process.env.GITHUB_SECRET }); 4 | var bodyParser = require('body-parser'); 5 | 6 | var app = express(); 7 | 8 | var handler = require('./lib/handler'); 9 | var worker = require('./lib/worker'); 10 | 11 | worker.run(); 12 | 13 | app.use(bodyParser.json()); 14 | app.use(webhookHandler); 15 | 16 | webhookHandler.on('push', handler); 17 | 18 | app.set('view engine', 'pug'); 19 | 20 | app.get('/jobs', function (req, res) { 21 | worker.list().then(function (jobs) { 22 | res.render('jobs', { jobs: jobs }); 23 | }); 24 | }); 25 | 26 | app.listen(4567, function () { 27 | console.log('Example app listening on port 4567!'); 28 | }); 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:6.6.0 2 | MAINTAINER david.morcillo@codegram.com 3 | 4 | ARG rancherAccessKey= 5 | ARG rancherSecretKey= 6 | ARG rancherUrl= 7 | ARG dockerAuth= 8 | 9 | ENV APP_HOME /code 10 | 11 | ENV RANCHER_VERSION v0.6.1 12 | 13 | RUN apt-get update && apt-get install -y wget apt-transport-https ca-certificates 14 | 15 | RUN apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D && \ 16 | echo "deb https://apt.dockerproject.org/repo ubuntu-trusty main" >> /etc/apt/sources.list 17 | 18 | RUN apt-get update && apt-get install -y docker-engine 19 | 20 | RUN npm install -g nodemon 21 | 22 | RUN mkdir -p $HOME/.rancher && \ 23 | echo "{\"accessKey\":\"$rancherAccessKey\",\"secretKey\":\"$rancherSecretKey\",\"url\":\"$rancherUrl\",\"environment\":\"1a5\"}" > $HOME/.rancher/cli.json 24 | 25 | RUN mkdir -p $HOME/.docker && \ 26 | echo "{\"auths\":{\"https://index.docker.io/v1/\": {\"auth\":\"$dockerAuth\",\"email\":\"david.morcillo@gmail.com\"}}}" > $HOME/.docker/config.json 27 | 28 | RUN mkdir -p /rancher && \ 29 | cd /rancher && \ 30 | wget https://github.com/rancher/cli/releases/download/$RANCHER_VERSION/rancher-linux-amd64-$RANCHER_VERSION.tar.gz && \ 31 | tar -xzvf rancher-linux-amd64-$RANCHER_VERSION.tar.gz 32 | 33 | # Set the timezone. 34 | RUN echo "Europe/Madrid" > /etc/timezone 35 | RUN dpkg-reconfigure -f noninteractive tzdata 36 | 37 | RUN mkdir -p $APP_HOME 38 | 39 | ADD package.json /tmp 40 | RUN cd /tmp && npm install && cp -r /tmp/node_modules $APP_HOME 41 | 42 | WORKDIR $APP_HOME 43 | ADD . $APP_HOMEk 44 | 45 | CMD npm start 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Decidim App builder 2 | 3 | This applications runs a simple express.js server which can listen github push webhooks. 4 | 5 | You need to provide a couple of environment variables: 6 | - `GITHUB_BRANCH`: Just listen push webhook events on this branch. 7 | - `GITHUB_SECRET`: Webhook secret to decrypt github payload. 8 | 9 | ## How it works 10 | 11 | When a webhook is received the applications runs the `build-app.sh` script. This script perfoms the following steps: 12 | 13 | 1. Clone the github repo and move the HEAD to the last branch commit. 14 | 2. Build `decidim` docker image and push it to Docker Hub. 15 | 3. Use the previous image to run `decidim` generator and creates a test application. 16 | 4. Build decidim test application docker image and push it to Docker Hub. 17 | 5. Use rancher CLI to upgrade the service running decidim test application. 18 | 19 | The script itself is running Docker through a Rancher host and it can be highly configured using the script environment variables. 20 | 21 | ## Build docker image 22 | 23 | In order to build the docker image you need some credentials. Rancher credentials can be found on `$HOME/.rancher/cli.json` and Docker credentials on `$HOME/.docker/config.json`. 24 | 25 | ``` 26 | docker build --build-arg rancherAccessKey=xxxx 27 | --build-arg rancherSecretKey=xxxx 28 | --build-arg rancherUrl=xxxx 29 | --build-arg dockerAuth=xxxx 30 | -t codegram/decidim-app-builder . 31 | ``` 32 | 33 | Since the image is storing sensitive data it is recommended to use a private docker registry. 34 | 35 | ## Roadmap 36 | 37 | - Create some kind of queue in order to handle multiple webhooks. 38 | - Support for multi-deployments. 39 | 40 | ## License 41 | 42 | MIT License, see LICENSE for details. Copyright 2012-2016 Codegram Technologies. -------------------------------------------------------------------------------- /lib/worker.js: -------------------------------------------------------------------------------- 1 | var redis = require("redis"); 2 | var client = redis.createClient(process.env.REDIS_URL); 3 | var moment = require('moment'); 4 | var uuid = require('node-uuid'); 5 | var buildApp = require('./build-app'); 6 | 7 | function processJob () { 8 | client.lpop("jobs", function (err, jobId) { 9 | if (jobId) { 10 | console.log("Processing job " + jobId + "..."); 11 | client.get("job-" + jobId, function (err, data) { 12 | var parsedData = JSON.parse(data); 13 | 14 | parsedData.startAt = moment().format('MMMM Do YYYY, h:mm:ss a'); 15 | 16 | client.set("job-" + jobId, JSON.stringify(parsedData), function () { 17 | buildApp(parsedData.githubUrl, parsedData.headCommit, function () { 18 | parsedData.endAt = moment().format('MMMM Do YYYY, h:mm:ss a'); 19 | 20 | client.set("job-" + jobId, JSON.stringify(parsedData), function () { 21 | console.log("Finished job " + jobId + "..."); 22 | processJob(); 23 | }); 24 | }); 25 | }); 26 | }); 27 | } else { 28 | processJob(); 29 | } 30 | }); 31 | } 32 | 33 | function run () { 34 | console.log("Start worker process..."); 35 | processJob(); 36 | } 37 | 38 | function enqueue(githubUrl, headCommit) { 39 | var jobId = uuid.v1(); 40 | 41 | console.log("Enqueue job " + jobId + "..."); 42 | client.set("job-" + jobId, JSON.stringify({ 43 | githubUrl: githubUrl, 44 | headCommit: headCommit 45 | }), function () { 46 | client.expire("job-" + jobId, 86400, function (err, data) { 47 | client.lpush("jobs", jobId); 48 | }); 49 | }); 50 | } 51 | 52 | function list() { 53 | var promise = new Promise(function (resolve, reject) { 54 | client.keys("job-*", function (err, keys) { 55 | Promise.all( 56 | keys.map(function (key) { 57 | var promise = new Promise(function (resolve, reject) { 58 | var matches = key.match(/job-(.*)/); 59 | var jobId = matches[1]; 60 | 61 | client.get("job-" + jobId, function (err, data) { 62 | var parsedData = JSON.parse(data); 63 | parsedData.id = jobId; 64 | resolve(parsedData); 65 | }); 66 | }); 67 | return promise; 68 | }) 69 | ).then(function (jobs) { 70 | return jobs.sort(function (a, b) { 71 | return moment(b.startAt, 'MMMM Do YYYY, h:mm:ss a').diff(moment(a.startAt, 'MMMM Do YYYY, h:mm:ss a')); 72 | }); 73 | }).then(resolve); 74 | }); 75 | }); 76 | 77 | return promise; 78 | } 79 | 80 | module.exports = { 81 | run: run, 82 | process: process, 83 | enqueue: enqueue, 84 | list: list 85 | }; --------------------------------------------------------------------------------