├── .gitignore ├── README.md ├── architecture.png ├── example.gif ├── handler.js ├── package.json ├── plugins └── serverless-portable-templates │ ├── index.js │ └── package.json ├── serverless.yml └── webhook.js /.gitignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | jspm_packages 4 | 5 | # Serverless directories 6 | .serverless -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Github CodeBuild Webhook 2 | ------------------------ 3 | 4 | This project will setup an api gateway endpoint, which you can have your github repository connect to. This will start and update a commit with the current build status. 5 | This will be triggered for any PR update, on any branch. 6 | 7 | !WARNING! 8 | --------- 9 | This repo is no longer maintained, codebuild nowadays has native Github integration, please use that. 10 | 11 | Installation 12 | ------------ 13 | Use the steps below to launch the stack directly into your AWS account. You can setup as much stacks as you want, as the stack is currently connected to 1 CodeBuild project. 14 | 15 | 1. First, we'll need to setup an [AWS CodeBuild](https://eu-west-1.console.aws.amazon.com/codebuild/home) project. Create a new project in the AWS console, and be sure to add a [`buildspec.yml`](http://docs.aws.amazon.com/codebuild/latest/userguide/build-spec-ref.html) file to your project with some steps. Here's an [example](https://github.com/svdgraaf/webhook-test/blob/master/buildspec.yml). 16 | 2. Create a github api token in your account here, so that the stack is allowed to use your account: [https://github.com/settings/tokens/new](https://github.com/settings/tokens/new). You can ofcourse choose to setup a seperate account for this. 17 | 3. Deploy the stack: 18 | 19 | [![Launch Awesomeness](https://s3.amazonaws.com/cloudformation-examples/cloudformation-launch-stack.png)](https://console.aws.amazon.com/cloudformation/home?region=eu-west-1#/stacks/new?stackName=serverless-build-trigger&templateURL=https://s3-eu-west-1.amazonaws.com/github-webhook-artifacts-eu-west-1/serverless/github-webhook/trigger/1494331984949-2017-05-09T12%3A13%3A04.949Z/compiled-cloudformation-template.json) 20 | 21 | (or use `sls deploy`). 22 | 23 | 4. Create a Pull Request on your project, and see the magic be invoked 😎 24 | 5. ... 25 | 6. Profit! 26 | 27 | Architecture 28 | ------------ 29 | ![Flow](https://raw.githubusercontent.com/svdgraaf/github-codebuild-webhook/master/architecture.png) 30 | 31 | When you create a PR, a notification is send to the Api Gateway endpoint and the lambda step function is triggered. This will trigger the start of a build for your project, and set a status of `pending` on your specific commit. Then it will start checking your build status every X seconds. When the status of the build changes to `done` or `failed`, the github api is called and the PR status will be updated accordingly. 32 | 33 | Example output 34 | -------------- 35 | In the Example below, a PR is create, and a build is run which fails. Then, a new commit is pushed, which fixes the build. When you click on the 'details' link of the PR status, it will take you to the CodeBuild build log. 36 | 37 | ![AWS Codebuild Triggered after PR update](https://github.com/svdgraaf/github-codebuild-webhook/blob/master/example.gif?raw=true) 38 | 39 | Todo 40 | ---- 41 | * Add (optional) junit parsing, so it can comment on files with (possible) issues. 42 | * Perhaps make build project dynamic through apigateway variable if possible 43 | -------------------------------------------------------------------------------- /architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svdgraaf/github-codebuild-webhook/1242f952c379cda29647fc919f1baa56f2eb449a/architecture.png -------------------------------------------------------------------------------- /example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svdgraaf/github-codebuild-webhook/1242f952c379cda29647fc919f1baa56f2eb449a/example.gif -------------------------------------------------------------------------------- /handler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var AWS = require('aws-sdk'); 4 | var codebuild = new AWS.CodeBuild(); 5 | 6 | var GitHubApi = require("github"); 7 | var github = new GitHubApi(); 8 | 9 | // setup github client 10 | github.authenticate({ 11 | type: "basic", 12 | username: process.env.GITHUB_USERNAME, 13 | password: process.env.GITHUB_ACCESS_TOKEN 14 | }); 15 | 16 | // get the region where this lambda is running 17 | var region = process.env.AWS_DEFAULT_REGION; 18 | 19 | // this function will be triggered by the github webhook 20 | module.exports.start_build = (event, context, callback) => { 21 | 22 | var response; 23 | 24 | // we only act on pull_request changes (can be any, but we don't need those) 25 | if('pull_request' in event) { 26 | 27 | var head = event.pull_request.head; 28 | var repo = head.repo; 29 | 30 | var params = { 31 | projectName: process.env.BUILD_PROJECT, 32 | sourceVersion: head.sha 33 | }; 34 | 35 | // start the codebuild process for this project 36 | codebuild.startBuild(params, function(err, data) { 37 | if (err) { 38 | console.log(err, err.stack); 39 | callback(err); 40 | } else { 41 | 42 | var build = data.build; 43 | 44 | // all is well, mark the commit as being 'in progress' 45 | github.repos.createStatus({ 46 | owner: repo.owner.login, 47 | repo: repo.name, 48 | sha: head.sha, 49 | state: 'pending', 50 | target_url: 'https://' + region + '.console.aws.amazon.com/codebuild/home?region=' + region + '#/builds/' + data.build.id + '/view/new', 51 | context: 'CodeBuild', 52 | description: 'Build is running...' 53 | }).then(function(data){ 54 | console.log(data); 55 | }); 56 | callback(null, build); 57 | } 58 | }); 59 | } else { 60 | callback("Not a PR"); 61 | } 62 | } 63 | 64 | module.exports.check_build_status = (event, context, callback) => { 65 | var params = { 66 | ids: [event.id] 67 | } 68 | codebuild.batchGetBuilds(params, function(err, data) { 69 | if (err) { 70 | console.log(err, err.stack); 71 | context.fail(err) 72 | callback(err); 73 | } else { 74 | callback(null, data.builds[0]); 75 | } 76 | }); 77 | } 78 | 79 | module.exports.build_done = (event, context, callback) => { 80 | // get the necessary variables for the github call 81 | var url = event.source.location.split('/'); 82 | var repo = url[url.length-1].replace('.git', ''); 83 | var username = url[url.length-2]; 84 | 85 | console.log('Found commit identifier: ' + event.sourceVersion); 86 | var state = ''; 87 | 88 | // map the codebuild status to github state 89 | switch(event.buildStatus) { 90 | case 'SUCCEEDED': 91 | state = 'success'; 92 | break; 93 | case 'FAILED': 94 | state = 'failure'; 95 | break; 96 | case 'FAULT': 97 | case 'STOPPED': 98 | case 'TIMED_OUT': 99 | state = 'error' 100 | default: 101 | state = 'pending' 102 | } 103 | console.log('Github state will be', state); 104 | 105 | github.repos.createStatus({ 106 | owner: username, 107 | repo: repo, 108 | sha: event.sourceVersion, 109 | state: state, 110 | target_url: 'https://' + region + '.console.aws.amazon.com/codebuild/home?region=' + region + '#/builds/' + event.id + '/view/new', 111 | context: 'CodeBuild', 112 | description: 'Build ' + event.buildStatus + '...' 113 | }).catch(function(err){ 114 | console.log(err); 115 | context.fail(data); 116 | }); 117 | } 118 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-codebuild-webhook", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "aws-sdk": "^2.40.0", 6 | "github": "^9.2.0" 7 | }, 8 | "devDependencies": { 9 | "serverless": "^1.12.1", 10 | "serverless-step-functions": "^1.0.2", 11 | "serverless-parameters": "^0.0.3", 12 | "serverless-pseudo-parameters": "^1.1.5", 13 | "serverless-portable-templates": "file:./plugins/serverless-portable-templates" 14 | }, 15 | "license": "MIT", 16 | "repository": { 17 | "git": "https://github.com/svdgraaf/github-codebuild-webhook" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /plugins/serverless-portable-templates/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const chalk = require('chalk'); 5 | 6 | 7 | class ServerlessPortableTemplates { 8 | constructor(serverless, options) { 9 | this.serverless = serverless; 10 | this.options = options; 11 | this.hooks = { 12 | 'before:deploy:deploy': this.makePortableTemplate.bind(this) 13 | }; 14 | } 15 | 16 | makePortableTemplate() { 17 | const template = this.serverless.service.provider.compiledCloudFormationTemplate; 18 | var serviceName = this.serverless.service.service; 19 | 20 | // remove the RoleName, so CloudFormation will generate one for us 21 | delete template['Resources']['IamRoleLambdaExecution']['Properties']['RoleName']; 22 | 23 | _.each(template['Resources'], function(resource, name){ 24 | 25 | // remove the name for loggroups, so CF will generate a name for us (actually) 26 | // those resources are not needed at all, but it's more work to remove them 27 | if(resource['Type'] == 'AWS::Logs::LogGroup') { 28 | delete resource['Properties']['LogGroupName'] 29 | template['Resources'][name] = resource 30 | console.log('Portable templates: ' + chalk.yellow(name + ' removed LogGroupName')); 31 | } 32 | 33 | // replace the service name with the stack name so the functions will be unique 34 | // per account 35 | if(resource['Type'] == 'AWS::Lambda::Function') { 36 | resource['Properties']['FunctionName'] = resource['Properties']['FunctionName'].replace(serviceName, "#{AWS::StackName}") 37 | template['Resources'][name] = resource 38 | console.log('Portable templates: ' + chalk.yellow(name + ' Added Pseudo Parameter for FunctionName')); 39 | } 40 | 41 | }) 42 | } 43 | } 44 | 45 | module.exports = ServerlessPortableTemplates; 46 | -------------------------------------------------------------------------------- /plugins/serverless-portable-templates/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-portable-templates", 3 | "version": "1.2.11", 4 | "devDependencies": {}, 5 | "license": "MIT", 6 | "repository": { 7 | "git": "https://github.com/svdgraaf/serverless-portable-templates" 8 | }, 9 | "main": "index.js", 10 | "dependencies": { 11 | "chalk": "^1.1.1", 12 | "lodash": "^4.13.1" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | service: github-webhook 2 | 3 | plugins: 4 | - serverless-step-functions 5 | - serverless-parameters 6 | - serverless-portable-templates 7 | - serverless-pseudo-parameters 8 | 9 | custom: 10 | parameters: 11 | GithubUsername: 12 | Type: String 13 | Default: ${env:GITHUB_USERNAME} 14 | Description: Github username, this is the user which will be displayed 15 | GithubRepository: 16 | Type: String 17 | Default: ${env:GITHUB_REPOSITORY} 18 | Description: "Github repository url, eg: https://github.com/[username]/[repository]" 19 | GithubAccessToken: 20 | Type: String 21 | Default: ${env:GITHUB_ACCESS_TOKEN} 22 | Description: Your generated github access token 23 | NoEcho: true 24 | CodebuildProject: 25 | Type: String 26 | Default: ${env:BUILD_PROJECT} 27 | Description: Name of the build project that should be triggered 28 | 29 | provider: 30 | name: aws 31 | runtime: nodejs6.10 32 | stage: trigger 33 | region: eu-west-1 34 | versionFunctions: false 35 | deploymentBucket: github-webhook-artifacts-${self:provider.region} 36 | 37 | # you can add statements to the Lambda function's IAM Role here 38 | iamRoleStatements: 39 | - Effect: "Allow" 40 | Action: 41 | - codebuild:* 42 | Resource: '*' 43 | 44 | environment: 45 | BUILD_PROJECT: 46 | Ref: CodebuildProject 47 | GITHUB_USERNAME: 48 | Ref: GithubUsername 49 | GITHUB_ACCESS_TOKEN: 50 | Ref: GithubAccessToken 51 | GITHUB_REPOSITORY: 52 | Ref: GithubRepository 53 | 54 | functions: 55 | start-build: 56 | handler: handler.start_build 57 | 58 | check-build-status: 59 | handler: handler.check_build_status 60 | 61 | build-done: 62 | handler: handler.build_done 63 | 64 | webhook-resource: 65 | handler: webhook.resource 66 | 67 | resources: 68 | Resources: 69 | GithubWebhook: 70 | Type: Custom::GithubWebhook 71 | Properties: 72 | ServiceToken: 73 | Fn::GetAtt: 74 | - WebhookDashresourceLambdaFunction 75 | - Arn 76 | Endpoint: 77 | Fn::Join: 78 | - "" 79 | - 80 | - https:// 81 | - Ref: "ApiGatewayRestApi" 82 | - .execute-api.eu-west-1.amazonaws.com/${self:provider.stage}/trigger-build/ 83 | 84 | Outputs: 85 | TriggerEndpoint: 86 | Value: 87 | Fn::Join: 88 | - "" 89 | - 90 | - https:// 91 | - Ref: "ApiGatewayRestApi" 92 | - .execute-api.eu-west-1.amazonaws.com/${self:provider.stage}/trigger-build/ 93 | 94 | stepFunctions: 95 | stateMachines: 96 | build-for-commit: 97 | events: 98 | - http: 99 | path: trigger-build 100 | method: POST 101 | definition: 102 | Comment: "Check for build status for the given build project, and mark it when done on GH" 103 | StartAt: start_build 104 | States: 105 | start_build: 106 | Type: Task 107 | Resource: "arn:aws:lambda:#{AWS::Region}:#{AWS::AccountId}:function:#{AWS::StackName}-${opt:stage}-start-build" 108 | Next: check_build_status 109 | wait_a_bit: 110 | Type: Wait 111 | Seconds: 5 112 | Next: check_build_status 113 | check_build_status: 114 | Type: Task 115 | Resource: "arn:aws:lambda:#{AWS::Region}:#{AWS::AccountId}:function:#{AWS::StackName}-${opt:stage}-check-build-status" 116 | Next: check_build_status_outcome 117 | check_build_status_outcome: 118 | Type: Choice 119 | Choices: 120 | - Variable: "$.buildComplete" 121 | BooleanEquals: true 122 | Next: build_done 123 | Default: wait_a_bit 124 | build_done: 125 | Type: Task 126 | Resource: "arn:aws:lambda:#{AWS::Region}:#{AWS::AccountId}:function:#{AWS::StackName}-${opt:stage}-build-done" 127 | End: true 128 | -------------------------------------------------------------------------------- /webhook.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var AWS = require('aws-sdk'); 4 | var codebuild = new AWS.CodeBuild(); 5 | 6 | var GitHubApi = require("github"); 7 | var github = new GitHubApi(); 8 | 9 | // setup github client 10 | github.authenticate({ 11 | type: "basic", 12 | username: process.env.GITHUB_USERNAME, 13 | password: process.env.GITHUB_ACCESS_TOKEN 14 | }); 15 | 16 | // get the region where this lambda is running 17 | var region = process.env.AWS_DEFAULT_REGION; 18 | var repo = process.env.GITHUB_REPOSITORY.split('/'); 19 | 20 | 21 | module.exports.resource = (event, context, callback) => { 22 | console.log("REQUEST RECEIVED:\n" + JSON.stringify(event)); 23 | 24 | // For Delete requests, immediately send a SUCCESS response. 25 | if (event.RequestType == "Delete") { 26 | // TODO remove webhook from repo 27 | sendResponse(event, context, "SUCCESS"); 28 | return; 29 | } else { 30 | var data = { 31 | owner: repo[3], 32 | repo: repo[4], 33 | name: 'web', 34 | events: ['pull_request'], 35 | active: true, 36 | config: { 37 | url: event.ResourceProperties.Endpoint, 38 | content_type:"json" 39 | } 40 | }; 41 | 42 | if(event.RequestType == "Create") { 43 | github.repos.createHook(data).then(function(data){ 44 | sendResponse(event, context, "SUCCESS", {}); 45 | }).catch(function(err){ 46 | console.log(err); 47 | sendResponse(event, context, "FAILED", {}); 48 | }); 49 | 50 | } else { 51 | github.repos.editHook(data).then(function(data){ 52 | sendResponse(event, context, "SUCCESS", {}); 53 | }).catch(function(err){ 54 | console.log(err); 55 | sendResponse(event, context, "FAILED", {}); 56 | });; 57 | } 58 | } 59 | // var responseStatus = "FAILED"; 60 | // var responseData = {}; 61 | // sendResponse(event, context, responseStatus, responseData); 62 | } 63 | 64 | // Send response to the pre-signed S3 URL 65 | function sendResponse(event, context, responseStatus, responseData) { 66 | 67 | var responseBody = JSON.stringify({ 68 | Status: responseStatus, 69 | Reason: "See the details in CloudWatch Log Stream: " + context.logStreamName, 70 | PhysicalResourceId: context.logStreamName, 71 | StackId: event.StackId, 72 | RequestId: event.RequestId, 73 | LogicalResourceId: event.LogicalResourceId, 74 | Data: responseData 75 | }); 76 | 77 | console.log("RESPONSE BODY:\n", responseBody); 78 | 79 | var https = require("https"); 80 | var url = require("url"); 81 | 82 | var parsedUrl = url.parse(event.ResponseURL); 83 | var options = { 84 | hostname: parsedUrl.hostname, 85 | port: 443, 86 | path: parsedUrl.path, 87 | method: "PUT", 88 | headers: { 89 | "content-type": "", 90 | "content-length": responseBody.length 91 | } 92 | }; 93 | 94 | console.log("SENDING RESPONSE...\n"); 95 | 96 | var request = https.request(options, function(response) { 97 | console.log("STATUS: " + response.statusCode); 98 | console.log("HEADERS: " + JSON.stringify(response.headers)); 99 | // Tell AWS Lambda that the function execution is done 100 | context.done(); 101 | }); 102 | 103 | request.on("error", function(error) { 104 | console.log("sendResponse Error:" + error); 105 | // Tell AWS Lambda that the function execution is done 106 | context.done(); 107 | }); 108 | 109 | // write data to request body 110 | request.write(responseBody); 111 | request.end(); 112 | } 113 | --------------------------------------------------------------------------------