Open5

[デバッグ中] Github Actions でワークフローを自動化 ~ PR作成⇒関連issueのステータスを変更

MK963MK963

背景

現在の案件では、以下の流れで開発しています。

  1. issueを起票。issue statusはなし。
    issueには案件のProjectsを設定します。
  2. メンバーでissueについて話し合い、issue statusをTODOにセットする。
  3. issueに着手。issue statusはIn Progressにセットする。
  4. 実装が必要であればPRを作成。PRとissueを関連付ける。issue statusをReviewingにセットする。
  5. LGTMをもらったらPRをmergeする。issue statusをresolvedにセットする。issueをcloseする。

操作漏れが発生するので、GithubのWorkflow機能でなるべく自動化しています。5については自動化できています。
PRでは、bodyにclose #344のように記載することでPRとissueを関連づけることができ、PRがmergeされたときに自動で関連するissueをcloseすることができます。

https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword

3まではフロー上、手動で問題ありません。
4についてはYAML構文を使用したワークフローを作成する必要があります。

MK963MK963

ワークフロー ファイル作成

.github/workflows/issue-status-update.ymlというワークフロー ファイルを作成。
https://docs.github.com/ja/actions/quickstart#creating-your-first-workflow

トリガー設定

ワークフロー開始トリガーはPRの open, reopen
デバッグ用に手動で行いたいのでトリガーにworkflow_dispatchを追加した。
https://docs.github.com/ja/actions/using-workflows/manually-running-a-workflow

name: Updating issue status
on:
  workflow_dispatch:
  pull_request:
    types:
      - opened
      - reopened
MK963MK963

jobの作成

実際に書いたコードを先に見せます。

jobs:
  issue-status-update:
    runs-on:
      ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Authenticate GitHub CLI
        run: gh auth login --with-token <<< "${{ secrets.GITHUB_TOKEN }}"

      - name: Get related issue number
        id: get_issue_number
        run: |
          PR_NUMBER=${{ github.event.number }}
          ISSUE_NUMBER=$(gh pr view $PR_NUMBER --json body --jq '.body' | grep -o 'close #[0-9]\+' | grep -o '[0-9]\+')
          echo "Retrieved ISSUE_NUMBER: $ISSUE_NUMBER"
          echo "ISSUE_NUMBER=$ISSUE_NUMBER" >> $GITHUB_ENV
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Get Issue Node ID
        id: get_issue_node_id
        run: |
          echo "ISSUE_NUMBER: ${{ env.ISSUE_NUMBER }}"
          echo "Owner: ${{ github.repository_owner }}"
          echo "Repo: ${{ github.event.repository.name }}"
          ISSUE_NODE_ID=$(gh api graphql -f query='
            query($issueNumber: Int!, $owner: String!, $repo: String!) {
              repository(owner: $owner, name: $repo) {
                issue(number: $issueNumber) {
                  id
                }
              }
            }
          ' -f issueNumber=${{ env.ISSUE_NUMBER }} -f owner=${{ github.repository_owner }} -f repo=${{ github.event.repository.name }} -q '.data.repository.issue.id')
          echo "ISSUE_NODE_ID=$ISSUE_NODE_ID" >> $GITHUB_ENV
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Get Project Item ID and Field ID
        id: get_project_item_id
        run: |
          PROJECT_ITEM_ID=$(gh api graphql -f query='
            query($issueId: ID!) {
              node(id: $issueId) {
                ... on Issue {
                  projectItems(first: 1) {
                    nodes {
                      id
                      project {
                        fields(first: 10) {
                          nodes {
                            id
                            name
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          ' -f issueId=${{ env.ISSUE_NODE_ID }} -q '.data.node.projectItems.nodes[0].id')

          FIELD_ID=$(gh api graphql -f query='
            query($issueId: ID!) {
              node(id: $issueId) {
                ... on Issue {
                  projectItems(first: 1) {
                    nodes {
                      project {
                        fields(first: 10) {
                          nodes {
                            id
                            name
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          ' -f issueId=${{ env.ISSUE_NODE_ID }} -q '.data.node.projectItems.nodes[0].project.fields.nodes | map(select(.name == "Status")) | .[0].id')

          echo "PROJECT_ITEM_ID=$PROJECT_ITEM_ID" >> $GITHUB_ENV
          echo "FIELD_ID=$FIELD_ID" >> $GITHUB_ENV
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Update Project Status to Reviewing
        run: |
          gh api graphql -f query='
            mutation (
              $projectItemId: ID!
              $fieldId: ID!
              $status_value: String!
            ) {
              updateProjectV2ItemFieldValue(input: {
                projectId: "<PROJECT_ID>"
                itemId: $projectItemId
                fieldId: $fieldId
                value: {
                  singleSelectOptionId: $status_value
                }
              }) {
                projectV2Item {
                  id
                }
              }
            }
          ' -f projectItemId=${{ env.PROJECT_ITEM_ID }} -f fieldId=${{ env.FIELD_ID }} -f status_value="Reviewing"
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

runs-onは、ジョブが実行される環境を指定します。ここでは、GitHubが提供するホストランナーである ubuntu-latest を使用しています。
steps以下のnameは各ステップで何を実行しているかを示しています。
大まかな流れとしては以下です。

  1. リポジトリ取得
  2. GitHub CLIを使用してGitHubに認証
  3. 関連 Issue番号取得
  4. Issue取得
  5. IssueのプロジェクトIDとフィールドID取得
  6. IssueのStatusをRevieingにセット
MK963MK963

リポジトリ取得

ワークフローを実行するためにリポジトリの内容をランナーにクローンします。
actions/checkout@v4はGithubが用意しているアクションなのでそのまま使います。
ちなみに、permissionを明示的に設定すると、ここで失敗するので注意です(自分もハマりました)。
https://github.com/actions/checkout/issues/254

MK963MK963

GitHub CLIを使用してGitHubに認証

ワークフローがリポジトリの内容にアクセスし、認証された状態でGitHub APIやその他のリポジトリ操作を実行できるようにします。