この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
こんにちは、CX事業本部 IoT事業部の若槻です。
最近、Pull Requestでの開発の自動化のために、「Pull Requestの変更をデプロイ(≠マージ)をしたらLinked Issueに必要な情報を書き込む」という処理を実装する必要がありました。(あまり無いパターンかも知れませんが。)
そこで今回は、GitHub API v3で指定のPull RequestのLinked Issueを取得する方法を確認してみました。
Linked Issueとは?
Pull RequestのBodyにclose #{Issue番号}
と記載すると、そのIssueはPull Requestの「Linked Issue」となります。このPull RequestをCloseすると、Linked Issueも合わせてCloseされるため、Issueで機能開発やバグフィックスを管理する場合に便利です。
Linkされた側のIssueではLink元の「Linked Puull Request」が確認できます。
やってみた
アクセストークンの発行
まだの場合は下記から発行します。
スコープはrepo
を指定します。(repo:status
だけだと後述のAPI操作で権限不足となります。)
Pull Requestを取得する
指定のPull Requestの情報は、GitHub API v3の下記のエンドポイントを使用すれば取得可能です。
GET /repos/{owner}/{repo}/pulls/{pull_number}
取得してみます。
% GITHUB_ACCESS_AOKEN=##GITHUB_ACCESS_AOKEN##
% OWNER=cm-rwakatsuki
% REPO=test
% pull_number=22
% result=$(curl \
-H "Accept: application/vnd.github.v3+json" \
-H "Authorization: token ${GITHUB_ACCESS_AOKEN}" \
"https://api.github.com/repos/${OWNER}/${REPO}/pulls/${pull_number}")
取得結果のJSONは下記のようになります。
$ echo ${result} > get_pull_result.json
get_pull_result.json
{
"url": "https://api.github.com/repos/cm-rwakatsuki/test/pulls/22",
"id": 723439756,
"node_id": "MDExOlB1bGxSZXF1ZXN0NzIzNDM5NzU2",
"html_url": "https://github.com/cm-rwakatsuki/test/pull/22",
"diff_url": "https://github.com/cm-rwakatsuki/test/pull/22.diff",
"patch_url": "https://github.com/cm-rwakatsuki/test/pull/22.patch",
"issue_url": "https://api.github.com/repos/cm-rwakatsuki/test/issues/22",
"number": 22,
"state": "open",
"locked": false,
"title": "Develop",
"user": {
"login": "cm-rwakatsuki",
"id": 57384023,
"node_id": "MDQ6VXNlcjU3Mzg0MDIz",
"avatar_url": "https://avatars.githubusercontent.com/u/57384023?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/cm-rwakatsuki",
"html_url": "https://github.com/cm-rwakatsuki",
"followers_url": "https://api.github.com/users/cm-rwakatsuki/followers",
"following_url": "https://api.github.com/users/cm-rwakatsuki/following{/other_user}",
"gists_url": "https://api.github.com/users/cm-rwakatsuki/gists{/gist_id}",
"starred_url": "https://api.github.com/users/cm-rwakatsuki/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/cm-rwakatsuki/subscriptions",
"organizations_url": "https://api.github.com/users/cm-rwakatsuki/orgs",
"repos_url": "https://api.github.com/users/cm-rwakatsuki/repos",
"events_url": "https://api.github.com/users/cm-rwakatsuki/events{/privacy}",
"received_events_url": "https://api.github.com/users/cm-rwakatsuki/received_events",
"type": "User",
"site_admin": false
},
"body": "close #23",
"created_at": "2021-08-31T14:37:26Z",
"updated_at": "2021-08-31T15:01:54Z",
"closed_at": null,
"merged_at": null,
"merge_commit_sha": "6401b9ab1edc6e700343b26ab1132bf7f1234670",
"assignee": null,
"assignees": [
],
"requested_reviewers": [
],
"requested_teams": [
],
"labels": [
],
"milestone": null,
"draft": false,
"commits_url": "https://api.github.com/repos/cm-rwakatsuki/test/pulls/22/commits",
"review_comments_url": "https://api.github.com/repos/cm-rwakatsuki/test/pulls/22/comments",
"review_comment_url": "https://api.github.com/repos/cm-rwakatsuki/test/pulls/comments{/number}",
"comments_url": "https://api.github.com/repos/cm-rwakatsuki/test/issues/22/comments",
"statuses_url": "https://api.github.com/repos/cm-rwakatsuki/test/statuses/5e34d6c373c794e5142909259659b7fc03f4bb30",
"head": {
"label": "cm-rwakatsuki:develop",
"ref": "develop",
"sha": "5e34d6c373c794e5142909259659b7fc03f4bb30",
"user": {
"login": "cm-rwakatsuki",
"id": 57384023,
"node_id": "MDQ6VXNlcjU3Mzg0MDIz",
"avatar_url": "https://avatars.githubusercontent.com/u/57384023?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/cm-rwakatsuki",
"html_url": "https://github.com/cm-rwakatsuki",
"followers_url": "https://api.github.com/users/cm-rwakatsuki/followers",
"following_url": "https://api.github.com/users/cm-rwakatsuki/following{/other_user}",
"gists_url": "https://api.github.com/users/cm-rwakatsuki/gists{/gist_id}",
"starred_url": "https://api.github.com/users/cm-rwakatsuki/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/cm-rwakatsuki/subscriptions",
"organizations_url": "https://api.github.com/users/cm-rwakatsuki/orgs",
"repos_url": "https://api.github.com/users/cm-rwakatsuki/repos",
"events_url": "https://api.github.com/users/cm-rwakatsuki/events{/privacy}",
"received_events_url": "https://api.github.com/users/cm-rwakatsuki/received_events",
"type": "User",
"site_admin": false
},
"repo": {
"id": 302627041,
"node_id": "MDEwOlJlcG9zaXRvcnkzMDI2MjcwNDE=",
"name": "test",
"full_name": "cm-rwakatsuki/test",
"private": true,
"owner": {
"login": "cm-rwakatsuki",
"id": 57384023,
"node_id": "MDQ6VXNlcjU3Mzg0MDIz",
"avatar_url": "https://avatars.githubusercontent.com/u/57384023?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/cm-rwakatsuki",
"html_url": "https://github.com/cm-rwakatsuki",
"followers_url": "https://api.github.com/users/cm-rwakatsuki/followers",
"following_url": "https://api.github.com/users/cm-rwakatsuki/following{/other_user}",
"gists_url": "https://api.github.com/users/cm-rwakatsuki/gists{/gist_id}",
"starred_url": "https://api.github.com/users/cm-rwakatsuki/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/cm-rwakatsuki/subscriptions",
"organizations_url": "https://api.github.com/users/cm-rwakatsuki/orgs",
"repos_url": "https://api.github.com/users/cm-rwakatsuki/repos",
"events_url": "https://api.github.com/users/cm-rwakatsuki/events{/privacy}",
"received_events_url": "https://api.github.com/users/cm-rwakatsuki/received_events",
"type": "User",
"site_admin": false
},
"html_url": "https://github.com/cm-rwakatsuki/test",
"description": null,
"fork": false,
"url": "https://api.github.com/repos/cm-rwakatsuki/test",
"forks_url": "https://api.github.com/repos/cm-rwakatsuki/test/forks",
"keys_url": "https://api.github.com/repos/cm-rwakatsuki/test/keys{/key_id}",
"collaborators_url": "https://api.github.com/repos/cm-rwakatsuki/test/collaborators{/collaborator}",
"teams_url": "https://api.github.com/repos/cm-rwakatsuki/test/teams",
"hooks_url": "https://api.github.com/repos/cm-rwakatsuki/test/hooks",
"issue_events_url": "https://api.github.com/repos/cm-rwakatsuki/test/issues/events{/number}",
"events_url": "https://api.github.com/repos/cm-rwakatsuki/test/events",
"assignees_url": "https://api.github.com/repos/cm-rwakatsuki/test/assignees{/user}",
"branches_url": "https://api.github.com/repos/cm-rwakatsuki/test/branches{/branch}",
"tags_url": "https://api.github.com/repos/cm-rwakatsuki/test/tags",
"blobs_url": "https://api.github.com/repos/cm-rwakatsuki/test/git/blobs{/sha}",
"git_tags_url": "https://api.github.com/repos/cm-rwakatsuki/test/git/tags{/sha}",
"git_refs_url": "https://api.github.com/repos/cm-rwakatsuki/test/git/refs{/sha}",
"trees_url": "https://api.github.com/repos/cm-rwakatsuki/test/git/trees{/sha}",
"statuses_url": "https://api.github.com/repos/cm-rwakatsuki/test/statuses/{sha}",
"languages_url": "https://api.github.com/repos/cm-rwakatsuki/test/languages",
"stargazers_url": "https://api.github.com/repos/cm-rwakatsuki/test/stargazers",
"contributors_url": "https://api.github.com/repos/cm-rwakatsuki/test/contributors",
"subscribers_url": "https://api.github.com/repos/cm-rwakatsuki/test/subscribers",
"subscription_url": "https://api.github.com/repos/cm-rwakatsuki/test/subscription",
"commits_url": "https://api.github.com/repos/cm-rwakatsuki/test/commits{/sha}",
"git_commits_url": "https://api.github.com/repos/cm-rwakatsuki/test/git/commits{/sha}",
"comments_url": "https://api.github.com/repos/cm-rwakatsuki/test/comments{/number}",
"issue_comment_url": "https://api.github.com/repos/cm-rwakatsuki/test/issues/comments{/number}",
"contents_url": "https://api.github.com/repos/cm-rwakatsuki/test/contents/{+path}",
"compare_url": "https://api.github.com/repos/cm-rwakatsuki/test/compare/{base}...{head}",
"merges_url": "https://api.github.com/repos/cm-rwakatsuki/test/merges",
"archive_url": "https://api.github.com/repos/cm-rwakatsuki/test/{archive_format}{/ref}",
"downloads_url": "https://api.github.com/repos/cm-rwakatsuki/test/downloads",
"issues_url": "https://api.github.com/repos/cm-rwakatsuki/test/issues{/number}",
"pulls_url": "https://api.github.com/repos/cm-rwakatsuki/test/pulls{/number}",
"milestones_url": "https://api.github.com/repos/cm-rwakatsuki/test/milestones{/number}",
"notifications_url": "https://api.github.com/repos/cm-rwakatsuki/test/notifications{?since,all,participating}",
"labels_url": "https://api.github.com/repos/cm-rwakatsuki/test/labels{/name}",
"releases_url": "https://api.github.com/repos/cm-rwakatsuki/test/releases{/id}",
"deployments_url": "https://api.github.com/repos/cm-rwakatsuki/test/deployments",
"created_at": "2020-10-09T11:54:38Z",
"updated_at": "2021-07-14T16:52:42Z",
"pushed_at": "2021-08-31T14:37:26Z",
"git_url": "git://github.com/cm-rwakatsuki/test.git",
"ssh_url": "git@github.com:cm-rwakatsuki/test.git",
"clone_url": "https://github.com/cm-rwakatsuki/test.git",
"svn_url": "https://github.com/cm-rwakatsuki/test",
"homepage": null,
"size": 61,
"stargazers_count": 0,
"watchers_count": 0,
"language": "Shell",
"has_issues": true,
"has_projects": true,
"has_downloads": true,
"has_wiki": true,
"has_pages": false,
"forks_count": 0,
"mirror_url": null,
"archived": false,
"disabled": false,
"open_issues_count": 8,
"license": null,
"forks": 0,
"open_issues": 8,
"watchers": 0,
"default_branch": "main"
}
},
"base": {
"label": "cm-rwakatsuki:main",
"ref": "main",
"sha": "50adcafd9bd8b662af5ee34c0456663e34cd940d",
"user": {
"login": "cm-rwakatsuki",
"id": 57384023,
"node_id": "MDQ6VXNlcjU3Mzg0MDIz",
"avatar_url": "https://avatars.githubusercontent.com/u/57384023?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/cm-rwakatsuki",
"html_url": "https://github.com/cm-rwakatsuki",
"followers_url": "https://api.github.com/users/cm-rwakatsuki/followers",
"following_url": "https://api.github.com/users/cm-rwakatsuki/following{/other_user}",
"gists_url": "https://api.github.com/users/cm-rwakatsuki/gists{/gist_id}",
"starred_url": "https://api.github.com/users/cm-rwakatsuki/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/cm-rwakatsuki/subscriptions",
"organizations_url": "https://api.github.com/users/cm-rwakatsuki/orgs",
"repos_url": "https://api.github.com/users/cm-rwakatsuki/repos",
"events_url": "https://api.github.com/users/cm-rwakatsuki/events{/privacy}",
"received_events_url": "https://api.github.com/users/cm-rwakatsuki/received_events",
"type": "User",
"site_admin": false
},
"repo": {
"id": 302627041,
"node_id": "MDEwOlJlcG9zaXRvcnkzMDI2MjcwNDE=",
"name": "test",
"full_name": "cm-rwakatsuki/test",
"private": true,
"owner": {
"login": "cm-rwakatsuki",
"id": 57384023,
"node_id": "MDQ6VXNlcjU3Mzg0MDIz",
"avatar_url": "https://avatars.githubusercontent.com/u/57384023?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/cm-rwakatsuki",
"html_url": "https://github.com/cm-rwakatsuki",
"followers_url": "https://api.github.com/users/cm-rwakatsuki/followers",
"following_url": "https://api.github.com/users/cm-rwakatsuki/following{/other_user}",
"gists_url": "https://api.github.com/users/cm-rwakatsuki/gists{/gist_id}",
"starred_url": "https://api.github.com/users/cm-rwakatsuki/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/cm-rwakatsuki/subscriptions",
"organizations_url": "https://api.github.com/users/cm-rwakatsuki/orgs",
"repos_url": "https://api.github.com/users/cm-rwakatsuki/repos",
"events_url": "https://api.github.com/users/cm-rwakatsuki/events{/privacy}",
"received_events_url": "https://api.github.com/users/cm-rwakatsuki/received_events",
"type": "User",
"site_admin": false
},
"html_url": "https://github.com/cm-rwakatsuki/test",
"description": null,
"fork": false,
"url": "https://api.github.com/repos/cm-rwakatsuki/test",
"forks_url": "https://api.github.com/repos/cm-rwakatsuki/test/forks",
"keys_url": "https://api.github.com/repos/cm-rwakatsuki/test/keys{/key_id}",
"collaborators_url": "https://api.github.com/repos/cm-rwakatsuki/test/collaborators{/collaborator}",
"teams_url": "https://api.github.com/repos/cm-rwakatsuki/test/teams",
"hooks_url": "https://api.github.com/repos/cm-rwakatsuki/test/hooks",
"issue_events_url": "https://api.github.com/repos/cm-rwakatsuki/test/issues/events{/number}",
"events_url": "https://api.github.com/repos/cm-rwakatsuki/test/events",
"assignees_url": "https://api.github.com/repos/cm-rwakatsuki/test/assignees{/user}",
"branches_url": "https://api.github.com/repos/cm-rwakatsuki/test/branches{/branch}",
"tags_url": "https://api.github.com/repos/cm-rwakatsuki/test/tags",
"blobs_url": "https://api.github.com/repos/cm-rwakatsuki/test/git/blobs{/sha}",
"git_tags_url": "https://api.github.com/repos/cm-rwakatsuki/test/git/tags{/sha}",
"git_refs_url": "https://api.github.com/repos/cm-rwakatsuki/test/git/refs{/sha}",
"trees_url": "https://api.github.com/repos/cm-rwakatsuki/test/git/trees{/sha}",
"statuses_url": "https://api.github.com/repos/cm-rwakatsuki/test/statuses/{sha}",
"languages_url": "https://api.github.com/repos/cm-rwakatsuki/test/languages",
"stargazers_url": "https://api.github.com/repos/cm-rwakatsuki/test/stargazers",
"contributors_url": "https://api.github.com/repos/cm-rwakatsuki/test/contributors",
"subscribers_url": "https://api.github.com/repos/cm-rwakatsuki/test/subscribers",
"subscription_url": "https://api.github.com/repos/cm-rwakatsuki/test/subscription",
"commits_url": "https://api.github.com/repos/cm-rwakatsuki/test/commits{/sha}",
"git_commits_url": "https://api.github.com/repos/cm-rwakatsuki/test/git/commits{/sha}",
"comments_url": "https://api.github.com/repos/cm-rwakatsuki/test/comments{/number}",
"issue_comment_url": "https://api.github.com/repos/cm-rwakatsuki/test/issues/comments{/number}",
"contents_url": "https://api.github.com/repos/cm-rwakatsuki/test/contents/{+path}",
"compare_url": "https://api.github.com/repos/cm-rwakatsuki/test/compare/{base}...{head}",
"merges_url": "https://api.github.com/repos/cm-rwakatsuki/test/merges",
"archive_url": "https://api.github.com/repos/cm-rwakatsuki/test/{archive_format}{/ref}",
"downloads_url": "https://api.github.com/repos/cm-rwakatsuki/test/downloads",
"issues_url": "https://api.github.com/repos/cm-rwakatsuki/test/issues{/number}",
"pulls_url": "https://api.github.com/repos/cm-rwakatsuki/test/pulls{/number}",
"milestones_url": "https://api.github.com/repos/cm-rwakatsuki/test/milestones{/number}",
"notifications_url": "https://api.github.com/repos/cm-rwakatsuki/test/notifications{?since,all,participating}",
"labels_url": "https://api.github.com/repos/cm-rwakatsuki/test/labels{/name}",
"releases_url": "https://api.github.com/repos/cm-rwakatsuki/test/releases{/id}",
"deployments_url": "https://api.github.com/repos/cm-rwakatsuki/test/deployments",
"created_at": "2020-10-09T11:54:38Z",
"updated_at": "2021-07-14T16:52:42Z",
"pushed_at": "2021-08-31T14:37:26Z",
"git_url": "git://github.com/cm-rwakatsuki/test.git",
"ssh_url": "git@github.com:cm-rwakatsuki/test.git",
"clone_url": "https://github.com/cm-rwakatsuki/test.git",
"svn_url": "https://github.com/cm-rwakatsuki/test",
"homepage": null,
"size": 61,
"stargazers_count": 0,
"watchers_count": 0,
"language": "Shell",
"has_issues": true,
"has_projects": true,
"has_downloads": true,
"has_wiki": true,
"has_pages": false,
"forks_count": 0,
"mirror_url": null,
"archived": false,
"disabled": false,
"open_issues_count": 8,
"license": null,
"forks": 0,
"open_issues": 8,
"watchers": 0,
"default_branch": "main"
}
},
"_links": {
"self": {
"href": "https://api.github.com/repos/cm-rwakatsuki/test/pulls/22"
},
"html": {
"href": "https://github.com/cm-rwakatsuki/test/pull/22"
},
"issue": {
"href": "https://api.github.com/repos/cm-rwakatsuki/test/issues/22"
},
"comments": {
"href": "https://api.github.com/repos/cm-rwakatsuki/test/issues/22/comments"
},
"review_comments": {
"href": "https://api.github.com/repos/cm-rwakatsuki/test/pulls/22/comments"
},
"review_comment": {
"href": "https://api.github.com/repos/cm-rwakatsuki/test/pulls/comments{/number}"
},
"commits": {
"href": "https://api.github.com/repos/cm-rwakatsuki/test/pulls/22/commits"
},
"statuses": {
"href": "https://api.github.com/repos/cm-rwakatsuki/test/statuses/5e34d6c373c794e5142909259659b7fc03f4bb30"
}
},
"author_association": "OWNER",
"auto_merge": null,
"active_lock_reason": null,
"merged": false,
"mergeable": true,
"rebaseable": true,
"mergeable_state": "clean",
"merged_by": null,
"comments": 0,
"review_comments": 0,
"maintainer_can_modify": false,
"commits": 7,
"additions": 0,
"deletions": 0,
"changed_files": 1
}
指定のPull Requestの情報が取得できました。
Pull RequestのLinked Issueを取得
あとは前述のPull Requestの取得結果からLinked Issueの情報を取得すればOKだと思いきやここで問題発生です。Pull Requestの取得結果の中にLinked Issueについてのプロパティがなぜか見当たりません。どうやらAPIで取得したPull RequestのデータにはLinked Issueの情報が含まれないようです。
これに関してはGitHubのCommunityでも取り沙汰されていますが、現在もなお実装に至っていないようです。
仕方がないので.body
のclose #{pull number}
の文字列からgrepを使用して取得します。
$ echo ${result} | jq .body -r | grep "close #[0-9]\+" | grep -o "[0-9]\+"
23
取得できました。
おわりに
GitHub API v3で指定のPull RequestのLinked Issueを取得してみました。
Linked Issueの情報はGUI上では表示されているのに、APIで取得した結果には含まれないというのは納得がいきませんが、スマートで無いながらも取得方法が見つけられて良かったです。
参考
以上