├── .devcontainer ├── Dockerfile ├── README.md └── devcontainer.json ├── .env.example ├── .github ├── dependabot.yml └── workflows │ └── deno.yml ├── .gitignore ├── .slack ├── .gitignore └── hooks.json ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── assets └── default_new_app_icon.png ├── deno.jsonc ├── external_auth └── github_provider.ts ├── functions ├── create_issue.ts └── create_issue_test.ts ├── manifest.ts ├── triggers └── create_new_issue_shortcut.ts └── workflows └── create_new_issue.ts /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the Ubtuntu base image 2 | FROM ubuntu:22.04 3 | 4 | # Download development tools 5 | RUN apt-get update 6 | RUN apt-get install -y curl 7 | RUN apt-get install -y git 8 | RUN apt-get install -y unzip 9 | 10 | # Run the container as a user 11 | RUN useradd -m -s /bin/bash slackdev 12 | RUN chown slackdev /usr/local/bin 13 | USER slackdev 14 | 15 | # Install the project runtime 16 | RUN curl -fsSL https://deno.land/install.sh | sh 17 | RUN export DENO_INSTALL="/home/slackdev/.deno" 18 | RUN export PATH="$DENO_INSTALL/bin:$PATH" 19 | 20 | # Set the working directory 21 | WORKDIR /workspaces 22 | 23 | # Install the Slack CLI 24 | RUN curl -fsSL https://downloads.slack-edge.com/slack-cli/install.sh | bash -s -- -d 25 | -------------------------------------------------------------------------------- /.devcontainer/README.md: -------------------------------------------------------------------------------- 1 | # .devcontainer 2 | 3 | A [development container][container] provides a predefined environment with 4 | some tools needed for development, which can be useful in editors such as 5 | [Visual Studio Code][vscode] or remote settings like [Codespaces][codespaces]. 6 | 7 | This specific container packages [the Slack CLI][cli] with the project runtime 8 | and a few development tools. The `Dockerfile` details the container. 9 | 10 | ## Editor extensions 11 | 12 | Modifications to an editor can be made with changes to the `devcontainer.json` 13 | file: 14 | 15 | ```diff 16 | { 17 | "customizations": { 18 | "vscode": { 19 | "extensions": [ 20 | + "GitHub.copilot", 21 | "denoland.vscode-deno", 22 | "ms-azuretools.vscode-docker" 23 | ], 24 | + "settings": { 25 | + "terminal.integrated.defaultProfile.linux": "zsh" 26 | + } 27 | } 28 | } 29 | } 30 | ``` 31 | 32 | [codespaces]: https://github.com/features/codespaces 33 | [cli]: https://api.slack.com/automation/cli 34 | [container]: https://containers.dev/ 35 | [vscode]: https://code.visualstudio.com/docs/devcontainers/containers 36 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Slack Platform", 3 | "dockerFile": "Dockerfile", 4 | "remoteUser": "slackdev", 5 | "customizations": { 6 | "vscode": { 7 | "extensions": [ 8 | "denoland.vscode-deno", 9 | "ms-azuretools.vscode-docker" 10 | ] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Environment variables needed to run your app. Rename this file to .env to start! 2 | 3 | # The client ID for your GitHub OAuth App 4 | GITHUB_CLIENT_ID=e845f73fa2... 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | -------------------------------------------------------------------------------- /.github/workflows/deno.yml: -------------------------------------------------------------------------------- 1 | name: Deno app build and testing 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | deno: 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 5 15 | 16 | steps: 17 | - name: Setup repo 18 | uses: actions/checkout@v4 19 | 20 | - name: Setup Deno 21 | uses: denoland/setup-deno@v2 22 | with: 23 | deno-version: v2.x 24 | 25 | - name: Verify formatting 26 | run: deno fmt --check 27 | 28 | - name: Run linter 29 | run: deno lint 30 | 31 | - name: Run tests 32 | run: deno task test 33 | 34 | - name: Run type check 35 | run: deno check *.ts && deno check **/*.ts 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | package 3 | .DS_Store 4 | .env 5 | -------------------------------------------------------------------------------- /.slack/.gitignore: -------------------------------------------------------------------------------- 1 | apps.dev.json 2 | cache/ 3 | -------------------------------------------------------------------------------- /.slack/hooks.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "get-hooks": "deno run -q --allow-read --allow-net https://deno.land/x/deno_slack_hooks@1.3.2/mod.ts" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true, 4 | "deno.unstable": true, 5 | "deno.suggest.imports.hosts": { 6 | "https://deno.land": false 7 | }, 8 | "[typescript]": { 9 | "editor.formatOnSave": true, 10 | "editor.defaultFormatter": "denoland.vscode-deno" 11 | }, 12 | "editor.tabSize": 2 13 | } 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022- Slack Technologies, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Workflows for GitHub Sample App 2 | 3 | This app brings oft-used GitHub functionality - such as creating new issues - to 4 | Slack using functions and workflows. 5 | 6 | **Guide Outline**: 7 | 8 | - [Supported Workflows](#supported-workflows) 9 | - [Setup](#setup) 10 | - [Install the Slack CLI](#install-the-slack-cli) 11 | - [Clone the Sample App](#clone-the-sample-app) 12 | - [Register an OAuth App on GitHub](#register-an-oauth-app-on-github) 13 | - [Configure Outgoing Domains](#configure-outgoing-domains) 14 | - [Create a Link Trigger](#create-a-link-trigger) 15 | - [Running Your Project Locally](#running-your-project-locally) 16 | - [Deploying Your App](#deploying-your-app) 17 | - [Viewing Activity Logs](#viewing-activity-logs) 18 | - [Project Structure](#project-structure) 19 | - [Resources](#resources) 20 | 21 | --- 22 | 23 | ## Supported Workflows 24 | 25 | - **Create new issue**: Create and assign new issues in repositories. 26 | 27 | ## Setup 28 | 29 | Before getting started, make sure you have a development workspace where you 30 | have permissions to install apps. If you don’t have one set up, go ahead and 31 | [create one](https://slack.com/create). Also, please note that the workspace 32 | requires any of [the Slack paid plans](https://slack.com/pricing). 33 | 34 | ### Install the Slack CLI 35 | 36 | To use this sample, you first need to install and configure the Slack CLI. 37 | Step-by-step instructions can be found in our 38 | [Quickstart Guide](https://api.slack.com/automation/quickstart). 39 | 40 | ### Clone the Sample App 41 | 42 | Start by cloning this repository: 43 | 44 | ```zsh 45 | # Clone this project onto your machine 46 | $ slack create my-github-app -t slack-samples/deno-github-functions 47 | 48 | # Change into this project directory 49 | $ cd my-github-app 50 | ``` 51 | 52 | ### Register a GitHub App 53 | 54 | With [external authentication](https://api.slack.com/automation/external-auth) 55 | you can connect your GitHub account to your Slack app to easily access the 56 | GitHub API from a custom function, creating a base for programmatic 57 | personalizations! 58 | 59 | > Connecting your GitHub account with external auth allows your application to 60 | > perform the API calls used by functions as though it was _from this GitHub 61 | > account_. This means all issues created from the **Create GitHub issue** 62 | > workflow will appear to have been created by the account used when 63 | > authenticating. 64 | 65 | #### Create an OAuth App on GitHub 66 | 67 | Begin by creating a new OAuth App from your 68 | [developer settings on GitHub](https://github.com/settings/developers) using any 69 | **Application name** and **Homepage URL** you'd like, but leaving **Enable 70 | Device Flow** unchecked. 71 | 72 | The **Authorization callback URL** must be set to 73 | `https://oauth2.slack.com/external/auth/callback` to later exchange tokens and 74 | complete the OAuth2 handshake. 75 | 76 | Once you're satisfied with these configurations, go ahead and click **Register 77 | application**! 78 | 79 | #### Add your GitHub Client ID 80 | 81 | Start by renaming the `.env.example` file at the top level of your project to 82 | `.env`, being sure not to commit this file to version control. This file will 83 | store sensitive, app-specific variables that are determined by the environment 84 | being used. 85 | 86 | From your new GitHub app's dashboard, copy the **Client ID** and paste it as the 87 | value for `GITHUB_CLIENT_ID` in the `.env` file. This value will be used in 88 | `external_auth/github_provider.ts` – the custom OAuth2 provider definition for 89 | this GitHub app. 90 | 91 | Once complete, use `slack run` or `slack deploy` to update your local or hosted 92 | app! 93 | 94 | > Note: Unlike environment variables used at runtime, this variable is only used 95 | > when generating your app manifest. Therefore, you do **not** need to use the 96 | > `slack env add` command to set this value for 97 | > [deployed apps](#deploying-your-app). 98 | 99 | #### Generate a Client Secret 100 | 101 | Returning to your GitHub app's dashboard, press **Generate a new client secret** 102 | then run the following command, replacing `GITHUB_CLIENT_SECRET` with your own 103 | secret: 104 | 105 | ```zsh 106 | $ slack external-auth add-secret --provider github --secret GITHUB_CLIENT_SECRET 107 | ``` 108 | 109 | When prompted to select an app, choose the `(local)` app only if you're running 110 | the app locally. 111 | 112 | #### Initiate the OAuth2 Flow 113 | 114 | With your GitHub OAuth application created and the Client ID and secret set, 115 | you're ready to initate the OAuth flow! 116 | 117 | If all the right values are in place, then the following command will prompt you 118 | to choose an app, select a provider (hint: choose the `github` one), then pick 119 | the GitHub account you want to authenticate with: 120 | 121 | ```zsh 122 | $ slack external-auth add 123 | ``` 124 | 125 | **Note: when working with repositories that are part of an organization, be sure 126 | to grant access to that organization when authorizing your OAuth app.** 127 | 128 | After you've added your authentication, you'll need to assign it to the 129 | `#/workflows/create_new_issue_workflow` workflow using the following command: 130 | 131 | ```zsh 132 | $ slack external-auth select-auth 133 | ``` 134 | 135 | Once you've successfully connected your account, you're almost ready to create a 136 | link into your workflow! 137 | 138 | #### Collaborating with External Authentication 139 | 140 | When developing collaboratively on a deployed app, the external authentication 141 | tokens used for your app will be shared by all collaborators. For this reason, 142 | we recommend creating your GitHub OAuth App using an organization account so all 143 | collaborators can access the same account. 144 | 145 | Local development does not require a shared account, as each developer will have 146 | their own local app and can individually add their own external authentication 147 | tokens. 148 | 149 | ### Configure Outgoing Domains 150 | 151 | Hosted custom functions must declare which 152 | [outgoing domains](https://api.slack.com/automation/manifest) are used when 153 | making network requests, including Github API calls. `api.github.com` is already 154 | configured as an outgoing domain in this sample's manifest. If your organization 155 | uses a separate Github Enterprise to make API calls to, add that domain to the 156 | `outgoingDomains` array in `manifest.ts`. 157 | 158 | ## Create a Link Trigger 159 | 160 | [Triggers](https://api.slack.com/automation/triggers) are what cause workflows 161 | to run. These triggers can be invoked by a user, or automatically as a response 162 | to an event within Slack. 163 | 164 | A [link trigger](https://api.slack.com/automation/triggers/link) is a type of 165 | Trigger that generates a **Shortcut URL** which, when posted in a channel or 166 | added as a bookmark, becomes a link. When clicked, the link trigger will run the 167 | associated workflow. 168 | 169 | Link triggers are _unique to each installed version of your app_. This means 170 | that Shortcut URLs will be different across each workspace, as well as between 171 | [locally run](#running-your-project-locally) and 172 | [deployed apps](#deploying-your-app). When creating a trigger, you must select 173 | the Workspace that you'd like to create the trigger in. Each Workspace has a 174 | development version (denoted by `(local)`), as well as a deployed version. 175 | 176 | To create a link trigger for the "Create New Issue" workflow, run the following 177 | command: 178 | 179 | ```zsh 180 | $ slack trigger create --trigger-def triggers/create_new_issue_shortcut.ts 181 | ``` 182 | 183 | After selecting a Workspace, the output provided will include the link trigger 184 | Shortcut URL. Copy and paste this URL into a channel as a message, or add it as 185 | a bookmark in a channel of the workspace you selected. 186 | 187 | **Note: this link won't run the workflow until the app is either running locally 188 | or deployed!** Read on to learn how to run your app locally and eventually 189 | deploy it to Slack hosting. 190 | 191 | ## Running Your Project Locally 192 | 193 | While building your app, you can see your changes propagated to your workspace 194 | in real-time with `slack run`. In both the CLI and in Slack, you'll know an app 195 | is the development version if the name has the string `(local)` appended. 196 | 197 | ```zsh 198 | # Run app locally 199 | $ slack run 200 | 201 | Connected, awaiting events 202 | ``` 203 | 204 | Once running, click the 205 | [previously created Shortcut URL](#create-a-link-trigger) associated with the 206 | `(local)` version of your app. This should start a workflow that opens a form 207 | used to create a new GitHub issue! 208 | 209 | To stop running locally, press ` + C` to end the process. 210 | 211 | ## Deploying Your App 212 | 213 | Once you're done with development, you can deploy the production version of your 214 | app to Slack hosting using `slack deploy`: 215 | 216 | ```zsh 217 | $ slack deploy 218 | ``` 219 | 220 | After deploying, [create a new link trigger](#create-a-link-trigger) for the 221 | production version of your app (not appended with `(local)`). Once the trigger 222 | is invoked, the workflow should run just as it did in when developing locally. 223 | 224 | ### Viewing Activity Logs 225 | 226 | Activity logs for the production instance of your application can be viewed with 227 | the `slack activity` command: 228 | 229 | ```zsh 230 | $ slack activity 231 | ``` 232 | 233 | ## Project Structure 234 | 235 | ### `.slack/` 236 | 237 | Contains `apps.dev.json` and `apps.json`, which include installation details for 238 | development and deployed apps. 239 | 240 | Contains `hooks.json` used by the CLI to interact with the project's SDK 241 | dependencies. It contains script hooks that are executed by the CLI and 242 | implemented by the SDK. 243 | 244 | ### `manifest.ts` 245 | 246 | The [app manifest](https://api.slack.com/automation/manifest) contains the app's 247 | configuration. This file defines attributes like app name and description. 248 | 249 | ### `/functions` 250 | 251 | [Functions](https://api.slack.com/automation/functions) are reusable building 252 | blocks of automation that accept inputs, perform calculations, and provide 253 | outputs. Functions can be used independently or as steps in workflows. 254 | 255 | ### `/workflows` 256 | 257 | A [workflow](https://api.slack.com/automation/workflows) is a set of steps that 258 | are executed in order. Each step in a Workflow is a function. 259 | 260 | Workflows can be configured to run without user input or they can collect input 261 | by beginning with a [form](https://api.slack.com/automation/forms) before 262 | continuing to the next step. 263 | 264 | ### `/triggers` 265 | 266 | [Triggers](https://api.slack.com/automation/triggers) determine when workflows 267 | are executed. A trigger file describes a scenario in which a workflow should be 268 | run, such as a user pressing a button or when a specific event occurs. 269 | 270 | ## Resources 271 | 272 | To learn more about developing with the CLI, you can visit the following guides: 273 | 274 | - [Creating a new app with the CLI](https://api.slack.com/automation/create) 275 | - [Configuring your app](https://api.slack.com/automation/manifest) 276 | - [Developing locally](https://api.slack.com/automation/run) 277 | 278 | To view all documentation and guides available, visit the 279 | [Overview page](https://api.slack.com/automation/overview). 280 | -------------------------------------------------------------------------------- /assets/default_new_app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slack-samples/deno-github-functions/462535915a55b38060a00f11df85bf1764b7f88e/assets/default_new_app_icon.png -------------------------------------------------------------------------------- /deno.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/denoland/deno/main/cli/schemas/config-file.v1.json", 3 | "fmt": { 4 | "include": [ 5 | "README.md", 6 | "datastores", 7 | "external_auth", 8 | "functions", 9 | "manifest.ts", 10 | "triggers", 11 | "types", 12 | "views", 13 | "workflows" 14 | ] 15 | }, 16 | "lint": { 17 | "include": [ 18 | "datastores", 19 | "external_auth", 20 | "functions", 21 | "manifest.ts", 22 | "triggers", 23 | "types", 24 | "views", 25 | "workflows" 26 | ] 27 | }, 28 | "lock": false, 29 | "tasks": { 30 | "test": "deno fmt --check && deno lint && deno test --allow-read" 31 | }, 32 | "imports": { 33 | "deno-slack-sdk/": "https://deno.land/x/deno_slack_sdk@2.15.0/", 34 | "deno-slack-api/": "https://deno.land/x/deno_slack_api@2.8.0/", 35 | "@std/dotenv": "jsr:@std/dotenv@^0.225.4", 36 | "@std/testing": "jsr:@std/testing@^1.0.12", 37 | "@std/assert": "jsr:@std/assert@^1.0.13" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /external_auth/github_provider.ts: -------------------------------------------------------------------------------- 1 | import { DefineOAuth2Provider, Schema } from "deno-slack-sdk/mod.ts"; 2 | import "@std/dotenv/load"; 3 | 4 | /** 5 | * External authentication uses the OAuth 2.0 protocol to connect with 6 | * accounts across various services. Once authenticated, an access token 7 | * can be used to interact with the service on behalf of the user. 8 | * Learn more: https://api.slack.com/automation/external-auth 9 | */ 10 | 11 | // https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps#web-application-flow 12 | const GitHubProvider = DefineOAuth2Provider({ 13 | provider_key: "github", 14 | provider_type: Schema.providers.oauth2.CUSTOM, 15 | options: { 16 | provider_name: "GitHub", 17 | authorization_url: "https://github.com/login/oauth/authorize", 18 | token_url: "https://github.com/login/oauth/access_token", 19 | client_id: Deno.env.get("GITHUB_CLIENT_ID")!, 20 | scope: [ 21 | "repo", 22 | "read:org", 23 | "read:user", 24 | "user:email", 25 | "read:enterprise", 26 | ], 27 | identity_config: { 28 | url: "https://api.github.com/user", 29 | account_identifier: "$.login", 30 | }, 31 | }, 32 | }); 33 | 34 | export default GitHubProvider; 35 | -------------------------------------------------------------------------------- /functions/create_issue.ts: -------------------------------------------------------------------------------- 1 | import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts"; 2 | 3 | /** 4 | * Functions are reusable building blocks of automation that accept inputs, 5 | * perform calculations, and provide outputs. Functions can be used as steps in 6 | * a workflow or independently. 7 | * Learn more: https://api.slack.com/automation/functions/custom 8 | */ 9 | export const CreateIssueDefinition = DefineFunction({ 10 | callback_id: "create_issue", 11 | title: "Create GitHub issue", 12 | description: "Create a new GitHub issue in a repository", 13 | source_file: "functions/create_issue.ts", // The file with the exported function handler 14 | input_parameters: { 15 | properties: { 16 | githubAccessTokenId: { 17 | type: Schema.slack.types.oauth2, 18 | oauth2_provider_key: "github", 19 | }, 20 | url: { 21 | type: Schema.types.string, 22 | description: "Repository URL", 23 | }, 24 | title: { 25 | type: Schema.types.string, 26 | description: "Issue Title", 27 | }, 28 | description: { 29 | type: Schema.types.string, 30 | description: "Issue Description", 31 | }, 32 | assignees: { 33 | type: Schema.types.string, 34 | description: "Assignees", 35 | }, 36 | }, 37 | required: [ 38 | "githubAccessTokenId", 39 | "url", 40 | "title", 41 | ], 42 | }, 43 | output_parameters: { 44 | properties: { 45 | GitHubIssueNumber: { 46 | type: Schema.types.number, 47 | description: "Issue number", 48 | }, 49 | GitHubIssueLink: { 50 | type: Schema.types.string, 51 | description: "Issue link", 52 | }, 53 | }, 54 | required: ["GitHubIssueNumber", "GitHubIssueLink"], 55 | }, 56 | }); 57 | 58 | /** 59 | * The default export for a custom function accepts a function definition 60 | * and a function handler that contains the custom logic for the function. 61 | */ 62 | export default SlackFunction( 63 | CreateIssueDefinition, 64 | async ({ inputs, client }) => { 65 | /** 66 | * Gather the stored external authentication access token using the access 67 | * token id passed from the workflow's input. This token can be used to 68 | * authorize requests made to an external service on behalf of the user. 69 | */ 70 | const token = await client.apps.auth.external.get({ 71 | external_token_id: inputs.githubAccessTokenId, 72 | }); 73 | 74 | if (!token.ok) throw new Error("Failed to access auth token"); 75 | 76 | const headers = { 77 | Accept: "application/vnd.github+json", 78 | Authorization: `Bearer ${token.external_token}`, 79 | "Content-Type": "application/json", 80 | "X-GitHub-Api-Version": "2022-11-28", 81 | }; 82 | 83 | const { url, title, description, assignees } = inputs; 84 | 85 | try { 86 | const { hostname, pathname } = new URL(url); 87 | const [_, owner, repo] = pathname.split("/"); 88 | 89 | // https://docs.github.com/en/enterprise-server@3.3/rest/guides/getting-started-with-the-rest-api 90 | const apiURL = hostname === "github.com" 91 | ? "api.github.com" 92 | : `${hostname}/api/v3`; 93 | 94 | // https://docs.github.com/en/rest/issues/issues#create-an-issue 95 | const issueEndpoint = `https://${apiURL}/repos/${owner}/${repo}/issues`; 96 | 97 | const body = JSON.stringify({ 98 | title, 99 | body: description, 100 | assignees: assignees?.split(",").map((assignee: string) => { 101 | return assignee.trim(); 102 | }), 103 | }); 104 | 105 | const issue = await fetch(issueEndpoint, { 106 | method: "POST", 107 | headers, 108 | body, 109 | }).then((res: Response) => { 110 | if (res.status === 201) return res.json(); 111 | throw new Error(`${res.status}: ${res.statusText}`); 112 | }); 113 | 114 | return { 115 | outputs: { 116 | GitHubIssueNumber: issue.number, 117 | GitHubIssueLink: issue.html_url, 118 | }, 119 | }; 120 | } catch (err) { 121 | console.error(err); 122 | return { 123 | error: `An error was encountered during issue creation: \`${ 124 | err instanceof Error ? err.message : String(err) 125 | }\``, 126 | }; 127 | } 128 | }, 129 | ); 130 | -------------------------------------------------------------------------------- /functions/create_issue_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "@std/assert"; 2 | import { stub } from "@std/testing/mock"; 3 | 4 | import { SlackFunctionTester } from "deno-slack-sdk/mod.ts"; 5 | import handler from "./create_issue.ts"; 6 | 7 | /** 8 | * The actual outputs of a function can be compared to expected outputs for a 9 | * collection of given inputs. 10 | */ 11 | const { createContext } = SlackFunctionTester("create_issue"); 12 | const env = { logLevel: "CRITICAL" }; 13 | 14 | Deno.test("Create a GitHub issue with given inputs", async () => { 15 | /** 16 | * Mocked responses of Slack API and external endpoints can be set with 17 | * mock_fetch. 18 | */ 19 | using _fetchStub = stub( 20 | globalThis, 21 | "fetch", 22 | (url: string | URL | Request, options?: RequestInit) => { 23 | const req = url instanceof Request ? url : new Request(url, options); 24 | assertEquals(req.method, "POST"); 25 | switch (req.url) { 26 | case "https://slack.com/api/apps.auth.external.get": 27 | return Promise.resolve( 28 | new Response(`{"ok": true, "external_token": "example-token"}`), 29 | ); 30 | case "https://api.github.com/repos/slack-samples/deno-github-functions/issues": 31 | return Promise.resolve( 32 | new Response( 33 | `{"number": 123, "html_url": "https://www.example.com/expected-html-url"}`, 34 | { 35 | status: 201, 36 | }, 37 | ), 38 | ); 39 | default: 40 | throw Error( 41 | `No stub found for ${req.method} ${req.url}\nHeaders: ${ 42 | JSON.stringify(Object.fromEntries(req.headers.entries())) 43 | }`, 44 | ); 45 | } 46 | }, 47 | ); 48 | 49 | const inputs = { 50 | githubAccessTokenId: {}, 51 | url: "https://github.com/slack-samples/deno-github-functions", 52 | title: "The issue title", 53 | description: "issue description", 54 | assignees: "batman", 55 | }; 56 | 57 | const { outputs } = await handler(createContext({ inputs, env })); 58 | assertEquals(outputs?.GitHubIssueNumber, 123); 59 | assertEquals( 60 | outputs?.GitHubIssueLink, 61 | "https://www.example.com/expected-html-url", 62 | ); 63 | }); 64 | -------------------------------------------------------------------------------- /manifest.ts: -------------------------------------------------------------------------------- 1 | import { Manifest } from "deno-slack-sdk/mod.ts"; 2 | import GitHubProvider from "./external_auth/github_provider.ts"; 3 | import CreateNewIssueWorkflow from "./workflows/create_new_issue.ts"; 4 | 5 | /** 6 | * The app manifest contains the app's configuration. This file defines 7 | * attributes like app name, description, available workflows, and more. 8 | * Learn more: https://api.slack.com/automation/manifest 9 | */ 10 | export default Manifest({ 11 | name: "Workflows for GitHub", 12 | description: "Bringing oft-used GitHub functionality into Slack", 13 | icon: "assets/default_new_app_icon.png", 14 | externalAuthProviders: [GitHubProvider], 15 | workflows: [CreateNewIssueWorkflow], 16 | /** 17 | * Domains used in remote HTTP requests must be specified as outgoing domains. 18 | * If your organization uses a seperate GitHub Enterprise domain, add it here 19 | * to make API calls to it from a custom function. 20 | */ 21 | outgoingDomains: ["api.github.com"], 22 | botScopes: ["commands", "chat:write", "chat:write.public"], 23 | }); 24 | -------------------------------------------------------------------------------- /triggers/create_new_issue_shortcut.ts: -------------------------------------------------------------------------------- 1 | import type { Trigger } from "deno-slack-sdk/types.ts"; 2 | import { TriggerContextData, TriggerTypes } from "deno-slack-api/mod.ts"; 3 | import CreateNewIssueWorkflow from "../workflows/create_new_issue.ts"; 4 | 5 | /** 6 | * Triggers determine when workflows are executed. A trigger file describes a 7 | * scenario in which a workflow should be run, such as a user clicking a link. 8 | * Learn more: https://api.slack.com/automation/triggers/link 9 | */ 10 | const createNewIssueShortcut: Trigger< 11 | typeof CreateNewIssueWorkflow.definition 12 | > = { 13 | type: TriggerTypes.Shortcut, 14 | name: "Create GitHub issue", 15 | description: "Create a new GitHub issue in a repository", 16 | workflow: `#/workflows/${CreateNewIssueWorkflow.definition.callback_id}`, 17 | inputs: { 18 | interactivity: { 19 | value: TriggerContextData.Shortcut.interactivity, 20 | }, 21 | channel: { 22 | value: TriggerContextData.Shortcut.channel_id, 23 | }, 24 | }, 25 | }; 26 | 27 | export default createNewIssueShortcut; 28 | -------------------------------------------------------------------------------- /workflows/create_new_issue.ts: -------------------------------------------------------------------------------- 1 | import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts"; 2 | import { CreateIssueDefinition } from "../functions/create_issue.ts"; 3 | 4 | /** 5 | * A workflow is a set of steps that are executed in order. Each step in a 6 | * workflow is a function – either a built-in or custom function. 7 | * Learn more: https://api.slack.com/automation/workflows 8 | */ 9 | const CreateNewIssueWorkflow = DefineWorkflow({ 10 | callback_id: "create_new_issue_workflow", 11 | title: "Create new issue", 12 | description: "Create a new GitHub issue", 13 | input_parameters: { 14 | properties: { 15 | /** 16 | * This workflow users interactivity to collect input from the user. 17 | * Learn more: https://api.slack.com/automation/forms#add-interactivity 18 | */ 19 | interactivity: { 20 | type: Schema.slack.types.interactivity, 21 | }, 22 | channel: { 23 | type: Schema.slack.types.channel_id, 24 | }, 25 | }, 26 | required: ["interactivity", "channel"], 27 | }, 28 | }); 29 | 30 | /** 31 | * Collecting input from users can be done with the built-in OpenForm function 32 | * as the first step. 33 | * Learn more: https://api.slack.com/automation/functions#open-a-form 34 | */ 35 | const issueFormData = CreateNewIssueWorkflow.addStep( 36 | Schema.slack.functions.OpenForm, 37 | { 38 | title: "Create an issue", 39 | interactivity: CreateNewIssueWorkflow.inputs.interactivity, 40 | submit_label: "Create", 41 | description: "Create a new issue inside of a GitHub repository", 42 | fields: { 43 | elements: [{ 44 | name: "url", 45 | title: "Repository URL", 46 | description: "The GitHub URL of the repository", 47 | type: Schema.types.string, 48 | format: "url", 49 | }, { 50 | name: "title", 51 | title: "Issue title", 52 | type: Schema.types.string, 53 | }, { 54 | name: "description", 55 | title: "Issue description", 56 | type: Schema.types.string, 57 | long: true, 58 | }, { 59 | name: "assignees", 60 | title: "Issue assignees", 61 | description: 62 | "GitHub username(s) of the user(s) to assign the issue to (separated by commas)", 63 | type: Schema.types.string, 64 | }], 65 | required: ["url", "title"], 66 | }, 67 | }, 68 | ); 69 | 70 | /** 71 | * A custom function can be added as a workflow step to modify input data, 72 | * interact with an external API, and return responses from the API for use in 73 | * later steps. 74 | * Learn more: https://api.slack.com/automation/functions/custom 75 | */ 76 | const issue = CreateNewIssueWorkflow.addStep(CreateIssueDefinition, { 77 | /** 78 | * The credential source defines which external authentication tokens are 79 | * passed to the function. These are automatically injected at runtime. 80 | * Learn more: https://api.slack.com/automation/external-auth#workflow 81 | */ 82 | githubAccessTokenId: { 83 | credential_source: "DEVELOPER", 84 | }, 85 | url: issueFormData.outputs.fields.url, 86 | title: issueFormData.outputs.fields.title, 87 | description: issueFormData.outputs.fields.description, 88 | assignees: issueFormData.outputs.fields.assignees, 89 | }); 90 | 91 | /** 92 | * Messages can be sent into a channel with the built-in SendMessage function. 93 | * Learn more: https://api.slack.com/automation/functions#catalog 94 | */ 95 | CreateNewIssueWorkflow.addStep(Schema.slack.functions.SendMessage, { 96 | channel_id: CreateNewIssueWorkflow.inputs.channel, 97 | message: 98 | `Issue #${issue.outputs.GitHubIssueNumber} has been successfully created\n` + 99 | `Link to issue: <${issue.outputs.GitHubIssueLink}>`, 100 | }); 101 | 102 | export default CreateNewIssueWorkflow; 103 | --------------------------------------------------------------------------------