At ActiveViam, we’ve recently updated our commit process to take advantage of the possibilities offered by the new GitHub Apps to enforce a Git commit convention throughout the company’s repositories. This blog post will describe how we deployed our GitHub App as an Azure function written in JavaScript, how it improved our process and what we learned along the way. For those interested in making their own GitHub App, this post can act as a tutorial.

The “Git check” App flags the pull request when one of its commits do not follow the Git commit convention.

ActiveViam’s Context

At ActiveViam, our Git commit convention is to prefix our commit titles by one or several unresolved JIRA issue keys and thus requires calling the JIRA API. It allows us to track which issue motivated a change in the codebase.

Until recently, we relied on a pre-receive hook configured on our internal self-hosted Gitolite server to enforce this rule. The hook would get triggered every time someone pushed some commits to a repository and would reject the push if some commit messages were invalid.

We decided to switch to GitHub in order to improve code quality and shared knowledge of the codebase. We wanted to achieve that by adopting a systematic code review process based on the well-known GitHub pull request feature.

The details of why we made the switch and how our workflow looks like now will be part of a future blog post. Stay tuned!

However, GitHub does not support pre-receive hooks. The alternative is to attach statuses to commits part of a pull request. To do this, we have to use the GitHub API and we decided to do it through a GitHub App.

GitHub App vs. OAuth application vs. bot account

At the time of this writing, GitHub Apps have just been released. They provide a new way for organizations to extend GitHub. Indeed, there were already OAuth applications that could call the GitHub API on behalf of a user but since an App uses its own identity when performing its function, it can provide a service to the entire organization. With Apps you do not have to worry about a user leaving the company and breaking every OAuth application interacting with the organization’s repositories on his behalf. Before the introduction of GitHub Apps, you could also have achieved this with a GitHub bot account, but you would have had to pay for its seat within the organization. For a complete list of the differences between GitHub Apps and OAuth applications, take a look at the GitHub developer reference.

Creating the Azure Function

GitHub Apps work by subscribing to one or more webhook events. For instance, an App could subscribe to the Membership event and get triggered every time users are added to a team to greet them. Webhooks consist in regular HTTP POST requests with a signed payload delivered to a customizable URL. It is thus a perfect fit for the event-based and serverless compute experience provided by Azure Functions or AWS Lambda.

Follow the first part (stop at the GitHub screenshots) of the Microsoft guide to create a JavaScript function triggered by a GitHub Webhook. Once you have your function URL and GitHub secret we are ready for the next step.

Registering the GitHub App

Follow the GitHub guide to register your App. Use the function URL from Azure as the Webhook URL and the GitHub secret as the Webhook secret. Give your App the name, description, homepage and callback URL you want and the following permissions:

The App needs write access on commit statuses to flag invalid pull requests.

The App needs to get triggered every time a pull request is opened or synchronized.

Generate a private key for your App and keep it nearby because we will need it during the next steps.

Developing the GitHub App

Our new Azure Function starts with two files: function.js and index.js . While we can leave function.js untouched, index.js need to be changed:

const checkCommitTitles = require('./git-commit-convention'); const handleEvent = require('./github'); // Convert the result of checkCommitTitles to a status payload, // see https://developer.github.com/v3/repos/statuses/#parameters. const getPullRequestStatus = commitTitles => checkCommitTitles(commitTitles) .then(() => ({ description: 'All the pull request commits follow our Git commit convention.', state: 'success' })) .catch(error => ({ description: String(error), state: 'error' })); module.exports = (context, payload) => { handleEvent(payload, getPullRequestStatus).then( message => { context.res = {body: message}; context.done(); }, error => { const errorMessage = String(error); context.res = {body: errorMessage, status: 500}; context.done(errorMessage); } ); };

In this post, to keep it simple, we will not use ActiveViam’s specific Git commit convention but instead enforce a subpart of the popular AngularJS Git commit convention and only make sure that commit titles match their type(scope) text pattern:

