在创建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将其直接获取一次是很困难的,因此需要掌握一些技巧。
那么,让我们今年也继续努力吧!