Slackの次世代プラットフォームを使ってJiraと連携するアプリを作る

## はじめに
KWCのSRE部に所属している、@kumico_kwcです。
この記事は[KWC Advent Calendar 2022](https://qiita.com/advent-calendar/2022/kwc)の9日目の投稿となります。本稿では、現在オープンベータとして提供されている[Slackの次世代プラットフォーム](https://api.slack.com/future)を使ったSlackアプリ開発を紹介します。

## Slackの次世代プラットフォーム
Slackのマネージドインフラストラクチャと[Deno](https://deno.land/)によって、従来のSlackアプリ開発とは全く違う手法を提供してくれるプラットフォームです。[Slack CLI](https://api.slack.com/future/tools/cli)と[SDK](https://github.com/slackapi/deno-slack-sdk)を使うのですが、開発からデプロイ、アプリの設定まで一つのプラットフォームで一貫して行えるので、作業環境とSlackやブラウザを行ったり来たりしないでよくなります。また、デプロイ先に何を採用するか悩まなくてもよいので、コードを書くことに集中することができます。

次世代プラットフォームの詳細については、公式ドキュメントと以下の解説記事が非常に参考になりました。

https://api.slack.com/future

https://qiita.com/seratch/items/ecc16b845483c9d6f9e1

## SlackからJira Issueを作成できるアプリを作る
今回は、「JiraのIssueを作成できる」アプリを作りました。以下の順番で処理が進んでいくアプリになります。
1. `Link trigger`で起動
1. フォームの呼び出し
1. フォームにJiraボードのID、Issueの要約・説明を入力
1. 送信
1. メッセージを返して終了
### Functions、Workflows、Triggers
次世代プラットフォームでは、Slackアプリは主に３つのコンポーネントで構成されます。

#### Functions
Functionsは定義された入力に基づいてデータを受け取り、何らかの処理を行ってから、定義された出力を返します。Functionsは、[組み込みFunctions](https://api.slack.com/future/functions)と[カスタムFunctions](https://api.slack.com/future/functions/custom)の2つに分かれます。今回は、4,5の**フォームから受け取ったデータをもとに依頼を送信する処理**と、**送信した後に投稿元のチャンネルにメッセージを返す処理**までをカスタムFunctionとして実装しました。

:::note info
KWCでは、jira-gatwayというJiraを外部から扱いやすくするためのエンドポイントを社内用で開発しています。以下のコードに出てくるJiraGatewayという単語はそのエンドポイントに関するものになります。
:::

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

// 実装するFunctionの入出力を定義している
export const JiraGatewayFunction = DefineFunction({
  callback_id: "jiragateway-function",
  title: "Create Jira Issue",
  description: "Create Jira Issue",
  source_file: "functions/function.ts",
  // フォームからstring型のproject, summary, descriptionを受け取る
  input_parameters: {
    properties: {
      project: {
        type: Schema.types.string,
      },
      summary: {
        type: Schema.types.string,
      },
      description: {
        type: Schema.types.string,
      },
    },
    required: ["project", "summary", "description"],
  },
  output_parameters: {
    properties: {
      message: {
        type: Schema.types.string,
      },
    },
    required: ["message"],
  },
});

// 外部APIにデータを送信する箇所と出力メッセージを実装
export default SlackFunction(
  JiraGatewayFunction,
  async ({ inputs, env }) => {
    const data = {
      project: inputs.project,
      summary: inputs.summary,
      desription: inputs.description,
    };
    const headers = {
      "X-API-KEY": env.X_API_KEY,
      "Content-Type": "application/json",
    };
    const endpoint = "https://api.example.net";
    const response = await fetch(endpoint, {
      method: "POST",
      body: JSON.stringify(data),
      headers: headers,
    });
    if (!response.ok) {
      return {
        outputs: {
          message: "Failed to create Jira Issue!",
        },
      };
    }
    return {
      outputs: {
        message: "Created Jira Issue!",
      },
    };
  },
);
```

#### Workflows
Functionsを束ねたものが[Workflows](https://api.slack.com/future/workflows)です。組み込みのFunctionsやカスタムFunctionsをWorkflow内でつなげていくことで、アプリを構築することができます。後述するTriggersからWorkflowsを起動するために、Triggersの出力を受け取るための入力を定義する必要があります。アプリにはn個のWorkflowを定義することができますが、今回は実装する機能が少ないので、ひとつのWorkflowで完結しています。

```typescript
import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";
import { JiraGatewayFunction } from "../functions/function.ts";

// Triggersから値を受け取るための入力を定義
const JiraGatewayWorkflow = DefineWorkflow({
  callback_id: "jiragateway_workflow",
  title: "create jira issue",
  description: "create jira issue",
  input_parameters: {
    properties: {
      channel_id: {
        type: Schema.slack.types.channel_id,
      },
      interactivity: {
        type: Schema.slack.types.interactivity,
      },
    },
    required: ["channel_id", "interactivity"],
  },
});

// フォームを展開するための組み込みfunctionを追加
const JiraGatewayForm = JiraGatewayWorkflow.addStep(
  Schema.slack.functions.OpenForm,
  {
    title: "Create Jira Issue",
    interactivity: JiraGatewayWorkflow.inputs.interactivity,
    submit_label: "Create Jira Issue",
    fields: {
      elements: [{
        name: "project",
        title: "Project",
        type: Schema.types.string,
        description: "Select the Jira Project Board",
        enum: [
          "SRE team",
          "Backend team",
        ],
        choices: [
          { value: "SRE", title: "SRE" },
          { value: "BACK", title: "Backend" },
        ],
        default: "SRE",
      }, {
        name: "summary",
        title: "Summary",
        type: Schema.types.string,
      }, {
        name: "description",
        title: "Description",
        type: Schema.types.string,
        long: true,
      }],
      required: ["project", "summary", "description"],
    },
  },
);

// 上記で実装したカスタムfunction JiraGatewayFunctionを追加
const requestJiraGateway = JiraGatewayWorkflow.addStep(
  JiraGatewayFunction,
  {
    project: JiraGatewayForm.outputs.fields.project,
    summary: JiraGatewayForm.outputs.fields.summary,
    description: JiraGatewayForm.outputs.fields.description,
  },
);

// 投稿元チャンネルにメッセージを出力する組み込みfunctionを追加
JiraGatewayWorkflow.addStep(Schema.slack.functions.SendMessage, {
  channel_id: JiraGatewayWorkflow.inputs.channel_id,
  message: requestJiraGateway.outputs.message,
});

export default JiraGatewayWorkflow;
```

#### Triggers
Workflowsを呼び出すために必要なのが、[Triggers](https://api.slack.com/future/triggers)です。Triggersにはメンションや、リアクションなど、Slackのイベントに関連したものや、Cronのように定期的に実行されるものなどが提供されています。今回は、次世代プラットフォームで導入された[Link Triggers](https://api.slack.com/future/triggers/link)を用いて、Workflowsを起動します。

```typescript
import { Trigger } from "deno-slack-api/types.ts";
import JiraGatewayWorkflow from "../workflows/workflow.ts";

// 上記のJiraGatewayWorkflowを呼び出すためのTrigger
const JiraGatewayTrigger: Trigger<typeof JiraGatewayWorkflow.definition> = {
  type: "shortcut",  // <- Link Trigger
  name: "Create Jira Issue",
  workflow: "#/workflows/jiragateway_workflow",
  inputs: {
    interactivity: {
      value: "{{data.interactivity}}", // フォームを呼び出すために必要
    },
    channel_id: {
      value: "{{data.channel_id}}", // チャンネルにメッセージを送信するために必要
    },
  },
};

export default JiraGatewayTrigger;
```

### Slack CLI
従来であれば、管理画面からアプリを登録する必要がありましたが、次世代プラットフォームではすべて、`slack`コマンドだけで行うことができます。アプリの権限やアイコンの設定もすべてマニフェスト(`manifest.ts`に記述)で管理し、マニフェストに基づいて`slack`コマンドがデプロイまで行ってくれます。

```typescript
import { Manifest } from "deno-slack-sdk/mod.ts";
import JiraGatewayWorkflow from "./workflows/workflow.ts";

export default Manifest({
  name: "jiragateway",
  description: "create Jira Issue",
  icon: "assets/default_new_app_icon.png",
  workflows: [JiraGatewayWorkflow],
  outgoingDomains: [
    "api.example.net", // 外部と通信するfunctionがある場合は設定する
  ],
  botScopes: [
    "commands", 
    "chat:write",
    "chat:write.public"
  ],
});
```

#### デプロイ・ローカル実行
ローカルでアプリを実行する
```terminal
$ slack run
```

Slackのクラウドにデプロイする
```terminal
$ slack deploy
```
ローカルで実行する場合と、デプロイする場合どちらも初めて実行する場合には、Slackのワークスペースにアプリの登録が行われます。ワークスペースにアプリを登録する権限がない場合でも、CLIから管理者に登録を依頼することができます(便利)。

#### Triggersの作成
アプリの登録ができたら、以下のコマンドでTriggersを作成します。
```terminal
$ slack trigger create --trigger-def /path/to/trigger.ts
```

#### アプリの呼び出し
今回は、Link Triggerを作成したため、ワークスペースにWorkflowのショートカットを登録するための特別なURLが発行されます。発行されたURLをチャンネルに投稿すると、ショートカットが生成されアプリを呼び出すことができるようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2984564/be9003bb-7d87-1765-c2d0-b5288c540400.png)
2回目以降は、チャンネル上部に表示されるワークフローからアプリを呼び出せます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2984564/28ac5a2f-e18f-494b-8e79-26041cafda51.png)
## おわりに
本稿では、次世代プラットフォームでのSlackアプリケーション開発手順について「JiraのIssueを作成できる」アプリを参考に紹介しました。従来の方法で開発したアプリを、次世代プラットフォームに乗せ換えるのはもう少し様子を見てからでもいいと感じてますが、新しくアプリを作る際は、次世代プラットフォームも選択肢の1つとして入ってくるのかなと思っています。本稿で紹介した内容が読んでくださった方のSlackアプリ開発になにか寄与できれば光栄です。

[KWC Advent Calendar 2022](https://qiita.com/advent-calendar/2022/kwc)も折り返しとなりました。後半も面白い記事が投稿されていきますので、お楽しみください！