const validTitlePattern = /(feat|fix|docs|style|refactor|test|chore)\(\S+\) .+/; module.exports = commitTitles => { const invalidTitle = commitTitles.find( title => !validTitlePattern.test(title) ); // Our implementation is synchronous but still return a Promise // to allow more complex checks involving calls to an HTTP API for instance. return invalidTitle ? Promise.reject( new Error( `The title "${invalidTitle}" does not follow our Git commit convention.` ) ) : Promise.resolve(); };

Now, we want to interact with the GitHub API. We install ActiveViam’s fetch-github-app package that handle the GitHub App authentication and wraps the Fetch Api:

npm init && npm install --save fetch-github-app

We can then write our last file, github.js :

const fetchGithubApp = require('fetch-github-app'); const getEnvOrThrow = (name, description) => { if (!process.env.hasOwnProperty(name)) { throw new Error(`Missing environment variable ${name}. ${description}`); } return process.env[name]; }; // We get it all from environment variables to be compliant // with https://12factor.net/config. const config = { appId: Number( getEnvOrThrow( 'GITHUB_APP_ID', 'Can be found in the "GitHub Apps" section of the organization settings.' ) ), // Using base64 encoding in order not to deal with cumbersome EOL escaping. appPrivateKey: Buffer.from( getEnvOrThrow( 'GITHUB_APP_PEM_BASE64_ENCODED', 'See https://developer.github.com/apps/building-integrations/setting-up-and-registering-github-apps/registering-github-apps/#generating-a-private-key.' ), 'base64' ).toString('utf8'), userAgent: getEnvOrThrow( 'GITHUB_USER_AGENT', 'See https://developer.github.com/v3/#user-agent-required.' ) }; // Use the API documented at: // https://developer.github.com/v3/repos/statuses/#create-a-status. const createStatus = ({githubApp, status, statusUrl}) => githubApp.fetch(statusUrl, {body: status, method: 'POST'}).then(body => { if (!body.id) { throw new Error(`GitHub responded with: ${JSON.stringify(body)}`); } return `Status created successfully at ${statusUrl}.`; }); const updatePullRequestStatus = ({ commitsUrl, getPullRequestStatus, githubApp, statusUrl, }) => githubApp.fetch(commitsUrl).then(body => { const commitTitles = body.map(({commit}) => commit.message.split(`

`)[0]); return getPullRequestStatus(commitTitles).then(({description, state}) => { const status = {context: 'Git check', description, state}; return createStatus({githubApp, status, statusUrl}); }); }); module.exports = (payload, getPullRequestStatus) => { // Our GitHub App only subscribes to the PullRequestEvent // so we only have to check the action. // A pull request is synchronized when a new push happens in its tracked branch. // See https://developer.github.com/v3/activity/events/types/#pullrequestevent. const isOpenedOrSynchronizedPullRequestEvent = payload.action === 'opened' || payload.action === 'synchronize'; if (!isOpenedOrSynchronizedPullRequestEvent) { return Promise.resolve( `Nothing to do with this "${payload.action}" event.` ); } return fetchGithubApp( Object.assign({installationId: payload.installation.id}, config) ).then(githubApp => updatePullRequestStatus({ commitsUrl: payload.pull_request.commits_url, getPullRequestStatus, githubApp, statusUrl: payload.pull_request.statuses_url, }) ); };

Configuring the Azure Function environment

In github.js , all the configuration is taken from environment variables. As explained by the Twelve-Factor App, this is a good practice as it makes it possible to change the configuration without changing any code and for the most part avoids checking plain secrets into source control.

To configure environment variables for our Azure Function, we open the Application settings panel:

Add the GITHUB_APP_ID , GITHUB_APP_PEM_BASE64_ENCODED , and GITHUB_USER_AGENT entries.

The private key generated by GitHub during the App registration contains end-of-line characters that would be stripped when pasting the private key in the Value field, therefore making it unusable. To prevent this, base64 encodes the private key and use the result as the value of GITHUB_APP_PEM_BASE64_ENCODED .

Ideally, we would want to store GITHUB_APP_PEM_BASE64_ENCODED in an Azure Key Vault but it is not natively supported yet. In the meantime, we suggest making sure to limit access to the Azure Function App to the organization’s administrators.

Deploying the Azure Function

Edit February 6 2018: Azure Functions can now be deployed with a simple zip push we recommend this technique over the one detailed in the rest of this section.

Now is the time to automate the deployment of our App to Azure. While there are many ways to deploy code to Azure, one of the most lightweight and least intrusive techniques is simply to upload our JavaScript files with FTP. It is convenient as it also allows us to upload our node_modules directory which contains our fetch-github-app dependencies. Otherwise, we would have to take steps like opening the Azure Console and running npm install through the Azure Portal for each deploy, at least until Azure natively runs npm install before function execution.

We use ftp-deploy-package to handle the FTP upload. However, this time it will just be a development dependency as our Azure Function does not need it at run time:

npm install --save-dev ftp-deploy-package

We also add a deploy entry to the scripts section of the package.json so in the end it is:

{ "dependencies": { "fetch-github-app": "^0.1.0" }, "devDependencies": { "ftp-deploy-package": "^0.2.0" }, "engines": { "node": ">= 6.11.0" }, "files": [ "function.json", "git-commit-convention.js", "github.js", "index.js" ], "name": "git-check", "private": true, "scripts": { "deploy": "ftp-deploy-package --secure" }, "version": "0.1.0" }

In order to call this deploy script, we need additional command line options. For this we follow the Azure guide to configure deployment credentials. The password we choose will be the one we have to give when prompted by the script. The values of the user and host options can be found in the Properties panel. The path is something like this: site/wwwroot/yourFunctionName where yourFunctionName is the one next to the f symbol (not the ⚡) on the Azure Portal.

Once we have all the options, we can finally upload the App code to Azure:

npm run deploy -- --host yourSubdomain.ftp.azurewebsites.windows.net --path site/wwwroot/yourFunctionName --user "functionAppName\deploymentUser"

Installing the GitHub App

The last remaining step is to actually use the App by installing it. Open the GitHub Apps page under the Developer settings of the organization settings. Once installed, select the repositories on which you want the App to operate.

We can display a summary of the permissions required by the App and which repositories it has access to.

Wrapping up

That’s it! We have registered, developed and installed our own GitHub App, created and configured the Azure Function running it and we deployed it via FTP.

Now, every time a pull request is opened or updated, our GitHub App will wake up and check that all its commits follow our Git commit convention.

At ActiveViam, we have also set up protected branches which are good practices to make really sure that all the commits pushed to our main branches respect our Git commit convention.