🆕

GitHub Projects (Beta) に Issue を自動登録・削除する Actions を作ってみた

2021/10/23に公開
1

はじめに

みなさん、GitHub Projects (Beta) 使ってますよね?
え?まだ使ってない?そりゃぁ勿体ない!
今すぐ GitHub Issues の Sign up for the beta ボタンをクリックしてください!
豊かなプロジェクト管理ライフが貴方を待っていますよ!!!


はい、というワケで深夜の謎テンションで記事を書き始めてみました。

Projects (Beta) 未使用の方向けに粗く概要を説明すると、GitHub Projects (Beta) は次のような特徴をもった GitHub のプロダクトです。

  • Board(いわゆるカンバン)や Table(Excel/Spread Sheet 的な表形式)で Issues, Pull Requests を管理できる
  • User/Orgs の配下に Project を作成し、複数のリポジトリの Issues, Pull Requests を管理できる
    • これまでも Projects 機能はあったが、それとは別モノとして扱われる
  • GitHub Issues 機能群の1つとして現在 Public Beta テスト中
    • Beta にサインアップすると Waiting リストに登録され、少し(数日〜数週間)待つとアクセス権が付与されます。

こちらの記事が凄く分かりやすく纏まっているので、詳細はそちらをご覧いただくとして、本稿はそんな GitHub Issues の新機能の一つである Projects (Beta) を便利に使えるようなツールを作ったよ、というお話しです。

TL; DR

次のような Workflow を .github/workflow/manage-project-on-issues.yml に置いておくと、「Project ラベルの付け外しに呼応して Projects (Beta) に登録・削除が行われる」というワークフローが仕込まれます。

name: Issue の Label 操作時に Projects (Beta) に登録・削除
on:
  issues:
    types:
      - labeled
      - unlabeled
env:
  PROJECT_OWNER: <PROJECT_OWNER>            # Project の所有者名(users, orgs のどちらでも OK)
  PROJECT_NUMBER: <PROJECT_NUMBER>          # Project 番号(確認方法は本題参照)
  TARGET_LABEL: 'Project'
  GITHUB_TOKEN: ${{ secrets.<SECRET_KEY> }} # `repo`, `read:org`, `write:org` 権限を付与した Personal Access Token を保存した Secrets のキー
jobs:
  manage_project:
    name: 「Project」ラベルの付け外しに対応して、Issue を Projects (Beta) に出し入れする
    runs-on: ubuntu-latest
    steps:
    - name: Add Issue to Project
      if: ${{ github.event.action == 'labeled' && contains(github.event.issue.labels.*.name, env.TARGET_LABEL) }}
      id: add-issue-to-project
      uses: monry/actions-add-issue-to-project@v1
      with:
        github-token: ${{ env.GITHUB_TOKEN }}
        project-owner: ${{ env.PROJECT_OWNER }}
        project-number: ${{ env.PROJECT_NUMBER }}
        issue-id: ${{ github.event.issue.node_id }}
    - name: Delete Issue from Project
      if: ${{ github.event.action == 'unlabeled' && !contains(github.event.issue.labels.*.name, env.TARGET_LABEL) }}
      id: delete-issue-from-project
      uses: monry/actions-delete-issue-from-project@v1
      with:
        github-token: ${{ env.GITHUB_TOKEN }}
        project-owner: ${{ env.PROJECT_OWNER }}
        project-number: ${{ env.PROJECT_NUMBER }}
        issue-id: ${{ github.event.issue.node_id }}

本題

GitHub Projects (Beta) には Workflow という自動化機能が公式に備わっているのですが、2021/10/23 時点ではカスタマイズ性が低く「Issue が Close されたら Card の Status を Done に変更する(Done カラムに移動する)」「Projects (Beta) に追加された時点で自動的に Status を Backlog に変更する(Backlog カラムに移動する)」といった最低限の設定しか行えません。

まぁ詳細なカスタマイズについては、設定出来そうな UI が Coming soon ってなっているので、そのうち便利になると思われます。

で、まぁそれは別に良いんですが、「Projects に Card を登録する」という処理については、自動化できなさそうだったので「できないなら自分で作ろう!」と思い立ちました。

具体的には任意のリポジトリに仕込む Workflows から利用できる汎用的な Actions を作るという方針を決め、次の4つの Actions を作りました。

それぞれの使い方などの詳細を以下に纏めます。

なお、GitHub Actions の Workflow から呼び出されるコトが前提の Action となっているため、「Actions?Workflow?なにそれ美味しいの?🤔」という方は GitHub 公式ドキュメントを読み、概要を理解しておくことを強くお薦めいたします。

Get Project Id

