Skip to main content

GitHub issues app

Workflow apps require a paid plan

Join the Developer Program and provision a sandbox with access to all Slack features for free.

In this tutorial, you'll learn how to build a workflow app to create an issue in Github. We'll walk you through sequencing the right workflow steps together and building a function to call GitHub's APIs. Even if you're not looking to create issues on Github, you'll learn how you might invoke any third-party API from a function.

If creating issues on GitHub is actually what you want to do, then all you'll need to do is deploy your app when you're done customizing it. By following a form with a custom function that calls an eminent endpoint by GitHub, we can create an issue and close it ourselves!

Some features you’ll acquaint yourself with while building this app include:

  • Functions: the building blocks of common Slack functionality.
  • Workflows: a set of steps for calling your functions that are executed in order.
  • Custom functions: building blocks that you define!
  • Triggers: for kicking off your workflows.

Before we begin, ensure you have the following prerequisites completed:

  • Install the Slack CLI.
  • Run slack auth list and ensure your workspace is listed.
  • If your workspace is not listed, address any issues by following along with the Getting started, then come on back.

Choose your adventure

After you've installed the command-line interface, you have two ways you can get started:

Use a blank app

You can create a blank app with the Slack CLI using the following command:

slack create github-functions-app --template https://github.com/slack-samples/deno-blank-template

Use a pre-built app

Or, you can use the pre-built GitHub Functions app:

slack create github-functions-app --template https://github.com/slack-samples/deno-github-functions

Change your directory

Once you have your new project ready to go, change into your project directory.

Create a GitHub personal access token

A personal access token is required when calling the GitHub API. Create a new token for this tutorial by visiting your developer settings on GitHub.

Since your personal access token will be used in this tutorial, all issues created from the workflow will appear to have been created by your account.

Select required scopes

To access public repositories, create a new personal token on GitHub with the following scopes:

  • public_repo, repo:invite
  • read:org
  • read:user, user:email
  • read:enterprise

To prevent 404: Not Found errors when attempting to access private repositories, the repo scope must also be selected.

Add the token to your environment variables

When developing locally, you can store your API credentials by adding your GitHub token to your app's environment variables. To do this, create a file called .env in the root directory of your project, and add your token to the file as follows:

GITHUB_TOKEN=ghp_1234AbCd5678

For more information, refer to using environment variables with the Slack CLI.

Define the custom function

Defining the definitions and manifest of our app gives us a birds-eye view before we dive into building. Open your text editor (we recommend VSCode with the Deno plugin) and point to the directory we created earlier.

Since the workflow we're creating revolves around creating a new GitHub issue, we'll begin by defining a custom function with the inputs we know (the repository and information about the issue) and the outputs we expect (the issue number and link).

The DefineFunction method will allow us to define the attributes that comprise this function. Here, we'll describe the attributes seen by other people and used by workflows, as well as the input and output types.

// functions/create_issue/definition.ts

import { DefineFunction, Schema } from "deno-slack-sdk/mod.ts";

const CreateIssueDefinition = DefineFunction({
callback_id: "create_issue",
title: "Create GitHub issue",
description: "Create a new GitHub issue in a repository",
source_file: "functions/create_issue/mod.ts",
input_parameters: {
properties: {
url: {
type: Schema.types.string,
description: "Repository URL",
},
title: {
type: Schema.types.string,
description: "Issue Title",
},
description: {
type: Schema.types.string,
description: "Issue Description",
},
assignees: {
type: Schema.types.string,
description: "Assignees",
},
},
required: ["url", "title"],
},
output_parameters: {
properties: {
GitHubIssueNumber: {
type: Schema.types.number,
description: "Issue number",
},
GitHubIssueLink: {
type: Schema.types.string,
description: "Issue link",
},
},
required: ["GitHubIssueNumber", "GitHubIssueLink"],
},
});

export default CreateIssueDefinition;

The source code for functions/create_issue/mod.ts is shared in Step 4, but keep this definition in mind until then! Or rush ahead and write the function! We won't mind. 😉

