iPhoneショートカットからLinearにInbox issueを一発登録する

iPhoneショートカットからLinearにInbox issueを一発登録する

はじめに

今回はプロジェクト管理ツール「Linear」で、iPhoneのショートカットからissueの追加、および任意のコメントの追加ができるようにする方法について備忘録を残したいと思います。

Linearが公開しているGraphQL APIをiPhoneのショートカットから直接叩くことで、思いついたタスクをその場でLinearのInboxに集約できます(+補足情報をコメントとして追加することも可能です)。すぐに記録できること、記録先が1か所に集約されて一元管理できること、(なんかかっこいい)という点が、この仕組みの利点です。

もともとタスク管理には TickTick を使っていました。あるとき、「もっとロマンのあるツールを使いたい(TickTickは個人で使うタスク管理アプリとしては十分すぎる機能があります。)」という気持ちが勝ってしまい、Linear に乗り換えました。開発チーム向けのプロジェクト管理ツールであるLinearを個人のタスク管理に使うのは正直オーバースペックな感じがしますが、ロマンを追い求めて移行しました。その中で日頃のタスクやメモなどを思いついた瞬間に記録できるようにこの仕組みを作成しました。

全体構成

必要なアクションはシンプルで、以下の2つだけで完結します。

  1. テキストを要求 — issueのタイトルを入力
  2. URLの内容を取得 — Linear APIにGraphQL mutationを送信

これだけで、Linear側に追加したissueが反映されます。追加したissueには、続けて任意のコメントを投稿することもできます。

事前準備:Linear側で必要な3つの情報の取得

ショートカット本体を組む前に、Linear側で以下の3つ(APIキー・teamId・stateId)を用意します。

1. 個人APIキーの発行

Linear → Settings → Security & access → Personal API keys から発行します(lin_api_... という形式のキーが得られます)。
APIキー発行画面

2. teamId とステータス(state)のIDの取得

Apollo Studio Explorerで以下のクエリを実行して確認します。

query {
  teams {
    nodes {
      id
      name
      key
      states {
        nodes {
          id
          name
          type
        }
      }
    }
  }
}

実行結果から、対象チームの teamId と、"Inbox" に対応する状態の stateId をメモしておきます。

3. (アサインしたい場合)ユーザーIDの取得

上記と同様に、特定のユーザーに自動でアサインしたい場合は、以下のクエリで userId も取得しておきます。

query {
  users {
    nodes {
      id
      name
      email
    }
  }
}

ユーザーID取得結果

GraphQL Mutationの組み立て

以下のmutationをiPhoneのショートカット側に入力します。

mutation IssueCreate(
  $title: String!
  $teamId: String!
  $stateId: String!
  $assigneeId: String
) {
  issueCreate(
    input: {
      title: $title
      teamId: $teamId
      stateId: $stateId
      assigneeId: $assigneeId
    }
  ) {
    success
    issue {
      identifier
      url
      assignee {
        id
        name
      }
    }
  }
}

assigneeId はアサインしない場合を考慮し、String!(必須)ではなくString(任意)として定義しています。返却フィールドに assignee { id name } を含めておくと、アサインが実際に反映されたかをレスポンスだけで確認できるため、使用する上では必須ではありませんが、デバッグ用に書いておいたほうがよいです。

ショートカットの組み方

実際のショートカット設定画面

ショートカット設定画面
ショートカット設定画面
ショートカット設定画面
ショートカット設定画面
ショートカット設定画面
ショートカット設定画面

1. 「URLの内容を取得」をJSON本文モードに設定

アクションを展開し、「リクエスト本文」を「なし」から「JSON」に切り替えます。

2. 辞書エディタによるフィールドの構築

生のJSON文字列を貼り付けるのではなく、Shortcutsの辞書エディタ(キーと値をGUI上で組み立てられる入力方式)でキーを組み立てていきます。なお、Shortcutsでは直前のアクションの出力を次のアクションに変数として渡す仕組みを「マジック変数」と呼びます。

