Appleのショートカット機能を使ってGitHubにIssueを追加する

2023/08/16に公開

前提と範囲

筆者はToDo管理をGitHubに作ったプライベートリポジトリと,そのリポジトリのIssueを紐付けるGitHub Projects(V2)で行っています.

このToDo管理の方法や細かいノウハウについてはこの記事では議論しません.ToDoをIssueとして登録して,GitHub Projectsでカンバンな感じで管理してると思ってください.それTrelloでもできるでしょ?と言われるとその通りです.

なお,本記事では「ショートカット」のアクションを全て英語表記で解説します[1]

本記事ではGitHubのPersonal Access Tokenを使ってIssueとProjectの操作をしています.ショートカットの仕様上,iCloudを経由してあなたの全デバイスにPersonal Access Tokenが共有されて保存されることになるので取扱いには十分に注意してください.

要求

今回の目的は,Appleのショートカット機能を使ってGitHubにIssueを追加することです.
何故これが必要なのかというと,ToDoを管理するにあたって(個人的に)大切なことが「タスク登録が気軽にできること」だからです.ノートPCなどを使って作業している際はブラウザから簡単にToDo管理用のプロジェクトページに移動できるので,タスクの登録が簡単に可能なのですが,外出時やノートPCが手元にない時にタスクを追加したくなったときは登録が困難になります.「今は登録できないから後でタスク登録をしよう」なんていってその後タスクをしっかりと登録する保証なんて全くありません.タスクは発生した時点で即座に登録するに限ります.

解決方法

macOSやiOSで使用できる「ショートカット」機能を使って実現します.ショートカットは結構高機能なのですが,まともなプログラミング言語ではないので結構泥臭い実装になります.

GitHubへのIssue登録はGitHub REST APIを使用します.登録したIssueをGitHub Projectsに登録する際にはGitHub GraphQL APIを使用します.Projects関係の情報はREST APIが提供されていないのでこういう構成になります(多分全部GraphQL APIを使って実装も可能です).

ショートカットの流れ

ショートカットの大まかな流れは以下の通りです.

  1. 新規登録するIssueのタイトルをユーザに入力させる.
  2. GitHub REST APIを使用してIssueを登録する.
  3. 登録した結果から,新規登録したIssueの(GraphQLの)ノードIDを取得し,覚えておく.
  4. GitHub GraphQL APIを使用して登録したIssueをGitHub Projectsに紐付ける.
  5. 紐付けたIssue(GitHub ProjectsではItemという扱い)のStatusを変更する.

以降,この流れにそって具体的な実装を説明していきます.
ショートカットの作成はiPhoneでも可能ですが筆者はmacOSでやりました.macを持ってるならこちらを使う方がオススメです(iCloud経由でiPhoneと同期されます).

新規登録するIssueのタイトルをユーザに入力させる


この部分は非常に簡単にです. Ask for Input を使ってIssueのタイトルを入力してもらい,その結果を issue_title という変数に記録しておきます.この変数は次のステップでREST APIに送るリクエストに使います.

GitHub REST APIを使用してIssueを登録する

ショートカットからAPIを叩く際は Get Contetns of URL を使用します.アクセス先のURLを https://api.github.com/repos/<user_name>/<repo_name>/issues としてPOSTメソッドでリクエストを送るとIssueの新規作成ができます.この際, <user_name> 部分は自身のユーザ名, <repo_name> はIssueを新規作成したいリポジトリ名に適宜変更してください.

GitHubのREST APIにアクセスするためにはPersonal Access Tokenが必要になります.こちらのページで新規作成できるので作成しましょう.
なお,Fine-grainedなトークンとClassicなトークンがありますが,残念ながらFine-grainedなトークンはProjectsに関係する権限を設定できないので今回はClassicなトークンを作成しましょう.その際, repoproject の権限を有効にしておいてください.

