loading...
Cover image for Send GCP Build Status to Discord
CodingCatDev

Send GCP Build Status to Discord

ajonp profile image Alex Patterson Originally published at codingcat.dev on ・9 min read

Send GCP Build Status to Discord

You will need NPM (which comes with Node), create-react-app, and firebase-tools installed globally for this project. You will also need to add billing (with Credit Card), although you should not be billed at all for the project.

https://nodejs.org/en/download/

https://reactjs.org/docs/create-a-new-react-app.html

https://firebase.google.com/docs/cli

Creating a new project

You can choose to pick any type of project that you would like, for our example I choose a create-react-app (CRA). This way we still have options for running something within npm. However you could just as easily use this guide for Angular, Vue etc. You could even just write a text file and no site at all. I thought it would lend well to my next lesson of deploying the a storage bucket and CDN.

You can create a new project by running and open in VS Code

npx create-react-app gcp-buil-discord
cd gcp-buil-discord
code gcp-buil-discord

How to run build locally

The goal is to see a real life example for using Google Cloud build. In order to do this we will run the production build of CRA.

https://create-react-app.dev/docs/production-build/

After running npm build command in the root of your project you will see a folder created like below.

Production Build of CRA

Setting Up GCP Project

Now it is time to start creating a new GCP project https://console.cloud.google.com/. If you already have an acccount you can skip to the existing users section.

New Users

If you do not have an account you will be presented with the screen below. You must agree to the Terms of the platform and APIs.

New Users GCP Agreement

I would recommend trying out the 12 months $300 free trial so you can checkout everything that GCP has to offer.

GCP Home Screen for new user

To start a project you can now follow the existing Users guide.

Existing Users

Click on Select a project from the toolbar at the top of the screen.

Select New project

Name your new project something meaningful, for example gcp-build-discord.

New Project Screen

Setting Up a GitHub Repo

Making an assumption that you have created a GitHub account before. https://github.com/join

You should see a "New" button inside of your home page like below.

New GitHub Repo

Enter a Repo name that will work for you like below, remember this as we will use it within our Google Platform Cloud Build.

Github new Repo

Now you can run the below command in your project to add GitHub as your upstream remote.

git remote add origin git@github.com:ajonp/gcp-build-discord-webhook.git
git push -u origin master

Connecting GitHub to Cloud Build

If this is your first time using Cloud Build, you will need to enable the API. Please note the fees related if you start to use 2 hours of build time in a given day (I know a lot).

New Users

Cloud Build API Enable

TODO_UPLOAD TODO_UPLOAD

Follow the next prompts to setup your billing with Credit Card.

Existing Users

You will receive a prompt to Enable Cloud Build API, if you receive a failure make sure to add billing to your account.

TODO_UPLOAD

Now you can connect you GitHub repository.

TODO_UPLOAD

Select the GitHub Source

TODO_UPLOAD

Once you authenticate you will see all of your repositories listed select the one you want to connect with our trigger, for example gcp-build-discord-webhook.

TODO_UPLOAD

After you connect you will see that a push default push trigger can be setup, go ahead and create the default push trigger.

TODO_UPLOAD

You will see a screen similar to below showing the new trigger, you can get into the details of this trigger further, but for now it is enough to trigger on any git push that we run.

TODO_UPLOAD

Cloud Build YAML

For our example react project we have two phases that are going to trigger

  1. npm install - which will install all of our dependencies
  2. npm run build - which will create our production build folder

This file will be located in the root of your project and called cloudbuild.yaml.

# Simple React Build
steps:
- name: node:10.15.1
  entrypoint: npm
  args: ['alex']
- name: node:10.15.1
  entrypoint: npm
  args: ['run', 'build']

Once you add this you can now run the below command to push our new commit.

git commit -m 'first push' && git push

If you like to use VSCode to push instead like below.

TODO_UPLOAD

Back in Google Cloud Console https://console.cloud.google.com you should see a newly triggered build, like below.

TODO_UPLOAD

Pub/Sub Topic Creation

You can look for Pub/Sub under Bit Data, otherwise you can simply search for it.

TODO_UPLOAD

Select Create Topic so we can create a new cloud-builds topic that Google Cloud Build triggers during each job. Please note the topic must be exactly this name! Here you can find more details https://cloud.google.com/cloud-build/docs/send-build-notifications

TODO_UPLOAD

Initialize Firebase

If you haven't installed firebase-tools, please do that before run this section https://firebase.google.com/docs/cli

Within the root of your project you can run the command firebase init to setup Firebase Cloud Functions. Select Functions by hitting the space bar, then hit enter to continue.

TODO_UPLOAD

We will be using TypeScript in our example, but you could also choose JavaScript if you have done this type of thing and prefer it.

TODO_UPLOAD

Creating Firebase Cloud Function for Pub/Sub

In your root folder you will find a new directory called functions. Navigate to functions/src/index.ts file and uncomment the default helloWorld function and we will push this as well just to see the difference on functions.

In the video I walk through how you can use Pub/Sub to walk through console logging and other options in your build before running the actual function. Here I will just add the final code code so that you can implement and try it directly.

Within the functions/src directory add a new directory and file with the final path being

functions/src/DiscordBuildHooks/DiscordBuildHooks.ts

import * as functions from 'firebase-functions';
import fetch from 'node-fetch';