Projects (Beta) の Node Id を取得します。
Node Id というのは、GitHub のあらゆる要素を一意に特定するための文字列で、GraphQL を用いた API アクセスを行う際のパラメータとして利用されます。

この Action を単体で利用してもあまり意味はなく、Job の後続 Step から Project の ID を参照する用途で利用されることを想定しております。
実際、後述の Get Project Item Id や Add Issue to Project などで Project の Node Id を取得するために利用しています。

使用例

次のような Workflow を .github/workflows/get-project-id-sample.yml として保存すると、Issue 作成時にワークフローが発火しログに PN_ から始まる Project の Node Id が表示されます。

name: Example
on:
  issues:
    types:
      - opened
env:
  PROJECT_OWNER: monry
  PROJECT_NUMBER: 1
jobs:
  example:
    name: Example job
    runs-on: ubuntu-latest
    steps:
    - uses: monry/actions-get-project-id@v1
      id: get-project-id
      with:
        github-token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
        project-owner: ${{ env.PROJECT_OWNER }}
        project-number: ${{ env.PROJECT_NUMBER }}
    - name: Output result
      run: |
        echo '${{ steps.get-project-id.outputs.project-id }}'

このサンプルは on: issues: types: [ 'opened' ] となっているので、Action に渡すパラメータのうち project-ownerproject-owner: ${{ github.event.issue.repository.owner.login }} などとしても良いかもしれません。

Inputs

github-token

権限として repo, read:org を付与した Personal Access Token を設定します。
リポジトリや Organization の Secrets として設定しておいて、それを渡すと良いでしょう。

Secrets の名前は任意です。
本稿ではリポジトリか Organization の Secrets として PERSONAL_ACCESS_TOKEN という名前で登録されている想定になっています。
実際の運用では、利用範囲を絞って不要になった際に破棄しやすくすると良いかもしれません。(私は PAT_ プレフィックスを付けて管理しています。)

project-owner

Project を所有している User / Organization の名前を設定します。

project-number

Projects (Beta) には Project Number という概念があり、URL の /users/monry/projects/:number/orgs/kidsstar/projects/:number:number に該当する値がソレになります。

プロジェクト一覧にも表示されています。

Outputs

project-id

project-ownerproject-number から絞り込まれた Projects (Beta) の ID を返します。

以降の Step で参照する際には、本 Action を呼び出す際に id を設定したうえで、 ${{ steps.<id>.outputs.project-id }} とすることで値を取得できます。

    steps:
    - uses: monry/actions-get-project-id@v1
      id: get-project-id # この ID が steps. のキーになる
      with:
        github-token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
        project-owner: ${{ env.PROJECT_OWNER }}
        project-number: ${{ env.PROJECT_NUMBER }}
    - name: Output result
      run: |
        echo '${{ steps.get-project-id.outputs.project-id }}'

Get Project Item Id

Projects (Beta) に登録された Item の Node Id を取得します。
ココで言う Item は、Board(カンバン)で言う所のカードや、Table(表)で言う所の1行分の要素のコトを指します。

この Action も Get Project Id 同様単体で利用してもあまり意味はなく、Job の後続 Step から Project Item の ID を参照する用途で利用されることを想定しております。
実際、後述の Remove Issue from Project で利用しています。

使用例

次のような Workflow を .github/workflows/get-project-item-id-sample.yml として保存すると、Issue に Label を設定した際にワークフローが発火し、当該 Issue が Project に登録されている場合はログに PNI_ から始まる Project Item の Node Id が表示されます。(未登録の場合は空文字が表示されます。)

name: Example
on:
  issues:
    types:
      - labeled
env:
  PROJECT_OWNER: monry
  PROJECT_NUMBER: 1
jobs:
  example:
    name: Example job
    runs-on: ubuntu-latest
    steps:
    - uses: monry/actions-get-project-item-id@v1
      id: get-project-item-id
      with:
        github-token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
        project-owner: ${{ env.PROJECT_OWNER }}
        project-number: ${{ env.PROJECT_NUMBER }}
        issue-id: ${{ github.event.issue.node_id }}
    - name: Output result
      run: |
        echo '${{ steps.get-project-item-id.outputs.project-item-id }}'

Inputs

github-token

権限として repo, read:org を付与した Personal Access Token を設定します。
リポジトリや Organization の Secrets として設定しておいて、それを渡すと良いでしょう。(名前は何でもOK)

project-id

Item を検索する Project の Node Id が分かっている場合に設定します。

project-name, project-number の組み合わせを設定する場合、内部で Project Id を問い合わせることになるので、前述の Get Project Id を用いて取得した値を定数的に設定しておくと、Rate Limit の節約ができます。

project-owner

Project を所有している User / Organization の名前を設定します。

project-id を設定しなかった場合に必須になります。