Scaffold your workflow

Start by defining the workflow and outlining the steps. We'll add functions and inputs to these steps later.

// workflows/create_new_issue.ts

import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";

const CreateNewIssueWorkflow = DefineWorkflow({
callback_id: "create_new_issue_workflow",
title: "Create new issue",
description: "Create a new GitHub issue",
input_parameters: {
properties: {
interactivity: {
type: Schema.slack.types.interactivity,
},
channel: {
type: Schema.slack.types.channel_id,
},
},
required: ["interactivity", "channel"],
},
});

/* Step 1 - Open a form */
// const issueFormData = CreateNewIssueWorkflow.addStep( ... );

/* Step 2 - Create a new issue */
// const issue = CreateNewIssueWorkflow.addStep( ... );

/* Step 3 - Post the new issue to channel */
// CreateNewIssueWorkflow.addStep( ... );

export default CreateNewIssueWorkflow;

Make your manifest

Import and add these definitions to your app's manifest.

// manifest.ts

import { Manifest } from "deno-slack-sdk/mod.ts";
import CreateIssueDefinition from "./functions/create_issue/definition.ts";
import CreateNewIssueWorkflow from "./workflows/create_new_issue.ts";

export default Manifest({
name: "Workflows for GitHub",
description: "Bringing oft-used GitHub functionality into Slack",
icon: "assets/icon.png",
functions: [CreateIssueDefinition],
workflows: [CreateNewIssueWorkflow],
outgoingDomains: [],
// If your organization uses a separate Github enterprise domain, add that domain to this list
// so that functions can make API calls to it.
outgoingDomains: ["api.github.com"],
botScopes: ["commands", "chat:write", "chat:write.public"],
});

Collect user input

You can call functions in an ordered sequence by adding them to your workflow.

The Slack function OpenForm can be used to collect input data that is used by later steps in the workflow.

// workflows/create_new_issue.ts

...

/* Step 1 - Open a form */
const issueFormData = CreateNewIssueWorkflow.addStep(
Schema.slack.functions.OpenForm,
{
title: "Create an issue",
interactivity: CreateNewIssueWorkflow.inputs.interactivity,
submit_label: "Create",
description: "Create a new issue inside of a GitHub repository",
fields: {
elements: [{
name: "url",
title: "Repository URL",
description: "The GitHub URL of the repository",
type: Schema.types.string,
}, {
name: "title",
title: "Issue title",
type: Schema.types.string,
}, {
name: "description",
title: "Issue description",
type: Schema.types.string,
}, {
name: "assignees",
title: "Issue assignees",
description:
"GitHub username(s) of the user(s) to assign the issue to (separated by commas)",
type: Schema.types.string,
}],
required: ["url", "title"],
},
},
);

Call your custom function

The second step of the workflow calls our custom function to create an issue on GitHub. Similar to other steps, the definition of this function is provided, along with the inputs to the function.

// workflows/create_new_issue.ts
import CreateIssueDefinition from "../functions/create_issue/definition.ts";

...

/* Step 2 - Create a new issue */
const issue = CreateNewIssueWorkflow.addStep(CreateIssueDefinition, {
url: issueFormData.outputs.fields.url,
title: issueFormData.outputs.fields.title,
description: issueFormData.outputs.fields.description,
assignees: issueFormData.outputs.fields.assignees,
});

Notice how the input of this function uses the output from the form in our previous step! The output of this step — the values to be returned from our custom function — will be used to construct and post a message with the basic details of a newly-created issue.

Post the GitHub response

The Slack function SendMessage can be used to post details about the newly-created issue in the channel.

// workflows/create_new_issue.ts

...

/* Step 3 - Post the new issue to channel */
CreateNewIssueWorkflow.addStep(Schema.slack.functions.SendMessage, {
channel_id: CreateNewIssueWorkflow.inputs.channel,
message:
`Issue #${issue.outputs.GitHubIssueNumber} has been successfully created\n` +
`Link to issue: ${issue.outputs.GitHubIssueLink}`,
});