Personal Access Tokenを作成したら,発行されたトークン文字列を手元に記録しておきましょう.今回のステップでは Get contetns of URL の Headers 部分で使用します.Headersを使うとショートカットが送るHTTPリクエストのヘッダを編集することができます.GitHub REST APIにアクセスするためには以下の様に設定します.

  • Authorization: Bearer <作成したPersonal Access Token>
  • Content-Type: application/json
    ヘッダへのKeyとValueの追加は左下に表示されているプラス(+)ボタンで可能です.

次に,新規作成するIssueの情報をJSON形式でRequest Bodyに設定します.
今回は以下の様に設定しました.

  • title: issue_title
  • body: このissueはショートカットにより自動生成されました

bodyのvalueは好みに応じて適宜カスタマイズしてください.注意点として,issue_title部分は必ず変数を参照してください.変数名を途中まで入力すると変数が候補として提示されるはずなのでそれをクリックして通常のテキストではなく変数を参照するように設定してください.

登録した結果から新規登録したIssueの(GraphQLの)ノードIDを取得し覚えておく

正常にIssueの登録が完了すれば,Get Contents of URLの結果には新規登録したIssueの情報がJSON形式で返ってきます.返ってくるJSONの詳細の紹介は割愛しますが,今回はGraphQLのノードIDを取得します.ノードIDは node_id に記録されています.注意点としては id はまた別のIDで,GraphQLで基本的に使用するのは node_id の方です(この id はGraphQLでは database idと呼ぶらしい).

JSONからデータを取得するためには Get Dictionary Value アクションを使用します.また,この値はこの後のステップで使用するので created_issue_node_id という変数に格納しておきます.

GitHub GraphQL APIを使用して登録したIssueをGitHub Projectsに紐付ける

新規作成したIssueを特定のGitHub Projectsに紐付けるにはGraphQLを使用する方法があります.APIの仕様上REST APIでは実現できません.Issueとプロジェクトの紐付けには addProjectV2ItemById mutationを使用します.

mutation {
  addProjectV2ItemById(input: {projectId: "<project_id>" contentId: "<created_issue_node_id>" } )
  {
    item {
      id
    }
  }
}

ここで,<project_id>は紐付け先のプロジェクトのID,<created_issue_node_id>は直前のステップで記憶した同名の変数を表しています.変数は既に記録済みなので問題ないのですが,プロジェクトのIDは現時点では分かっていません.面倒ですが,PythonやGitHubが提供しているGraphQLのエクスプローラーを使ってプロジェクトのIDを取得しましょう.

以下に,私がプロジェクトIDを取得するのに使用したPythonスクリプトを示します.

import requests
import json

TOKEN = "<Personal Access Token>"

# エンドポイント
ENDPOINT = "https://api.github.com/graphql"

# ヘッダー設定
headers = {
    "Authorization": f"Bearer {TOKEN}",
    "Content-Type": "application/json"
}

query = """
{
  repository(owner:"<YOUR_USER_NAME>", name:"<REPOSITORY_NAME>") {
    projects(first: 10) {
      nodes {
        name
        id
      }
    }
  }
}
"""

# POSTリクエスト
response = requests.post(ENDPOINT, headers=headers, json={'query': mutation2})

# レスポンスの表示
print(json.dumps(response.json(), indent=2))

<Personal Access Token><YOUR_USER_NAME><REPOSITORY_NAME>は適宜自身の情報に置き換えてください.なお,このスクリプトで送信しているクエリはかなり適当で,自身のプロジェクトを10件取ってきて,名前とidを表示しているだけです.プロジェクトが10件以上ある場合は適宜数を増やす必要があります.(筆者はユーザに紐付いているプロジェクトが1件しかなかったのでこれで十分でした)

プロジェクトIDを取得したら Text アクションを使用してmutation文字列を作成します.これも
変数(add_item_mutation)に格納しておきましょう.

mutation文字列ができたので,Get contetns of URLアクションを使って紐付けを行います.ここの画像は省略しますが,概要は以下のとおりです.

  • アクセス先のURLを https://api.github.com/graphql にする
  • Headers部分はREST APIでのアクセス時と同様にAuthorizationとContent-Typeを設定する
  • Request BodyはKeyがquery,Valueがadd_item_mutation変数となるデータとなるようにする.