project-number

Project Number を設定します。

project-id を設定しなかった場合に必須になります。
確認方法は Get Project Id の記述を参照してください。

issue-id

検索する Issue の Node Id を設定します。

Workflow が on: issues により発火する場合、github.event.issue.node_id に Node Id が格納されているので、そのまま渡すと良いでしょう。

issue-repository

検索する Issue のリポジトリ名を設定します。
ココで言うリポジトリ名は、monry/awesome-repos といった Owner 名を含む値を期待しています。

issue-id を設定しなかった場合に必須になります。

issue-number

検索する Issue の番号を設定します。

issue-id を設定しなかった場合に必須になります。

Outputs

project-item-id

渡された Project / Issue 関連のパラメータから絞り込まれた Project Item の ID を返します。

以降の Step で参照する際には、本 Action を呼び出す際に id を設定したうえで、 ${{ steps.<id>.outputs.project-item-id }} とすることで値を取得できます。

    steps:
    - uses: monry/actions-get-project-item-id@v1
      id: get-project-item-id # この ID が steps. のキーになる
      with:
        github-token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
        project-owner: ${{ env.PROJECT_OWNER }}
        project-number: ${{ env.PROJECT_NUMBER }}
        issue-id: ${{ github.event.issue.node_id }}
    - name: Output result
      run: |
        echo '${{ steps.get-project-item-id.outputs.project-item-id }}'

Add Issue to Project

本稿の本題です。

Issue を Project に登録します。
Project に登録済みの Issue が指定されてもエラーにはならず、Outputs の added-project-item-id には登録済みの Project Item Id が出力されます。

使用例

次のような Workflow を .github/workflows/add-issue-to-project-sample.yml として保存すると、Issue に FooBar という Label が設定された際にワークフローが発火し、当該 Issue が Project に登録されて、登録された Project Item Id がログに出力されます。

name: Example
on:
  issues:
    types:
      - labeled
env:
  PROJECT_OWNER: monry
  PROJECT_NUMBER: 1
  TARGET_LABEL: 'FooBar'
jobs:
  example:
    name: Example job
    runs-on: ubuntu-latest
    steps:
    - uses: monry/add-issue-to-project@v1
      if: ${{ contains(github.event.issue.labels.*.name, env.TARGET_LABEL) }} # ココを工夫するコトで、柔軟な設定ができる
      id: add-issue-to-project
      with:
        github-token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
        project-owner: ${{ env.PROJECT_OWNER }}
        project-number: ${{ env.PROJECT_NUMBER }}
        issue-id: ${{ github.event.issue.node_id }}
    - name: Output result
      run: |
        echo '${{ steps.add-issue-to-project.outputs.added-project-item-id }}'

Inputs

github-token

権限として repo, read:org, write:org を付与した Personal Access Token を設定します。
リポジトリや Organization の Secrets として設定しておいて、それを渡すと良いでしょう。(名前は何でもOK)
Get 系の Action とは異なり write:org も必要になるので注意。

project-id

Item を検索する Project の Node Id が分かっている場合に設定します。

project-name, project-number の組み合わせを設定する場合、内部で Project Id を問い合わせることになるので、前述の Get Project Id を用いて取得した値を定数的に設定しておくと、Rate Limit の節約ができます。

project-owner

Project を所有している User / Organization の名前を設定します。

project-id を設定しなかった場合に必須になります。

project-number

Project Number を設定します。

project-id を設定しなかった場合に必須になります。
確認方法は Get Project Id の記述を参照してください。

issue-id

検索する Issue の Node Id を設定します。

Workflow が on: issues により発火する場合、github.event.issue.node_id に Node Id が格納されているので、そのまま渡すと良いでしょう。

issue-repository

検索する Issue のリポジトリ名を設定します。
ココで言うリポジトリ名は、monry/awesome-repos といった Owner 名を含む値を期待しています。

issue-id を設定しなかった場合に必須になります。

issue-number

検索する Issue の番号を設定します。

issue-id を設定しなかった場合に必須になります。

Outputs

added-project-item-id

登録された Project Item Id を返します。

以降の Step で参照する際には、本 Action を呼び出す際に id を設定したうえで、 ${{ steps.<id>.outputs.added-project-item-id }} とすることで値を取得できます。

    steps:
    - uses: monry/add-issue-to-project@v1
      if: ${{ contains(github.event.issue.labels.*.name, env.TARGET_LABEL) }}
      id: add-issue-to-project # この ID が steps. のキーになる
      with:
        github-token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
        project-owner: ${{ env.PROJECT_OWNER }}
        project-number: ${{ env.PROJECT_NUMBER }}
        issue-id: ${{ github.event.issue.node_id }}
    - name: Output result
      run: |
        echo '${{ steps.add-issue-to-project.outputs.added-project-item-id }}'

