在创建Pull Request时,将元数据与Github Projects进行协作- GitHub GraphQL API v4关于Github Actions的内容

GitHub Projects在Github上是一个标准的功能。关于GitHub的Pull Request和GitHub Projects之间的协作,在网上找不到非常专门的文章,即使查看了官方参考手册,也需要事先理解基本的GitHub GraphQL API v4才能使用。因此,我打算为那些希望快速实现协作的人编写一篇文章。

准备个人访问令牌

从这里开始制作

在Github项目中,由于与组织相关联,所以需要一个较为广泛权限的PAT,而不仅仅是repo权限。仅使用secrets.GITHUB_TOKEN权限无法执行。

需要具备以下部分标有☑的权限。

repo

repo:status

repo_deployment

public_repo

repo:invite

securiry_events

write:packages

read:packages

admin:org

read:org

project

read:project

我有时候会思考是否有必要阅读”packages”或”org”之类的东西,但是看起来它们似乎在内部被引用,目前阶段(2023/1/3)似乎是必要的。

只需要一個選項,將以下句子翻譯成中文:只需要在想要應用的每個存儲庫中以任意名稱設置 Secrets Token,然後就完成了。在這裡,我們將其設置為 WRITABLE_GITHUB_TOKEN_FOR_PROJECT_UPDATE。

[Github Action] 当创建Pull Request时将其关联到Projects部分的工作流程

name: Auto Github Projects Update

on:
  pull_request:
    types:
      - opened
      - synchronize
      - closed

env:
  GH_TOKEN: ${{ secrets.WRITABLE_GITHUB_TOKEN_FOR_PROJECT_UPDATE }}
  GH_REPO: ${{ github.repository }}
  ISSUE_ID: ${{ github.event.pull_request.node_id }}
  PR_NO: ${{ github.event.number }}
  ORGANIZATION_NAME: ${{ github.event.organization.login }}
  PROJECT_NO: <<Github ProjectsのProjectNumber>>

