├── .gcloudignore ├── LICENSE ├── README.md ├── index.js └── package.json /.gcloudignore: -------------------------------------------------------------------------------- 1 | # This file specifies files that are *not* uploaded to Google Cloud Platform 2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of 3 | # "#!include" directives (which insert the entries of the given .gitignore-style 4 | # file at that point). 5 | # 6 | # For more information, run: 7 | # $ gcloud topic gcloudignore 8 | # 9 | .gcloudignore 10 | # If you would like to upload your .git directory, .gitignore file or files 11 | # from your .gitignore file, remove the corresponding line 12 | # below: 13 | .git 14 | .gitignore 15 | 16 | node_modules 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 leigh schrandt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Container Builder GitHub CI connector for Google Cloud Functions 2 | Cloud Function to use the PubSub events on the `cloud-builds` topic to update GitHub CI status. 3 | 4 | This function is designed to work with GitHub repositories mirrored under Google Cloud Source Control named `github-${config.repoOwner}-${ghRepoName}`. 5 | If you set this through the Container Builder build trigger UI, this will be named automatically. 6 | 7 | ## Deploy 8 | [Generate a new token](https://github.com/settings/tokens) with the `repo:status` OAuth scope. 9 | 10 | Set the following on `config` in [index.js](./index.js): 11 | - `ciUser` 12 | - `ciAccessToken` 13 | - `repoOwner` 14 | 15 | Deploy the cloud function to gcloud: 16 | ``` 17 | gcloud beta functions deploy setCIStatus --trigger-topic cloud-builds 18 | ``` 19 | 20 | ## Behavior 21 | CI Status **context** will be one of: 22 | - `${projectId}/gcb: ${tags.join('/')}` 23 | - `${projectId}/gcb: ${id.substring(0,8)}` 24 | 25 | Use the `tags` field in your build request to name your CI. 26 | Otherwise, it falls back to the build-GUID. 27 | 28 | CI Status **description** will either be: 29 | - nothing 30 | - a join of all images to be published: 31 | `gcr.io/project/image:v1 gcr.io/project/image:latest` 32 | - above all, the duration of the build: 33 | `3m 27s` 34 | - possibly with the last-running build step and error status: 35 | `3m 27s · test failure` 36 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const GitHubApi = require('github') 2 | 3 | const config = { 4 | ciUser: "githubUser", // document your CI user here 5 | ciAccessToken: "personalAccessToken", // document which token you're using 6 | repoOwner: "beatport" 7 | } 8 | 9 | // Can be one of error, failure, pending, or success. 10 | const statusMap = { 11 | QUEUED: "pending", 12 | WORKING: "pending", 13 | SUCCESS: "success", 14 | FAILURE: "failure", 15 | CANCELLED: "failure", 16 | TIMEOUT: "error", 17 | INTERNAL_ERROR: "error" 18 | } 19 | 20 | // setCIStatus is the main function. 21 | module.exports.setCIStatus = (event) => { 22 | build = eventToBuild(event.data.data) 23 | console.log(`gcloud builds describe --format=json ${build.id}`) 24 | 25 | const { 26 | id, 27 | projectId, 28 | status, 29 | steps, 30 | images, 31 | sourceProvenance: { 32 | resolvedRepoSource: repoSource 33 | }, 34 | logUrl, 35 | tags, 36 | createTime, 37 | finishTime, 38 | } = build 39 | 40 | const ghStatus = statusMap[status] 41 | 42 | if (!repoSource || !ghStatus) return 43 | 44 | const ghRepo = repoSource.repoName.indexOf(`github-${config.repoOwner}-`) == 0 45 | ? repoSource.repoName.replace(`github-${config.repoOwner}-`, '') 46 | : false 47 | if (!ghRepo) return 48 | 49 | const prettyTags = tags && tags.filter(t => !t.match(/(event|trigger|eval|invocation)-[\w-]{36}/)) 50 | const ghContext = prettyTags && prettyTags.length > 0 51 | ? `${projectId}/gcb: ${prettyTags.join('/')}` 52 | : `${projectId}/gcb: ${id.substring(0,8)}` 53 | 54 | const lastStep = steps.filter( s => s.timing && s.timing.startTime ).pop() 55 | const failureDescription = (ghStatus=='failure' || ghStatus=='error') 56 | ? ' · ' + (lastStep ? `${lastStep.id} `:'') + status.toLowerCase() 57 | : '' 58 | const ghDescription = ( 59 | createTime && finishTime 60 | ? secondsToString((new Date(finishTime) - new Date(createTime)) / 1000) + failureDescription 61 | : images && images.length > 0 62 | ? `${images.join('\n')}` 63 | : '' 64 | ).substring(0,140) 65 | 66 | console.log(status, ghStatus) 67 | console.log(ghRepo, repoSource) 68 | console.log(ghContext, tags) 69 | console.log(ghDescription, createTime, finishTime, images) 70 | 71 | let github = new GitHubApi() 72 | github.authenticate({ 73 | type: 'token', 74 | token: config.ciAccessToken 75 | }) 76 | 77 | let request = { 78 | owner: config.repoOwner, 79 | repo: ghRepo, 80 | sha: repoSource.commitSha, 81 | state: ghStatus, 82 | target_url: logUrl, 83 | description: ghDescription, 84 | context: ghContext 85 | } 86 | 87 | console.log(JSON.stringify(request, null, 2)) 88 | 89 | return github.repos.createStatus(request) 90 | } 91 | 92 | // eventToBuild transforms pubsub event message to a build object. 93 | const eventToBuild = (data) => 94 | JSON.parse(new Buffer(data, 'base64').toString()) 95 | 96 | // secondsToString turns a number of seconds into a human-readable duration. 97 | const secondsToString = (s) => { 98 | const years = Math.floor(s / 31536000) 99 | const days = Math.floor((s % 31536000) / 86400) 100 | const hours = Math.floor(((s % 31536000) % 86400) / 3600) 101 | const minutes = Math.floor((((s % 31536000) % 86400) % 3600) / 60) 102 | const seconds = Math.floor((((s % 31536000) % 86400) % 3600) % 60) 103 | 104 | return `${years}y ${days}d ${hours}h ${minutes}m ${seconds}s` 105 | .replace(/^(0[ydhm] )*/g, '') 106 | } 107 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "container-builder-github-ci-status", 3 | "version": "0.1.0", 4 | "description": "GitHub CI integration for Google Cloud Container Builder, using Google Cloud Functions /w PubSub", 5 | "main": "index.js", 6 | "dependencies": { 7 | "github": "12.0.3" 8 | } 9 | } 10 | --------------------------------------------------------------------------------