Notifying BitBucket of your Amplify Build Status

Written by Tom Wilkins
Sat, 20 Jun 2020

Ensure that only code passing all tests can be merged into your production branch

Notifying BitBucket of your Amplify Build Status

Amplify is a development platform from Amazon that I've been getting hands-on with a lot lately. One killer feature is that Amplify can build, test and deploy your app in the cloud - both for your production branch and your pull requests.

In fact, with Previews, Amplify can run the code from open pull requests through the full build and test pipeline and publish to a unique URL, so you and your colleagues can see what your code will be doing. All resources will be torn down when the pull request is closed.

This is all really cool, but wouldn't it be great if our version control system was aware of the build status? After all, this is where the pull request will be reviewed and ideally we want to prevent bad code being merged into the production branch.

Disclaimer

The following solution is a workaround. There is a feature request open to Provide build and deploy status for BitBucket repos and if you use GitHub, this feature is already available.

At the time of writing, this feature is not available if your repository is in BitBucket, so I came up with the workaround below.

Amplify Build Configuration

Let's say you have an Amplify configuration like the below. Under test, you have the command to run your integration tests through Cypress.

version: 0.1
frontend:
phases:
preBuild:
commands:
- npm install
build:
commands:
- npm run build
artifacts:
baseDirectory: app
files:
- "**/*"
cache:
paths:
- node_modules/**/*
test:
artifacts:
baseDirectory: cypress
configFilePath: "**/mochawesome.json"
files:
- "**/*.png"
- "**/*.mp4"
phases:
preTest:
commands:
- npm install
- npm install wait-on
- npm install mocha mochawesome mochawesome-merge mochawesome-report-generator
- "npm start & npx wait-on http://127.0.0.1:8080"
test:
commands:
- 'npx cypress run --reporter mochawesome --reporter-options "reportDir=cypress/report/mochawesome-report,overwrite=false,html=false,json=true,timestamp=mmddyyyy_HHMMss"'
postTest:
commands:
- npx mochawesome-merge@4 cypress/report/mochawesome-report/*.json > cypress/report/mochawesome.json

Although Amplify will know the outcome of the tests, our build script has no idea. So how will we tell BitBucket?

As you can see from the build script, Amplify is using Node.js and NPM. This means that we can create a Node.js JavaScript file and run it as part of the build.

Executing a Child Process for the Test Runner

Inside this file, we'll be using exec from the child_process module. This allows us to run terminal commands, and execute a callback function when they complete. On completion, we can check if an error code is returned to determine if any tests failed.

If Cypress finds any failing tests, the child process will exit with a code above 0, just like any other process where something went wrong.

What our Node.js script will do is execute the Cypress command as a child process and handle the outcome based on whether an error was returned.

run-test.js

const { exec } = require("child_process");
const command = 'npx cypress run --reporter mochawesome --reporter-options "reportDir=cypress/report/mochawesome-report,overwrite=false,html=false,json=true,timestamp=mmddyyyy_HHMMss"';

exec(command, (err, stdout, stderr) => {
console.log(stdout);
console.log(stderr);
const exitCode = (err && err.code) || 0;
// do something with the result

// exit the process with the exitCode returned from the test runner
process.exit(exitCode);
});

Notifying BitBucket

No we've intercepted the result of the tests and can do something with the result, so let's add something to notify BitBucket.

For this, we'll use the Build Status API from BitBucket.

To update the status of a build, we'll need to provide BitBucket with a few things, namely:

  • The name of the workspace
  • The name of the repository
  • The commit ID
  • An authorization key

For the first two, we can make these available to the script in the Environment Variables section of the Amplify console.

Amplify includes the commit ID as one of the default environment variables.

We'll need to generate an authorization key ourselves. I did this by creating an App key and then base64 encoding it with my username, then setting the result as an environment variable in the Amplify console. All environment variables in Amplify are completely secure.

In the below script, I've created a BitBucketNotifier function returning an object that will allow me to update the status relating to the test runner specified. This way, the notifier can be used for multiple test runners - for example unit tests in Jest, and Integration tests in Cypress.

We can also specify a URL to BitBucket, so the build status shown in the pull request can link somewhere. For this, we'll also bring in the AWS_APP_ID from the default environment variables so we can link to the preview section of the Amplify console.

The updateState function returns a promise so we know when the request has finished and if it was successful, this will be useful when we integrate it with the test script.

bitbucket-notifier.js

const https = require("https");

function BitBucketNotifier(testRunner) {
return {
updateState: function(state) {
const { AWS_APP_ID, AWS_COMMIT_ID, BITBUCKET_REPOSITORY, BITBUCKET_TOKEN, BITBUCKET_WORKSPACE } = process.env;
const url = `https://eu-west-2.console.aws.amazon.com/amplify/home?region=eu-west-2#/${AWS_APP_ID}/settings/previews`;
const options = {
method: "POST",
hostname: "api.bitbucket.org",
path: `/2.0/repositories/${BITBUCKET_WORKSPACE}/${BITBUCKET_REPOSITORY}/commit/${AWS_COMMIT_ID}/statuses/build`,
headers: {
"Content-Type": "application/json",
Authorization: `Basic ${BITBUCKET_TOKEN}`
},
maxRedirects: 20
};

return new Promise((resolve, reject) => {
const req = https.request(options, function(res) {
const chunks = [];

res.on("data", function(chunk) {
chunks.push(chunk);
});

res.on("end", () => {
const body = Buffer.concat(chunks);
resolve(body.toString());
});

res.on("error", reject);
});

const postData = JSON.stringify({
state,
key: testRunner,
name: "Amplify Build",
url,
description: "Triggered from the Amplify Console"
});

req.write(postData);

req.end();
});
}
};
}

module.exports = {
BitBucketNotifier
};

Integrating the Notifier into the Test Runner

Let's edit the test script to include the notifier. Below a new instance of the BitBucketNotifier has been created, specifying cypress as the test runner.

Before executing the command, we update the state to INPROGRESS, as per BitBucket's status specifications.

When the child process has finished, we create an exit code based on whether or not there was an error. If there isn't an error, we'll use 0.

This is then used to determine if the build was SUCCESSFUL or if it FAILED. We'll use the notifier again to send this state to BitBucket.

We'll then log the response from BitBucket to the console, so it's available as part of the build output, and then attach a finally to ensure we exit this script with the same exit code provided by the test runner child process, whether or not the BitBucket notifier was successful - we don't want the Amplify build to be dependent on the notifier.

const { BitBucketNotifier } = require('./bitbucket-notifier');
const { exec } = require("child_process");
const bitBucketNotifier = new BitBucketNotifier('cypress');
bitBucketNotifier.updateState('INPROGRESS');
const command = 'npx cypress run --reporter mochawesome --reporter-options "reportDir=cypress/report/mochawesome-report,overwrite=false,html=false,json=true,timestamp=mmddyyyy_HHMMss"';

exec(command, (err, stdout, stderr) => {
console.log(stdout);
console.log(stderr);
const exitCode = (err && err.code) || 0;
const state = exitCode === 0 ? 'SUCCESSFUL' : 'FAILED';
bitBucketNotifier.updateState(state)
.then(data => console.log(data))
.finally(() => process.exit(exitCode));
});

Updating Amplify Build Configuration

Now that the test script is ready, we can add it to the package.json in the source of the repository:

"scripts": {
"run test": "node run-test"
}

And then update the amplify.yml build configuration to run the script:

test:
commands:
- npm run test

Testing it works

  1. Make sure you have previews enabled in the Amplify console.
  2. Raise a pull request with these changes.
  3. Observe the results in Amplify and BitBucket.
  4. Introduce a failing test and ensure the test fails in Amplify and is visible in BitBucket.

Thank you for reading

You have reached the end of this blog post. If you have found it useful, feel free to share it on Twitter using the button below.

Tags: TIL, Node.js, JavaScript, Blog, AWS, Amplify, Serverless, Continous Integration, Continuous Deployment