Craft the custom function

Here's where you can take input from Slack, apply custom code to it, and return it back to a workflow.

Copy and paste the following code into functions/create_issue/mod.ts. The mod.ts filename is a convention to declare the entry point to your function in functions/create_issue.

// functions/create_issue/mod.ts

import { SlackFunction } from "deno-slack-sdk/mod.ts";
import CreateIssueDefinition from "./definition.ts";

// https://docs.github.com/en/rest/issues/issues#create-an-issue
export default SlackFunction(
CreateIssueDefinition,
async ({ inputs, env }) => {
const headers = {
Accept: "application/vnd.github+json",
Authorization: "Bearer " + env.GITHUB_TOKEN,
"Content-Type": "application/json",
};

const { url, title, description, assignees } = inputs;

try {
const { hostname, pathname } = new URL(url);
const [_, owner, repo] = pathname.split("/");

// https://docs.github.com/en/enterprise-server@3.3/rest/guides/getting-started-with-the-rest-api
const apiURL = hostname === "github.com"
? "api.github.com"
: `${hostname}/api/v3`;
const issueEndpoint = `https://${apiURL}/repos/${owner}/${repo}/issues`;

const body = JSON.stringify({
title,
body: description,
assignees: assignees?.split(",").map((assignee: string) => {
return assignee.trim();
}),
});

const issue = await fetch(issueEndpoint, {
method: "POST",
headers,
body,
}).then((res: Response) => {
if (res.status === 201) return res.json();
else throw new Error(`${res.status}: ${res.statusText}`);
});

return {
outputs: {
GitHubIssueNumber: issue.number,
GitHubIssueLink: issue.html_url,
},
};
} catch (err) {
console.error(err);
return {
error:
`An error was encountered during issue creation: \`${err.message}\``,
};
}
},
);

For the curious, this function dissects input from the workflow's form, then makes a POST API request to the "Create an issue" GitHub API endpoint. The result of this API call is then returned as output as defined in the function definition (functions/creation_issue/definition.ts); otherwise an error is returned.

Create a trigger

Triggers are how workflows are invoked. Each workflow can have multiple triggers.

There are four types of triggers: link triggers, scheduled triggers, event triggers, and webhook triggers. A link trigger is what we'll be using.

Link triggers are an interactive type, which means they require a user to manually start them. Define your link trigger in a separate file in a triggers folder called create_new_issue_shortcut.ts:

// triggers/create_new_issue_shortcut.ts

import { Trigger } from "deno-slack-api/types.ts";
import CreateNewIssueWorkflow from "../workflows/create_new_issue.ts";

const createNewIssueShortcut: Trigger<
typeof CreateNewIssueWorkflow.definition
> = {
type: "shortcut",
name: "Create GitHub issue",
description: "Create a new GitHub issue in a repository",
workflow: "#/workflows/create_new_issue_workflow",
inputs: {
interactivity: {
value: "{{data.interactivity}}",
},
channel: {
value: "{{data.channel_id}}",
},
},
};

export default createNewIssueShortcut;

Run the trigger create command in your terminal:

slack trigger create --trigger-def "triggers/create_new_issue_shortcut.ts"

After executing this command, select your workspace and choose the Local app environment. When the process completes, you'll be given a link called "shortcut URL." This is your link trigger for this workflow on this workspace. Save that URL for when you start testing.

Run your code to test and tweak

Here's the step we're going to leave you, but this is where your development experience will begin as you alter, test, falter, alter, and test again.

You're building along and your workflow should be, too. You can use development mode to run this workflow in Slack directly from the machine you're reading this from now:

slack run

After you've chosen your development app and assigned it to your workspace, you can switch over to your Slack app and try out your new workflow.

In Slack, you'll want to use the link trigger you created earlier. Once you paste its URL into the message box and post it, it'll unfurl and give you a button to invoke the workflow.

Next steps

For your next challenge, perhaps consider creating an app your users can use to request time off!