Create a GitHub issue with a Workflow

Beginner

In this tutorial, you'll learn how to create an issue in Github seamlessly using our future-generation platform. We'll walk you through sequencing the right workflow steps together and build 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 too.

If creating issues on GitHub is actually what you want to do, then all you need to do is deploy the app and optionally customize it to your wishes.

By following a form with a custom function that calls an eminent endpoint by GitHub, we can accomplish this and close this issue ourselves!

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

  • Functions: Building blocks of common Slack functionality.
  • Workflows: Building blocks that you define!
  • Built-in functions: a set of steps (either built-in or custom functions) executed in order.
  • Triggers: a method to invoke a Workflow.

Now that you know what we're up to, let's dive in!

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 embarking on this journey with the CLI.

  • Create a fresh project

    After you've installed the command-line interface you have two ways to dive in.

    Create a super fresh project

    You can create a fresh new project:

    slack create deno-github-functions
    

    But you'll still be presented with the paradox of choice: a totally clean project or something with some suggested structure to it. We'll walk you through the suggested structure regardless so it's best to go ahead and choose the blank project when following along directly.

    Or, use a template instead

    Of course, you could just jump right to the sample project on GitHub if you want. You can even just jumpstart your project using the CLI and skip all this copy and pasting.

    slack create --template https://github.com/slack-samples/deno-github-functions
    
  • 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. The new platform will one day 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

    You can store your API credentials by adding the GitHub token you created to your app's environment variables. Use the CLI's slack env add command in your terminal.

    slack env add ghp_4XSt0k3n4gh8
    
Step complete!

Step 2Architect your App

Focusing on the definitions and manifest of our app gives us a birds-eye view before we dive into building. Open up your text editor (we heartily recommend VSCode with the Deno plugin) pointing to the directory we created with the slack command earlier.

  • Define the custom function

    Since the Workflow we are 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 aptly allows us to define the attributes that make up this function. Here, we will 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 will add functions and inputs to these steps in Step 3.

    // 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;
    
  • Write 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: [],
      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 built-in 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 built-in function SendMessage can be used to post details about the newly created issue back into 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 4Write the custom function

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

  • Write the custom function

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

    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 functions/creation_issue/definition.ts file – or an error is returned.

    // 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}\``,
          };
        }
      },
    );
    
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, the true star of the show: Triggers πŸŽ‰

  • Create a Trigger

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

    There are four types of Triggers: Link Triggers, Scheduled Triggers, Event Triggers, and Webhook Triggers. Link Triggers are an interactive type, which is fancy speak for 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 terminal:

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

    After executing this command, select your development application and the workspace you're going to experiment with this at. 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 we start testing these out, as it'll be the only way to invoke this particular trigger.

Step complete!

Step 6Finish it up

Now that our code is in place let's run it and try it out.

  • Using `slack run` to test and tweak.

    So here's the step we're going to leave you at. This is where your development experience will begin to loop, as you alter, test, falter, alter, and test again.

    You're on a run and your workflow should be, too. Let's use the 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 (it's the one with (dev) in the name) and assigned it to your workspace, you can switch over to your Slack app and try out your new workflow.

    Once you've arrived in Slack, you're going to want to use the link trigger you created previously in Step 5. Once you paste its Shortcut URL into a chat box and post it as a message, it'll unfurl and give you a button to invoke the workflow. If it's something you're going to do a lot in a channel, consider adding the link as a bookmark.

    Thank you!

    Now that you've experimented with the platform, we'd love to hear what you think.

Step complete!

Was this page helpful?