Trigger Github Workflow from Slack using Firebase Functions

Cover image

Let's have a look at how to trigger a Github Workflow from Slack, using Firebase Functions.

Pre-requisites

  • A Firebase project
  • A Github Repository with Github Actions enabled

Create a Slack app

First, let's create a Slack application. This application will be used to create the slash command available in your workspace.

Create a new Slack application and create a slash command:

Slash Commands

We choose whatdafox in this example, but you can name your command however you like.

Don't worry about the Request URL for now; we'll come back later to input the right URL.

Slack should suggest to install the application in your Slack workspace, go ahead and do that.

Navigate to your Slack app basic information page, set aside the Signing Secret, we will use it later to ensure the requests to our Cloud Functions are coming from our Slack application.

Create a Github Personal Token

Since our slash command will trigger a Github Workflow, we need to generate a Github personal token to make a request to the Github API.

Navigate to Github Personal Token and generate a token with the repo scope:

Github Personal Token

Create your Github Workflow

To trigger a workflow from Slack, of course, we need a workflow! Let's create a basic workflow to deploy our project on Firebase, for example.

I am going to assume we have a regular JavaScript application to deploy, please adapt this workflow to your use-case.

Create a .github/workflows directory in your project, and create a deploy.yml file with the following:

name: Deploy

on:
  repository_dispatch:

jobs:
  deploy:
    name: Build and Deploy
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v1

      - name: Setup Node
        uses: actions/setup-node@v1
        with:
          node-version: '12.x'

      - name: Install Firebase
        run: |
          npm i -g firebase-tools

      - name: Build
        run: |
          npm ci
          npm run build

      - name: Deploy
        run: |
          firebase deploy --only hosting --token ${{ secrets.FIREBASE_TOKEN }}

The repository_dispatch event will be triggered by our Cloud Function later.

Push your code to GitHub. We will now setup the trigger.

If you'd like to test your workflow beforehand, use another trigger instead, like on: push, for example.

Add your Firebase Token to your Github repository

For this workflow to work, we need to generate a Firebase token and set it as a secret on our GitHub repository.

To generate a Firebase CI token, run the command:

$ firebase login:ci

Go to your Github repository, navigate to the Settings > Secrets page, create a new FIREBASE_TOKEN secret and paste the token provided by the Firebase CLI.

Firebase Token

Configure Firebase Functions

Setup your project

In this example, we'll use TypeScript to write our function to trigger our Github Workflow.

Run the following command to set up Cloud Functions in your Firebase project and follow the instructions:

$ firebase init functions

This should create a functions directory in your project, with some boilerplate files.

Setup the environment

We know our Cloud Functions will require multiple variables and secrets to work:

  • your GitHub username
  • your repository name
  • your GitHub personal token
  • your Slack app signing secret

We can use Firebase Functions config to store this information:

$ firebase functions:config:set github.username=your-username github.repository=your-repository github.access_token=your-github-personal-token slack.signing_secret=your-slack-signing-secret

Check that the information was saved by running:

$ firebase functions:config:get

You should see the following response:

{
  "github": {
    "username": "your-username",
    "access_token": "your-github-personal-token",
    "repository": "your-repository"
  },
  "slack": {
    "signing_secret": "your-slack-signing-secret"
  }
}

Writing the function

We will use express in our Firebase Function to respond to requests coming from our slash command.

First, we need to install the required dependencies. In the functions directory, install the necessary dependencies:

$ npm i --save express cors body-parser axios tsscmp

If you are using TypeScript, you can also install the following:

npm i --save @types/express @type/cors @types/tsscmp

Then create a functions/utilities/slack.ts file. We will use this file to store functions related to our slash command: verify and deploy:

import axios from "axios";
import * as crypto from 'crypto';
import timeSafeCompare from 'tsscmp';
import * as functions from 'firebase-functions';

const githubToken = functions.config().github.access_token;
const githubUsername = functions.config().github.username;
const githubRepo = functions.config().github.repository;
const slackSigningSecret = functions.config().slack.signing_secret;

export const verify = (request: any) => {
    //    
};

export const deploy = async (request: any, response: any) => {
    //
};

In the verify function, we will parse the request and verify its signature to ensure the request is legit and really coming from our slash command:

export const verify = (request: any) => {
    // Grab the signature and timestamp from the headers
    const requestSignature = request.headers['x-slack-signature'] as string;
    const requestTimestamp = request.headers['x-slack-request-timestamp'];

    const body = request.rawBody;
    const data = body.toString();

    // Create the HMAC
    const hmac = crypto.createHmac('sha256', slackSigningSecret);

    // Update it with the Slack Request
    const [version, hash] = requestSignature.split('=');
    const base = `${version}:${requestTimestamp}:${data}`;
    hmac.update(base);

    // Returns true if it matches
    return timeSafeCompare(hash, hmac.digest('hex'));
};

In the deploy function, we'll use Axios to make a call to the GitHub API to trigger the repository_dispatch event:

export const deploy = async (request: any, response: any) => {
    const http = axios.create({
        baseURL: 'https://api.github.com',
        auth: {
            username: githubUsername,
            password: githubToken,
        },
        headers: {
            // Required https://developer.github.com/v3/repos/#create-a-repository-dispatch-event
            Accept: 'application/vnd.github.everest-preview+json',
        },
    });

    return http.post(`/repos/${githubUsername}/${githubRepo}/dispatches`, { event_type: 'deployment' })
               .then(() => {
                    return response.send({
                        response_type: 'ephemeral',
                        text: 'Deployment started!'
                    });
               })
               .catch((error) => {
                    return response.send({
                        response_type: 'ephemeral',
                        text: "Something went wrong :/ \n ```\n" + JSON.stringify(error.toJSON()) + "\n```"
                    });
               });
};

Now, we will create the main API. Create a functions/slack-command.ts file:

import express from 'express';
import cors from 'cors';
import bodyParser from 'body-parser';
import { verify, deploy } from "./utilities/slack";

const urlencodedParser = bodyParser.urlencoded({ extended: false });

const app = express();

// Automatically allow cross-origin requests
app.use(cors({ origin: true }));

app.post('/', urlencodedParser, async (request: any, response: any) => {
    const isLegitRequest = verify(request);

    if(!isLegitRequest) {
        return response.send({
            response_type: 'ephemeral',
            text: 'Nope!'
        });
    }

    const { text } = request.body;

    switch (text) {
        case 'deploy':
            return deploy(request, response);
        default:
            return response.send({
                response_type: 'ephemeral',
                text: 'Nothing happened ¯\\_(ツ)_/¯'
            });
    }
});

export default app;

In this file, we create a new Express app and create a POST endpoint. When a request is made, it will verify the request legitimacy, parse the command (in our case deploy) and trigger the deploy() function.

If the request fails the verification, the function will return early, which will limit our costs on Firebase.

Note that we are using a switch case here, this is because this way it is trivial to add more commands to your slash command in the future 🙂

Open functions/index.ts and import our functions/slack-command.ts file:

import * as functions from 'firebase-functions';

import slackCommand from './slack-command';

export const onSlackCommand = functions.https.onRequest(slackCommand);

Deploy and test

Deploy the function to Firebase by running:

$ firebase deploy --only functions

Now go in your Slack workspace and type:

/whatdafox deploy

And voilà! Your GitHub Workflow can now be triggered from Slack 🚀