これで新規作成したIssueがプロジェクトに紐付けられるのですが,このままではStatusが設定されていないため,Board形式では宙ぶらりんな状態になってしまいます.次のステップでStatusを設定しましょう.

紐付けたIssue(GitHub ProjectsではItemという扱い)のStatusを変更する

先ほどのステップと同様にGraphQL APIを使用して紐付けたIssueのStatusを変更します.筆者は「Inbox」という,このショートカットで作成したIssueをとりあえず置いておくためのStatusを作成し,それに変更するようにしました.

ここでの処理ではupdateProjectV2ItemFieldValue mutationを使用します.この際,Issueのプロジェクト上でのID(Item ID),変更したいフィールド(つまりStatus)のID,設定する値(つまりInbox)のIDを事前に取得しておく必要があります.

IssueのItem IDは,先ほど説明した Get Contents of URLに続けて以下のようなアクションを並べて取得します.

これは,ただ面倒なだけなのですが,GraphQLでプロジェクトへの紐付けを行った結果がJSONとして返ってきて,そのJSONの data->addProjectV2ItemById->item->id に新規登録されたItemのIDが記録されているのでそれを Get Dictionary Value を連続して使用することで取得しています.最後に,取得したIDをprojectv2item_node_idという変数に格納しています.

Statusと,InboxのIDを取得するためには以下のクエリを使用しました.先ほどと同様にPythonスクリプトかGitHubのエクスプローラーを使用してクエリを送信して確認してください.<YOUR_PROJECT_ID>は自身のプロジェクトのIDに置き換えてください.返ってくる結果はそれほど複雑ではないので,どれがStatusのIDで,どれがInbox(というか目的のオプションの)IDなのかは結果を見れば分かると思います.

query {
  node(id: "<YOUR_PROJECT_ID>") {
    id
    ... on ProjectV2 {
      id
      url
      fields(first: 100) {
        edges {
          node {
            ... on ProjectV2SingleSelectField {
              id
              name
              options {
                id
                name
              }
            }
          }
        }
      }
    }
  }
}

これでmutationに必要な情報が集まったので,TextSet variableを使って以下のmutation文字列をchange_status_mutationという変数に格納します.

mutation {
    updateProjectV2ItemFieldValue(
      input: {
        projectId: "<project_id>"
        itemId: "<projectv2item_node_id>"
        fieldId: "<status_id>"
        value: {
          singleSelectOptionId: "<inbox_id>"
        }
      }
    ) {
      projectV2Item {
        id
      }
    }
}

後は,一つ前のステップと同様に Get Contents of URL アクションを使用して上記mutation文字列をqueryとして送信すれば,無事Statusの変更が完了します.

最後に処理が完了したことを示す表示を Result アクションを使ってしましょう.

これでショートカットは完成です.MacやiPhoneから簡単にGitHubにissue登録できるようになります.

おまけ:Apple Watchを使ってタスクを登録する

ここまでできたらApple Watchを使って音声入力でタスク登録をしたいと思ったのですが,思ったより簡単にできました.やり方としては,ここまで作ったショートカットを複製し,作成したショートカットの名前を「タスクの追加」とすることでSiriに「タスクの追加」と言うだけでこのショートカットを起動できるようになります.そして,最初のアクションをテキスト入力ではなく,Dictate Textアクションにすることで,音声入力が可能になります.残念ながら,Siriで起動したら音声入力,Siri以外で起動したらテキスト入力という分岐は現時点のショートカットの仕様では実現不可能なようなので,シンプルにショートカットを2バージョン用意して対応しています.テキスト版のショートカット名はSiriでうっかり起動されないように長めの紛らわしくない名前に変更しておきましょう.

脚注
  1. 筆者の環境が英語UIなので一々アクションの和訳を調べるのが面倒という理由です. ↩︎

Discussion