キー
query テキスト 上記mutation文字列をそのまま貼り付け
variables 辞書 下記の通りネストして構築

variables は必ず「辞書」型に設定します。
variables 辞書の中身:

キー
title テキスト 「テキストを要求」の出力(マジック変数)を挿入
teamId テキスト 固定値
stateId テキスト 固定値
assigneeId テキスト 固定値(常に同じ人にアサインする場合)

3. ヘッダーの設定

同アクション内の「ヘッダーを表示」から以下を追加します。

  • Content-Type: application/json
  • Authorization: lin_api_xxxxxxxx

ハマったポイント

ここでは、構築中に実際に遭遇したエラーを備忘録として2つ残しておきます。

Authentication required, not authenticated
teamIdをApollo Studio Explorerで確認する際に、Headersタブへの認証情報追加を忘れていたことが原因で発生します。

Variable "$stateId" of required type "String!" was not provided.
mutation文字列は記載していましたが、Variablesタブに対応するキー自体を追加し忘れていたことが原因です。クエリ側の変数定義と、実際に渡すvariablesオブジェクトのキーは別物なので、両方揃っているか確認する必要があります。

発展:任意のコメントの追加

issueのタイトルとは別に、コメントとして自由記述を残したい場合は、commentCreate mutationを使用します。ただし、commentCreateissueIdには1回目のissueCreateのレスポンスから得られるissueのIDが必要なため、1回のリクエストにまとめることはできません(GraphQL自体が複数mutationを1リクエストにまとめられないわけではなく、後続の入力に前段の出力を使うという依存関係があるためです)。
「issue作成 → 作成されたissueのIDを取得 → コメント作成」という2回のAPI呼び出しに分ける必要があります。

commentCreateのmutation

mutation CommentCreate($issueId: String!, $body: String!) {
  commentCreate(input: { issueId: $issueId, body: $body }) {
    success
    comment {
      id
      body
      createdAt
    }
  }
}

issueIdには、1回目のリクエスト(issueCreate)のレスポンスに含まれるissue.ididentifierではなくUUID形式のidの方)を渡します。

レスポンスからissueCreate.issue.idを取り出す方法

1回目の「URLの内容を取得」のレスポンスは以下のような構造になっています。

{
  "data": {
    "issueCreate": {
      "issue": {
        "id": "...",
        "identifier": "...",
        "url": "..."
      }
    }
  }
}
# アクション キー 辞書(入力元)
1 辞書の値を取得 data 「URLの内容を取得」の出力(マジック変数)
2 辞書の値を取得 issueCreate ①の出力
3 辞書の値を取得 issue ②の出力
4 辞書の値を取得 id ③の出力

具体的な操作は、「辞書の値を取得」アクションを追加 → 「キー」欄にキー名を入力 → 「辞書」欄をタップしてキーボード上部の変数バーから直前の出力を選択、を4回繰り返すだけです。最後にできた④の出力が、commentCreatevariables.issueIdや通知表示に使う変数になります。

ショートカットの流れ(拡張版)

  1. テキストを要求 — issueのタイトル
  2. テキストを要求 — コメント本文(任意入力にしたいので、後述の分岐で空チェックする)
  3. URLの内容を取得(1回目) — issueCreate mutationを送信
  4. 辞書の値を取得(4段チェーン) — レスポンスからissueCreate.issue.idを取り出し、変数として保持
  5. もし文 — ②のコメント変数が空でないかを判定
    • 真の場合: URLの内容を取得(2回目)でcommentCreate mutationを送信。variablesに④のissue.idと②のコメント本文をセット
    • 偽の場合: 何もしない(issue作成だけで終了)
  6. 通知を表示 — 作成結果(identifier・コメントの有無)を表示

この時、2回目の「URLの内容を取得」も、ヘッダー(Content-TypeAuthorization)は1回目と同じ内容を設定する必要があります。