GitHub Issue

Intermediate

In this tutorial, you'll learn how to build a next generation Slack 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 Quickstart guide, then come on back.

Step 1Complete the prerequisites

Every Slack app built using the CLI begins with the same shared steps. Make sure you have everything you need before constructing your app using the CLI.

  • Create an app

    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. In the future, next generation Slack apps will allow you to juggle user-specific tokens as well.

    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.

Step complete!

Step 2Architect your app

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.

  • Define the custom function

    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 organizaiton 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"],
    });
    
Step complete!

Step 3Add steps to your workflow

Call functions in an ordered sequence by adding them to your workflow.

  • Collect user input

    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}`,
    });
    
Step complete!

Step 4Craft the custom function

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

  • Craft the custom function

    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.

Step complete!

Step 5Create a trigger

Now that you've been acquainted with functions and workflows, let's dive in to the last building block, triggers.

  • 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 trigger 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.

Step complete!

Step 6Finish it up

Now that our code has been built, let's run it and try it out.

  • 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!

Step complete!

Was this page helpful?