jobs:
  create-pr-card:
    runs-on: ubuntu-20.04
    if: ${{ (github.event.pull_request.user.login != 'dependabot[bot]') && (github.event.pull_request.user.login != 'github-actions[bot]') }}
    timeout-minutes: 1
    steps:
      - name: Github Workflow Event JSONの中身を確認
        run: cat $GITHUB_EVENT_PATH
      - name: Github ProjectsのProjectIDを取得
        run: |
          # https://docs.github.com/en/graphql/reference/queries#organization
          # https://docs.github.com/en/graphql/reference/objects#projectv2
          PROJECT_ID=$(gh api graphql -f orgname="${ORGANIZATION_NAME}" -F projectno=${PROJECT_NO} -f query='
            query get_project_id($orgname: String!, $projectno: Int!) {
              organization(login: $orgname){
                projectV2(number: $projectno) {
                  id
                  fields(first:20) {
                    nodes {
                      ... on ProjectV2Field {
                        id
                        name
                      }
                      ... on ProjectV2SingleSelectField {
                        id
                        name
                        options {
                          id
                          name
                        }
                      }
                    }
                  }
                }
              }
            }' | jq -c -r '.data.organization.projectV2.id')
          echo PROJECT_ID=${PROJECT_ID} >> $GITHUB_ENV
      - name: Pull RequestをGithub Projectsに紐付け
        run: |
          gh api graphql -f projectid="${PROJECT_ID}" -f item="${ISSUE_ID}" -f query='
          mutation add_to_project($projectid: ID!, $item: ID!) {
          	addProjectV2ItemById(input: { projectId: $projectid, contentId: $item }) {
          		item {
          			id
          		}
          	}
          }'

GitHub GraphQL API v4的规范要求,或者说GraphQL的规范要求,查询的字段必须是标量类型。
无法以对象类型进行获取,所以必须明确地写明标量类型的定义,例如在这个例子中,如果不准确地写出id和name,将会导致错误。

如果要指定Query或Mutation的参数,可以使用 -f 或 -F 进行指定。
具体参考:https://cli.github.com/manual/gh_api

在请求数据负载中,以“键=值”格式传递一个或多个-f/–raw-field值以添加静态字符串参数。要添加非字符串或基于占位符确定的值,请参见–field选项。
-F/–field标志根据值的格式进行类型转换:
字面值”true”、”false”、”null”和整数转换为相应的JSON类型;
占位符值”{owner}”、”{repo}”和”{branch}”将由当前目录的存储库中的值填充;
如果值以”@”开头,则剩余的部分将被解释为要从中读取值的文件名。使用”-“从标准输入中读取。
对于GraphQL请求,除了”query”和”operationName”之外的所有字段都被解释为GraphQL变量。
要在请求数据负载中传递嵌套参数,请在声明字段时使用”键[子键]=值”语法。要将嵌套值传递为数组,请使用多个字段以”键[]=值1″,”键[]=值2″的语法声明。要传递空数组,请使用”键[]”而不带值。
要传递预先构建的JSON或其他格式的负载,可以从通过–input指定的文件中读取请求体。使用”-“从标准输入中读取。以这种方式传递请求体时,通过字段标志指定的任何参数都将添加到终点URL的查询字符串中。

如果使用 -f 参数,那么原始字段 raw-field 就会直接传递所指定的值。如果参数的值是 String 类型或 ID 类型,则可以使用 -f。

而 -F 则会自动转换为 JSON 类型。如果参数的值是数字或布尔类型,则需要指定 -F。也许,只要全部指定 -F 就没问题了…?

在初次接触GraphQL时,可能首先会遇到困难的是工会类型。因为节点中存在在执行时确切确定类型的字段,所以需要在内联片段中指定类型。就是指的… on ProjectV2Field { } … on ProjectV2SingleSelectField { }这部分。由于在GitHub GraphQL API v4中广泛使用了工会类型,所以我认为在理解接口结构之后再尝试会更好。

[Github操作] 将Pull Request的更新和关闭时的成果值写入到字段的工作流程部分

如果需要,在计算绩效值时,请适当修正,以排除跨越日期的非工作时间。

  update-pr-card-field:
    runs-on: ubuntu-20.04
    if: ${{ (github.event.pull_request.user.login != 'dependabot[bot]') && (github.event.pull_request.user.login != 'github-actions[bot]') }}
    needs: create-pr-card
    timeout-minutes: 1
    env:
      ACTUAL_FIELD_NAME: <<実績値を格納するFieldのフィールド名>>
      NON_WORKING_HOUR_ACROSS_DATE: 15
      RES_PJ_JSON: ${{ github.event.number }}_pj_fields.json
      RES_PJ_CARD_JSON: ${{ github.event.number }}_pj_pr_nodes.json
    steps:
      - name: Github Workflow Event JSONの中身を確認
        run: cat $GITHUB_EVENT_PATH
      - name: Github Projectsの情報を取得
        run: |
          # https://docs.github.com/en/graphql/reference/queries#organization
          # https://docs.github.com/en/graphql/reference/objects#projectv2
          gh api graphql -f orgname="${ORGANIZATION_NAME}" -F projectno=${PROJECT_NO} -f query='
            query get_project_id($orgname: String!, $projectno: Int!) {
              organization(login: $orgname){
                projectV2(number: $projectno) {
                  id
                  fields(first:20) {
                    nodes {
                      ... on ProjectV2Field {
                        id
                        name
                      }
                      ... on ProjectV2SingleSelectField {
                        id
                        name
                        options {
                          id
                          name
                        }
                      }
                    }
                  }
                }
              }
            }' | jq -c '.data.organization.projectV2' > ${RES_PJ_JSON}
      - name: Github Projectsの情報を確認
        run: cat ${RES_PJ_JSON}
      - name: ProjectIDと実績値更新用のFieldIDを取得
        run: |
          PROJECT_ID=$(cat ${RES_PJ_JSON} | jq -r '.id')
          FIELD_ID=$(cat ${RES_PJ_JSON} | jq -c -r ".fields.nodes | .[] | select(.name == \"${ACTUAL_FIELD_NAME}\") | .id")

          echo PROJECT_ID=${PROJECT_ID} >> $GITHUB_ENV
          echo FIELD_ID=${FIELD_ID} >> $GITHUB_ENV
      - name: Github Projectsのカード一覧をposition降順で取得
        run: |
          # https://docs.github.com/en/graphql/reference/queries#node
          # https://docs.github.com/en/graphql/reference/queries#search
          # https://docs.github.com/en/graphql/reference/objects#projectv2
          gh api graphql -f projectid="${PROJECT_ID}" -f query='
            query get_project_nodes_desc($projectid: ID!) {
              node(id: $projectid) {
                ... on ProjectV2 {
                  items(first: 100, orderBy: { field: POSITION, direction: DESC }) {
                    nodes{
                      id
                      content {
                        ...on PullRequest {
                          number
                          createdAt
                          closedAt
                          mergedAt
                        }
                      }
                      fieldValues(first: 20) {
                        nodes{
                          ... on ProjectV2ItemFieldTextValue {
                            text
                            field {
                              ... on ProjectV2FieldCommon {
                                id
                                name
                              }
                            }
                          }
                          ... on ProjectV2ItemFieldNumberValue {
                            number
                            field {
                              ... on ProjectV2FieldCommon {
                                id
                                name
                              }
                            }
                          }
                          ... on ProjectV2ItemFieldDateValue {
                            date
                            field {
                              ... on ProjectV2FieldCommon {
                                id
                                name
                              }
                            }
                          }
                          ... on ProjectV2ItemFieldSingleSelectValue {
                            name
                            field {
                              ... on ProjectV2FieldCommon {
                                id
                                name
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }' | jq -c ".data.node.items.nodes | .[] | select(.content.number == ${PR_NO})" > ${RES_PJ_CARD_JSON}
      - name: Github Projectsのカード一覧情報を確認
        run: cat ${RES_PJ_CARD_JSON}
      - name: Project Field更新に必要な情報を取得
        run: |
          ITEM_ID=$(cat ${RES_PJ_CARD_JSON} | jq -c -r '.id')
          CREATE_UNIXTIME=$(cat ${RES_PJ_CARD_JSON} | jq -c -r '.content.createdAt | fromdate')
          CLOSE_UNIXTIME=$(cat ${RES_PJ_CARD_JSON} | jq -c -r '.content.closedAt | try fromdate catch now | floor')
          ACTUAL_VALUE=$(awk "BEGIN {printf \"%0.3f\", ($CLOSE_UNIXTIME - $CREATE_UNIXTIME) / 60 / 60}")

          # 日付を跨いだ場合、非稼働時間分の工数を引くために非稼働時間を算出
          CREATE_DATE=$(date --date @${CREATE_UNIXTIME} +"%Y-%m-%d")
          CLOSE_DATE=$(date --date @${CLOSE_UNIXTIME} +"%Y-%m-%d")
          CREATE_DATE_UNIXTIME=$(date -d "$CREATE_DATE" +%s)
          CLOSE_DATE_UNIXTIME=$(date -d "$CLOSE_DATE" +%s)
          ACRESS_DATE=$((($CLOSE_DATE_UNIXTIME - $CREATE_DATE_UNIXTIME) / 60 / 60 / 24))
          if [ $ACRESS_DATE -ne 0 ]; then
            # 経過時間から非稼働時間を引く
            ACTUAL_VALUE=$(awk "BEGIN {printf \"%0.3f\", $ACTUAL_VALUE - $ACRESS_DATE * $NON_WORKING_HOUR_ACROSS_DATE}")
          fi

          echo ITEM_ID=${ITEM_ID} >> $GITHUB_ENV
          echo FIELD_ID=${FIELD_ID} >> $GITHUB_ENV
          echo CREATE_UNIXTIME=${CREATE_UNIXTIME} >> $GITHUB_ENV
          echo CLOSE_UNIXTIME=${CLOSE_UNIXTIME} >> $GITHUB_ENV
          echo ACTUAL_VALUE=${ACTUAL_VALUE} >> $GITHUB_ENV
      - name: Github ProjectsのPull Requestに紐付いているカードの実績値を更新
        run: |
          # https://docs.github.com/en/graphql/reference/mutations#updateprojectv2itemfieldvalue
          gh api graphql -f projectid="${PROJECT_ID}" -f itemid="${ITEM_ID}" -F fieldid="${FIELD_ID}" -f query="
          mutation UpdateProjectItemActual(\$projectid: ID!, \$itemid: ID!, \$fieldid: ID!) {
          	updateProjectV2ItemFieldValue(input: {
              projectId: \$projectid, itemId: \$itemid, fieldId: \$fieldid, value: { number: ${ACTUAL_VALUE} }
            }) {
          		projectV2Item {
          			id
          		}
          	}
          }"

如果您想要节省API请求次数,可以将获取ProjectID的部分进行共通化而不必拆分工作流程。

重点是通过按照position的降序来获取Projects的Card列表。
由于search指定的first值的最大限制为100,所以一次请求只能获取100个项目。
默认情况下,由于按升序获取,当与Projects关联的Card超过100个时,无法获取PR的Card。
如果按照最新的顺序获取,大致上100个以内应该会有目标Card。在使用时请注意这一点。

最后

如果你想更详细地了解,可以阅读H.saki写的以下文章,内容很好地总结了一般的构建过程。通过阅读此文后,只需阅读参考文献,就可以掌握并熟练使用Query/Mutation双方。

 

既更新字段ID将其直接获取一次是很困难的,因此需要掌握一些技巧。
那么,让我们今年也继续努力吧!

广告
将在 10 秒后关闭
bannerAds