export const gcpBuildTriggerDiscord = functions.pubsub.topic('cloud-builds').onPublish(async (pubSubEvent) => {
    const build = JSON.parse(Buffer.from(pubSubEvent.data, 'base64').toString());
    const status = ['SUCCESS', 'FAILURE', 'INTERNAL_ERROR', 'TIMEOUT'];
    if (status.indexOf(build.status) === -1) {
        console.log('Status not found');
        return false;
    }

    try {
        const msg = createBuildMessage(build);
        return sendDiscordBuildPost(msg);
    } catch (err) {
        console.log(err);
    }
});

const sendDiscordBuildPost = async (body: object) => {
    console.log(`Calling Discord wtih`, JSON.stringify(body));
    const result = await fetch(functions.config().discord.build_hook, {
        body: JSON.stringify(body),
        method: 'POST',
        headers: { "Content-Type": "application/json" }
    });
    console.log(result.json());
}

const createBuildMessage = (build: GoogleCloudBuild) => {
    const embeds: Embed[] = [];
    const msg = {
        content: `Build ${build.id} for project ${build.projectId} was a ${build.status} ` +
            `took ${(<any>new Date(build.finishTime) - <any>new Date(build.startTime)) * .001}`,
        tts: build.status === 'FAILURE' ? true : false,
        embeds: embeds
    }
    if (build && build.steps) {
        build.steps.forEach(step => {
            let time = '';
            if(step.timing && step.timing.endTime){
                time = `took ${(<any>new Date(step.timing.endTime) - <any>new Date(step.timing.startTime)) * .001}`
            }
            embeds.push({
                title: step.name,
                description:
                    `${step.entrypoint} ${step.args.join(' ')} ${time} and ${step.status}`,
                color: build.status === 'FAILURE' ? 16714507 : 6618931
            });
        });
        msg.embeds = embeds;
    }
    return msg;
}

export interface Embed {
    title?: string;
    description?: string;
    color?: number;
}

export interface GoogleCloudBuild {
    id: string;
    projectId: string;
    status: string;
    steps?: Step[];
    createTime: Date;
    startTime: Date;
    finishTime: Date;
    buildTriggerId: string;
    options: Options;
}

export interface Options {
    substitutionOption?: string;
    logging?: string;
}

export interface Step {
    name: string;
    args: string[];
    entrypoint: string;
    timing: Timing;
    pullTiming: Timing;
    status: string;
}

export interface Timing {
    startTime: Date;
    endTime: Date;
}

In order for your function to be exposed we need to add this to your index.ts file as well with the final code looking like below.

import * as functions from 'firebase-functions';

export const helloWorld = functions.https.onRequest((request, response) => {
 response.send("Hello from Firebase!");
});

export { gcpBuildTriggerDiscord } from './DiscordBuildHooks/DiscordBuildHooks';

Final folder structure

TODO_UPLOAD

Before deploying this function we will need to setup a Discord Web hook and add it to our firebase configuration.

Discord Webhook

You must register on Discord and have rights to manage Webhooks for this part.

Creating Hook

Select a given channel that you want to create a hook in and go to settings, by clicking the gear icon.

TODO_UPLOAD

Next navigate to Webhook and click Create Webhook.

TODO_UPLOAD

You will then need to copy the webhook url so we can add it to our Firebase Cloud Function configuration.

TODO_UPLOAD

Make sure you keep all of your webhooks private!

Firebase Cloud Function Config

In the video we go into greater detail if you would like to know more about adding a configuration to Firebase Cloud Functions. You may have noticed in the above code this line functions.config().discord.build_hook . This is how Firebase Functions can read configurations. You are going to set this configuration with your Discord Webhook URL.

The reason we don't just hardcode this is that we need to keep it private and out of our git repo. There is an argument that this is not the safest place to keep secrets and if you want to take this a step further look into https://cloud.google.com/kms

firebase functions:config:set discord.build_hook=<YOUR_WEBHOOK>

Deploy Firebase Cloud Functions

Now that you have your configuration set and all of the code you will need it is time to deploy your functions.

firebase deploy

If you find later you are working on front end hosting and want to only deploy functions you can run the below command.

firebase deploy --only functions

Firebase Console

You can now see your functions listed inside of the Firebase Console.

TODO_UPLOAD

If you want to see if your Cloud Functions are working correctly you can trigger your helloWorld function by entering the url into a browser, here is an example.

TODO_UPLOAD

Trigger Your Build

It is now time to try building your project to see if our full flow is working as expected

GitHub->Google Cloud Build->Pub/Sub->Firebase Functions->Discord

So commit and push all of your changes.

git commit -m 'first push' && git push

You will see the below messages in Discord (notice the bugs, fix them :D)

TODO_UPLOAD

Failure tests

  1. Try changing the name of your cloudbuild.yaml file to failurecloudbuild.yaml and see if it fails.
  2. Putting back cloudbuild.yaml if you changed it above. Now inside of cloudbuild.yaml you can change the arguments to something npm doesn't understand like 'alex'. This will trigger a failure build within a step and send it to Discord.

This should trigger the two messages below in Discord

TODO_UPLOAD

Trouble Shooting

If you find something is wrong with your function check the logs, you might find some bugs I missed.

TODO_UPLOAD

Posted on by:

ajonp profile

Alex Patterson

@ajonp

Hi I am the Founder of CodingCat.dev, a web development community. I love all things Web!

CodingCatDev

CodingCatDev is a community of developers creating resources for all to use!

Discussion

pic
Editor guide