Delete Issue from Project

こちらも本題に近いモノがあります。

Issue を Project から削除します。
Project に未登録 Issue が指定されてもエラーにはならず、Outputs の deleted-project-item-id には空文字が出力されます。

使用例

次のような Workflow を .github/workflows/delete-issue-to-project-sample.yml として保存すると、Issue から FooBar という Label が削除された際にワークフローが発火し、当該 Issue が Project から削除されて、削除された Project Item Id がログに出力されます。

name: Example
on:
  issues:
    types:
      - unlabeled
env:
  PROJECT_OWNER: monry
  PROJECT_NUMBER: 1
  TARGET_LABEL: 'FooBar'
jobs:
  example:
    name: Example job
    runs-on: ubuntu-latest
    steps:
    - uses: monry/delete-issue-to-project@v1
      if: ${{ !contains(github.event.issue.labels.*.name, env.TARGET_LABEL) }} # ココを工夫するコトで、柔軟な設定ができる
      id: delete-issue-to-project
      with:
        github-token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
        project-owner: ${{ env.PROJECT_OWNER }}
        project-number: ${{ env.PROJECT_NUMBER }}
        issue-id: ${{ github.event.issue.node_id }}
    - name: Output result
      run: |
        echo '${{ steps.delete-issue-to-project.outputs.deleted-project-item-id }}'

Inputs

github-token

権限として repo, read:org, write:org を付与した Personal Access Token を設定します。
リポジトリや Organization の Secrets として設定しておいて、それを渡すと良いでしょう。(名前は何でもOK)
Get 系の Action とは異なり write:org も必要になるので注意。

project-id

Item を検索する Project の Node Id が分かっている場合に設定します。

project-name, project-number の組み合わせを設定する場合、内部で Project Id を問い合わせることになるので、前述の Get Project Id を用いて取得した値を定数的に設定しておくと、Rate Limit の節約ができます。

project-owner

Project を所有している User / Organization の名前を設定します。

project-id を設定しなかった場合に必須になります。

project-number

Project Number を設定します。

project-id を設定しなかった場合に必須になります。
確認方法は Get Project Id の記述を参照してください。

project-item-id

削除対象の Project Item Id を設定します。

通常の Workflow でこの値を知る方法は無いため、利用頻度は低いかもしれません。
もし何らかの方法で削除対象の Project Item Id が分かっている場合には設定することで Rate Limit の節約ができます。

issue-id

検索する Issue の Node Id を設定します。

Workflow が on: issues により発火する場合、github.event.issue.node_id に Node Id が格納されているので、そのまま渡すと良いでしょう。

issue-repository

検索する Issue のリポジトリ名を設定します。
ココで言うリポジトリ名は、monry/awesome-repos といった Owner 名を含む値を期待しています。

project-item-id, issue-id を設定しなかった場合に必須になります。

issue-number

検索する Issue の番号を設定します。

project-item-id, issue-id を設定しなかった場合に必須になります。

Outputs

deleted-project-item-id

削除された Project Item Id を返します。
対象の Project Item が見付からないなどの理由で削除が行われなかった場合は空文字が返されます。

以降の Step で参照する際には、本 Action を呼び出す際に id を設定したうえで、 ${{ steps.<id>.outputs.deleted-project-item-id }} とすることで値を取得できます。

    steps:
    - uses: monry/delete-issue-to-project@v1
      if: ${{ !contains(github.event.issue.labels.*.name, env.TARGET_LABEL) }}
      id: delete-issue-to-project # この ID が steps. のキーになる
      with:
        github-token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
        project-owner: ${{ env.PROJECT_OWNER }}
        project-number: ${{ env.PROJECT_NUMBER }}
        issue-id: ${{ github.event.issue.node_id }}
    - name: Output result
      run: |
        echo '${{ steps.delete-issue-to-project.outputs.deleted-project-item-id }}'

おわりに

本業の方で Projects (Beta) を試用してみるコトになり、使っているうちに「こういうのあったら便利だなぁ」とか思ったので作ってみたんですが、「半日あればできるだろー」とか思って作り始めてみたら思いの外苦戦してしまい、トータルで12時間くらいは掛かってしまった気がします。
(なんなら、この記事も結構な大作になってしまった…😅)
GitHub Actions や GraphQL の勉強を兼ねて作っていたんですが、かなり理解が深まったのでトライしてみて良かったです。

「動きがオカシイ」とか「こういう機能が欲しい」とかあれば Issue 立ててもらったり Pull Request 送ってもらえたりすると嬉しいです。
使用した感想とかもらえると泣いて喜びます😂

Discussion