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:
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:
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.
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 :/'
});
});
};
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 🚀
Top comments (0)