Slack スタンダードワークフロー（無料）もコードで管理する

こんにちは、Slack の[公式 SDK 開発](https://github.com/slackapi)と日本の Developer Relations を担当している瀬良 (@seratch) と申します :wave:

### プレミアムワークフローとスタンダードワークフロー

これまで Qiita などで TypeScript のコードを書いて自由にカスタムステップを実装する例を数多く紹介してきました。

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

https://qiita.com/seratch/items/23e22b4eed489cdd04d7

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

これらは「プレミアムワークフロー」という扱いになるもので、ワークフローの実行回数に応じて従量課金となります。スタンダードかプレミアムかの最新の定義は[こちらのページ](https://slack.com/intl/ja-jp/help/articles/15363357403411)を参照してください。

### 開発者目線でのスタンダードワークフローの困りごと

ただ、この新しい Slack のワークフローの仕組みをいち早く活用されている方も、実際のところ、上で挙げたようなプレミアムワークフロー（従量課金）よりもスタンダードワークフロー（無料）の方をより多く利用されているのではないかと思います。

そして、開発者の方の中には、以下のような課題を感じている人もいるのではないでしょうか？

1. スタンダードワークフローも画面上でポチポチ操作するのではなく GitHub でバージョン管理したい、プルリクエストで変更のレビューもしたい
1. ワークフロービルダー画面上でワークフローのコピーを作ることはできるが、定義情報の export/import ができなくなったので、ワークスペースを跨いで再利用しづらい

この記事では、これらのニーズを満たす一つの方法として **コードでトリガー・ワークフローを管理する方法** について紹介します。

・・・
・・・・・・
・・・・・・・・・

**「ちょっと待って、コード書いたらプレミアムワークフローになって従量課金になるんじゃないの？」**

・・・・・・・・・
・・・・・・
・・・

**いいえ、Slack CLI を使ってワークフローとそのトリガーを管理するだけであれば、公開されたワークフローはスタンダードワークフローのままです。** 

プレミアムワークフローになるのは、Slack CLI を使うだけでなく、カスタムステップ（コードでいうところの `functions/` 配下の TypeScript で実装された独自処理のこと）を有効にしたときだけです。

ということで、ワークフロービルダーではなく Slack CLI を使うことで、上の二つの問題は解決することができるかもしれません。

### メリット・デメリット

ワークフローをコードで管理すると、さまざまなメリットがあります。

同じアプリ名・アイコンで OK な複数のワークフローは、一つの GitHub リポジトリで管理することができます。アプリを分けたい場合も、新しいプロジェクトでこれまでに作ったトリガーやワークフローの定義を流用することができるでしょう。

ワークフローへの変更を PR でレビューして、main ブランチにマージしたら自動でデプロイするよう GitHub Actions のジョブをセットアップできます。

https://qiita.com/seratch/items/8df4cb0b41e756edc578

運用面でもできることが増えます。そのワークフローのコラボレーターは `slack activity -t` というコマンドでリアルタイムで出力されたログを確認することができるようになります。

また、以下で紹介する「チャンネルをいい感じに作ってくれる君」の例でも説明しますが、コードで表現すると、まだワークフロービルダー画面上ではできないことがいちはやくできたりします。

最後に、これは普通ではないユースケースではありますが・・ワークフロービルダー画面上だとツラいような数十個のステップの追加も、慣れたエディター上のタイピングなら簡単に定義できるでしょう（ちなみにワークフローに設定できるステップの最大数は、この記事執筆時点で 100 ステップです）。

ただ、良いことばかりではなく、デメリットもありますのでそちらも触れておきます。

1. コードで管理する場合、そのワークフローはワークフロービルダーの画面から操作できません
1. TypeScript のコードを読み書きする必要があります
1. チャンネル・ユーザー ID といった概念や Block Kit など技術的な理解が必要となります
1. この記事投稿時点では、コネクターステップを（簡単に）組み込むことはできません

これらのデメリットを考えると、一般のユーザーはやはりワークフロービルダーの画面上で操作する方がよいかと思いますが、開発者の方であれば 4. のコネクターステップを使えない制約以外はあまり問題にならないのではないかと思います。

**EDIT:** [deno_slack_hub](https://github.com/slackapi/deno-slack-hub) というモジュールがリリースされたので、コネクターステップも簡単に利用できるようになりました（なお、スタンダードワークフローでは一つだけコネクターステップを組み込むことができます）。

それでは、実際にどのようにするのかを、具体例とともに紹介していきます。

### チャンネルをいい感じに作ってくれる君

最初にチャンネルをいい具合にセットアップするためのワークフローを考えてみましょう。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/70317fba-b679-6137-3ece-60d25fcfdd6b.png" width=400/>

このワークフローは

* リンクトリガーから起動するとモーダルが開いて、チャンネル名、トピック、チャンネルのマネージャー、その他最初から参加させたいメンバーを入力する
* 指定されたチャンネル名のチャンネルが作られて、指定されたチャンネルマネージャーが設定される
* チャンネルのトピックを指定された内容で更新する
* 他に参加させたいメンバーをチャンネルに招待する
* ワークフローからウェルカムメッセージを送る

という一連の作業を自動化してくれるものです。手作業でやってもよいのですが、抜け漏れを防げるという意味でもワークフローにする意味がありますね。

実際の操作としては、リンクトリガーをチャンネルのメッセージ・ブックマーク、canvas ドキュメントなどに埋め込んでおいて、そこから起動します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/f55c35c7-6a26-e1f6-cc13-962e50b39d1f.png" width=400/>

以下のようなモーダルダイアログで情報を入力します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/93625faa-8ee4-a3df-78d2-d21d2437dd00.png" width=400/>

いい感じにチャンネルをセットアップしてくれます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/84aa115c-c1bd-1459-67a4-967334c5e26d.png" width=400/>

一度作ってしまえばそんなにコピーして量産しないものではあるかもしれませんが、組織や会社ごとにカスタマイズしたいというニーズがありそうなユースケースではあります。

これをコードで表現してみましょう。Slack CLI をまだセットアップしていない場合は、以下のページの手順で `slack login` まで完了させてください。

https://api.slack.com/automation/quickstart

準備ができたら `slack create channel-creator` というコマンドを実行して、"Blank template" を選択します。

```bash
$ slack create channel-creator
? Select a template to build from: Blank template
⚙️  Creating a new Slack app in ~/tmp/channel-creator

📦 Installed project dependencies

✨ channel-creator successfully created

🧭 Explore the documentation to learn more
   Read the README.md or peruse the docs over at api.slack.com/automation
   Find available commands and usage info with slack help

📋 Follow the steps below to begin development
   Change into your project directory with cd channel-creator/
   Develop locally and see changes in real-time with slack run
   When you're ready to deploy for production use slack deploy
```

とりあえず何もないプロジェクトですが、起動できることを確認してみましょう。

```bash
cd channel-creator/
slack run
```

うまくいったら以下のような表示になります。

```
$ slack run
? Choose a local environment Install to a new team
? Install to a new team seratch T03E94MJU

🔔 If you leave this team, you can no longer manage the installed apps
   Installed apps will belong to the team if you leave the workspace

Updating local app install for "Acme Corp"

⚡ Listing triggers installed to the app...
   There are no triggers installed for the app

⚡ Create a trigger
   Searching for trigger definition files under 'triggers/*'...
   No trigger definition files found
   Learn more about triggers: https://api.slack.com/automation/triggers/link

✨ seratch of Acme Corp
Connected, awaiting events
```

それでは、このアプリに手を入れていきます。まず新しく `workflow.ts` というファイルを以下の内容で作成します。

```typescript
// ここでは説明を簡単にするために一つのファイルに書いていますが、通常は
// workflows/setup_channel.ts
// triggers/setup_channel.ts
// のように二つのファイルに分けるのが一般的です

// -------------------
// ワークフローの定義
// -------------------

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

export const workflow = DefineWorkflow({
  callback_id: "setup-channel-workflow",
  title: "チャンネルをいい感じに作ってくれる君",
  input_parameters: {
    properties: {
      interactivity: { type: Schema.slack.types.interactivity },
      user_id: { type: Schema.slack.types.user_id },
    },
    required: ["interactivity", "user_id"],
  },
});

const form = workflow.addStep(Schema.slack.functions.OpenForm, {
  title: "新しいチャンネル",
  interactivity: workflow.inputs.interactivity,
  submit_label: "チャンネル作成実行",
  fields: {
    elements: [
      {
        name: "channel_name",
        title: "チャンネル名",
        type: Schema.types.string,
        minLength: 3, // inclusive
        maxLength: 80, // inclusive
        description:
          "このワークフローはチャンネル名がすでに使われていないか、チャンネル名に使えない文字がないかをチェックしません。注意して名前を入力してください。",
      },
      {
        name: "channel_topic",
        title: "チャンネルのトピック",
        type: Schema.types.string,
        minLength: 10, // inclusive
        maxLength: 250, // inclusive
        description: "チャンネルの目的などを明確に書いてください。",
      },
      {
        name: "channel_managers",
        title: "チャンネルマネージャー",
        type: Schema.types.array,
        items: { type: Schema.slack.types.user_id },
        default: [workflow.inputs.user_id],
        description: "あなたと少なくとももう一人を指定してください。",
      },
      {
        name: "channel_members",
        title: "チャンネルメンバー",
        type: Schema.types.array,
        items: { type: Schema.slack.types.user_id },
        default: [workflow.inputs.user_id], // コードの場合は、この項目を optional にして指定がなかったときに InviteUserToChannel をスキップするということができないため、作成者自身を重複して指定している
        description:
          "上で指定したマネージャー以外で参加して欲しい人を指定してください。",
      },
      {
        name: "is_private",
        title: "プライベートチャンネルとして作成",
        type: Schema.types.boolean,
        default: false, // 「パブリックなら true」の方が自然だが、カスタムステップなしでここの入力値を次のステップに渡すまでに反転させることができないため、プライベートかどうかでそのまま指定してもらっている
        description:
          "パブリックチャンネルを推奨しますが、用途に合わせてプライベートにしてください。",
      },
    ],
    required: [
      "channel_name",
      "channel_topic",
      "channel_managers",
      "channel_members",
      "is_private",
    ],
  },
});

// 指定された名前、マネージャーとともにパブリック・プラベートチャンネルを作成
const channelCreation = workflow.addStep(Schema.slack.functions.CreateChannel, {
  channel_name: form.outputs.fields.channel_name,
  manager_ids: form.outputs.fields.channel_managers,
  is_private: form.outputs.fields.is_private,
});

// 指定された内容でトピックを更新
workflow.addStep(Schema.slack.functions.UpdateChannelTopic, {
  channel_id: channelCreation.outputs.channel_id,
  topic: form.outputs.fields.channel_topic,
});

// 追加のメンバーを招待
workflow.addStep(Schema.slack.functions.InviteUserToChannel, {
  channel_ids: [channelCreation.outputs.channel_id],
  user_ids: form.outputs.fields.channel_members,
});

// ウェルカムメッセージを送信
workflow.addStep(Schema.slack.functions.SendMessage, {
  channel_id: channelCreation.outputs.channel_id,
  message: `<!here> :wave: みなさん、ようこそ！
<@${workflow.inputs.user_id}> が以下の目的のためにこのチャンネルを作りました！
>${form.outputs.fields.channel_topic}
よろしくお願いします！
`,
});

// -------------------
// トリガーの定義
// -------------------
// slack trigger create --trigger-def workflow.ts

import { Trigger } from "deno-slack-sdk/types.ts";
import { TriggerContextData, TriggerTypes } from "deno-slack-api/mod.ts";

const trigger: Trigger<typeof workflow.definition> = {
  type: TriggerTypes.Shortcut,
  name: "チャンネルをいい感じに作ってくれる君",
  description: "チャンネルを新しく作ります",
  workflow: `#/workflows/${workflow.definition.callback_id}`,
  inputs: {
    interactivity: { value: TriggerContextData.Shortcut.interactivity },
    user_id: { value: TriggerContextData.Shortcut.user_id },
  },
};

export default trigger;
```

さらに元々存在している `manifest.ts` を以下のように書き換えます。

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

export default Manifest({
  name: "Channel Creator",
  description: "チャンネルをいい感じに作ってくれる君",
  icon: "assets/default_new_app_icon.png",
  workflows: [SetupChannel],
  botScopes: [
    "commands",
    "chat:write",
    "chat:write.public",
    "channels:manage", // チャンネルを作る
    "groups:write", // プライベートチャンネルに招待する
  ],
});
```

変更はこれだけです。リンクトリガーの作成は `slack trigger create --trigger-def workflow.ts` というコマンドを実行してください。

ターミナル上の出力に `https://slack.com/shortcuts/...` のような URL が表示されますので、それを使います。

ほぼ同等のモーダルダイアログが表示されますが、いくつか違いがあることに気づくはずです。


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/d4e0d6a4-9633-dca0-9d7d-95c7fd1ce09e.png" width=400 />

以下のような違いがあります。

* チャンネル名、チャンネルトピックの桁数チェックが入っている
* チャンネルマネージャー、メンバーにあらかじめ自分自身が追加されている
* プライベートチャンネルとして作成するかどうかが選べるようになっている
* 送信ボタンのラベルがカスタマイズされている

これらは将来的にはワークフロービルダー画面上でできるようになるかもしれませんが、現時点ではコードで表現したときだけに使える設定です。このように直接プラットフォームの機能を SDK 経由で使うことでいち早くより細かい設定や機能を利用できたりもします。

このフォームを送信すると、以下のように同じ要領でチャンネルが作成されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19163/70bf62cc-fea9-750c-83e0-ef329d49ad71.png" width=400 />

これを実運用する場合 `slack deploy` コマンドでデプロイした後、そのデプロイされたアプリのトリガーを作成し、それをワークスペース内で共有するだけです。GitHub Actions で次回以降のデプロイを自動化しておくと、尚良いでしょう。

https://qiita.com/seratch/items/8df4cb0b41e756edc578


### 毎朝のデイリースタンドアップ

・・・というワークフローも紹介しようかと思っていたのですが、ここまでですでにだいぶ長い記事になってきたので、GitHub リポジトリとしてまとめておきました。ぜひこちらも見てみてください。

https://github.com/seratch/slack-daily-standup-ja


### コネクターステップを一つ使ったスタンダードワークフロー

コネクターステップを組み込む方法についても記事を書きました。こちらもぜひ読んでみてください。

https://qiita.com/seratch/items/28e2503a4ec72c60710b


### 終わりに

いかがだったでしょうか？「思ったより簡単だな」と感じていただけていれば嬉しいです。

最初はお試しでスタンダードワークフローで運用してみて、使っていくうちに「これでも便利だが、もっと作り込みたいところが出てきた。従量課金でもいいからもっと細部を作り込みたい！」となった段階で、カスタムステップを導入してより洗練したワークフローに進化させるという道もあるかなと思います。

その場合にも今回紹介した方法ではじめからコードで管理していれば、ただその GitHub リポジトリにプルリクエストを送るだけで移行ができます。

ぜひ快適なワークフロー活用を楽しんでみてください。それでは！ :rocket: