GitHub API v3を使ってPull Requestの最終Push日時を取得する

2021.07.11

こんにちは、CX事業本部の若槻です。

最近、Pull RequestのCloseの自動化のために、「最後にCommitがPushされた日時を取得」する処理を実装する必要がありました。

そこで今回は、GitHub API v3を使ってPull Requestの最終Push日時を取得する方法を確認してみました。

やってみた

アクセストークンの発行

まだの場合は下記から発行します。スコープはreporepo:statusを指定します。

Pull Requestの最終Push日時を取得する

まずPull Requestの情報を取得します。GitHub API v3の下記のエンドポイントを使用すれば取得可能です。

GET /repos/{owner}/{repo}/pulls/{pull_number}

GITHUB_ACCESS_AOKENには先ほどのアクセストークンを指定します。octocat/hello-worldはGitHubが公開しているRepositoryです。このRepositoryの番号997のPull Requestを取得してみます。

% GITHUB_ACCESS_AOKEN=##GITHUB_ACCESS_AOKEN##
% result=$(curl \
  -H "Accept: application/vnd.github.v3+json" \
  -H "Authorization: token ${GITHUB_ACCESS_AOKEN}" \
  https://api.github.com/repos/octocat/hello-world/pulls/997)

するとPull Requestの情報がJSONで出力されます。(長いため折りたたんでいます。)

クリックで展開

result.json

{
  "url": "https://api.github.com/repos/octocat/Hello-World/pulls/997",
  "id": 685581540,
  "node_id": "MDExOlB1bGxSZXF1ZXN0Njg1NTgxNTQw",
  "html_url": "https://github.com/octocat/Hello-World/pull/997",
  "diff_url": "https://github.com/octocat/Hello-World/pull/997.diff",
  "patch_url": "https://github.com/octocat/Hello-World/pull/997.patch",
  "issue_url": "https://api.github.com/repos/octocat/Hello-World/issues/997",
  "number": 997,
  "state": "open",
  "locked": false,
  "title": "modify by logerror",
  "user": {
    "login": "logerrors",
    "id": 31824303,
    "node_id": "MDQ6VXNlcjMxODI0MzAz",
    "avatar_url": "https://avatars.githubusercontent.com/u/31824303?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/logerrors",
    "html_url": "https://github.com/logerrors",
    "followers_url": "https://api.github.com/users/logerrors/followers",
    "following_url": "https://api.github.com/users/logerrors/following{/other_user}",
    "gists_url": "https://api.github.com/users/logerrors/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/logerrors/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/logerrors/subscriptions",
    "organizations_url": "https://api.github.com/users/logerrors/orgs",
    "repos_url": "https://api.github.com/users/logerrors/repos",
    "events_url": "https://api.github.com/users/logerrors/events{/privacy}",
    "received_events_url": "https://api.github.com/users/logerrors/received_events",
    "type": "User",
    "site_admin": false
  },
  "body": "",
  "created_at": "2021-07-07T23:48:41Z",
  "updated_at": "2021-07-07T23:51:57Z",
  "closed_at": null,
  "merged_at": null,
  "merge_commit_sha": "7f951a24c1a3e83ce53230e80f243b7a5585ddb6",
  "assignee": null,
  "assignees": [

  ],
  "requested_reviewers": [

  ],
  "requested_teams": [

  ],
  "labels": [

  ],
  "milestone": null,
  "draft": false,
  "commits_url": "https://api.github.com/repos/octocat/Hello-World/pulls/997/commits",
  "review_comments_url": "https://api.github.com/repos/octocat/Hello-World/pulls/997/comments",
  "review_comment_url": "https://api.github.com/repos/octocat/Hello-World/pulls/comments{/number}",
  "comments_url": "https://api.github.com/repos/octocat/Hello-World/issues/997/comments",
  "statuses_url": "https://api.github.com/repos/octocat/Hello-World/statuses/9f81418f73fbebff5974c33bd705eaa8957f1dfc",
  "head": {
    "label": "logerrors:master",
    "ref": "master",
    "sha": "9f81418f73fbebff5974c33bd705eaa8957f1dfc",
    "user": {
      "login": "logerrors",
      "id": 31824303,
      "node_id": "MDQ6VXNlcjMxODI0MzAz",
      "avatar_url": "https://avatars.githubusercontent.com/u/31824303?v=4",
      "gravatar_id": "",
      "url": "https://api.github.com/users/logerrors",
      "html_url": "https://github.com/logerrors",
      "followers_url": "https://api.github.com/users/logerrors/followers",
      "following_url": "https://api.github.com/users/logerrors/following{/other_user}",
      "gists_url": "https://api.github.com/users/logerrors/gists{/gist_id}",
      "starred_url": "https://api.github.com/users/logerrors/starred{/owner}{/repo}",
      "subscriptions_url": "https://api.github.com/users/logerrors/subscriptions",
      "organizations_url": "https://api.github.com/users/logerrors/orgs",
      "repos_url": "https://api.github.com/users/logerrors/repos",
      "events_url": "https://api.github.com/users/logerrors/events{/privacy}",
      "received_events_url": "https://api.github.com/users/logerrors/received_events",
      "type": "User",
      "site_admin": false
    },
    "repo": {
      "id": 383948788,
      "node_id": "MDEwOlJlcG9zaXRvcnkzODM5NDg3ODg=",
      "name": "Hello-World",
      "full_name": "logerrors/Hello-World",
      "private": false,
      "owner": {
        "login": "logerrors",
        "id": 31824303,
        "node_id": "MDQ6VXNlcjMxODI0MzAz",
        "avatar_url": "https://avatars.githubusercontent.com/u/31824303?v=4",
        "gravatar_id": "",
        "url": "https://api.github.com/users/logerrors",
        "html_url": "https://github.com/logerrors",
        "followers_url": "https://api.github.com/users/logerrors/followers",
        "following_url": "https://api.github.com/users/logerrors/following{/other_user}",
        "gists_url": "https://api.github.com/users/logerrors/gists{/gist_id}",
        "starred_url": "https://api.github.com/users/logerrors/starred{/owner}{/repo}",
        "subscriptions_url": "https://api.github.com/users/logerrors/subscriptions",
        "organizations_url": "https://api.github.com/users/logerrors/orgs",
        "repos_url": "https://api.github.com/users/logerrors/repos",
        "events_url": "https://api.github.com/users/logerrors/events{/privacy}",
        "received_events_url": "https://api.github.com/users/logerrors/received_events",
        "type": "User",
        "site_admin": false
      },
      "html_url": "https://github.com/logerrors/Hello-World",
      "description": "My first repository on GitHub!",
      "fork": true,
      "url": "https://api.github.com/repos/logerrors/Hello-World",
      "forks_url": "https://api.github.com/repos/logerrors/Hello-World/forks",
      "keys_url": "https://api.github.com/repos/logerrors/Hello-World/keys{/key_id}",
      "collaborators_url": "https://api.github.com/repos/logerrors/Hello-World/collaborators{/collaborator}",
      "teams_url": "https://api.github.com/repos/logerrors/Hello-World/teams",
      "hooks_url": "https://api.github.com/repos/logerrors/Hello-World/hooks",
      "issue_events_url": "https://api.github.com/repos/logerrors/Hello-World/issues/events{/number}",
      "events_url": "https://api.github.com/repos/logerrors/Hello-World/events",
      "assignees_url": "https://api.github.com/repos/logerrors/Hello-World/assignees{/user}",
      "branches_url": "https://api.github.com/repos/logerrors/Hello-World/branches{/branch}",
      "tags_url": "https://api.github.com/repos/logerrors/Hello-World/tags",
      "blobs_url": "https://api.github.com/repos/logerrors/Hello-World/git/blobs{/sha}",
      "git_tags_url": "https://api.github.com/repos/logerrors/Hello-World/git/tags{/sha}",
      "git_refs_url": "https://api.github.com/repos/logerrors/Hello-World/git/refs{/sha}",
      "trees_url": "https://api.github.com/repos/logerrors/Hello-World/git/trees{/sha}",
      "statuses_url": "https://api.github.com/repos/logerrors/Hello-World/statuses/{sha}",
      "languages_url": "https://api.github.com/repos/logerrors/Hello-World/languages",
      "stargazers_url": "https://api.github.com/repos/logerrors/Hello-World/stargazers",
      "contributors_url": "https://api.github.com/repos/logerrors/Hello-World/contributors",
      "subscribers_url": "https://api.github.com/repos/logerrors/Hello-World/subscribers",
      "subscription_url": "https://api.github.com/repos/logerrors/Hello-World/subscription",
      "commits_url": "https://api.github.com/repos/logerrors/Hello-World/commits{/sha}",
      "git_commits_url": "https://api.github.com/repos/logerrors/Hello-World/git/commits{/sha}",
      "comments_url": "https://api.github.com/repos/logerrors/Hello-World/comments{/number}",
      "issue_comment_url": "https://api.github.com/repos/logerrors/Hello-World/issues/comments{/number}",
      "contents_url": "https://api.github.com/repos/logerrors/Hello-World/contents/{+path}",
      "compare_url": "https://api.github.com/repos/logerrors/Hello-World/compare/{base}...{head}",
      "merges_url": "https://api.github.com/repos/logerrors/Hello-World/merges",
      "archive_url": "https://api.github.com/repos/logerrors/Hello-World/{archive_format}{/ref}",
      "downloads_url": "https://api.github.com/repos/logerrors/Hello-World/downloads",
      "issues_url": "https://api.github.com/repos/logerrors/Hello-World/issues{/number}",
      "pulls_url": "https://api.github.com/repos/logerrors/Hello-World/pulls{/number}",
      "milestones_url": "https://api.github.com/repos/logerrors/Hello-World/milestones{/number}",
      "notifications_url": "https://api.github.com/repos/logerrors/Hello-World/notifications{?since,all,participating}",
      "labels_url": "https://api.github.com/repos/logerrors/Hello-World/labels{/name}",
      "releases_url": "https://api.github.com/repos/logerrors/Hello-World/releases{/id}",
      "deployments_url": "https://api.github.com/repos/logerrors/Hello-World/deployments",
      "created_at": "2021-07-07T23:32:53Z",
      "updated_at": "2021-07-07T23:41:01Z",
      "pushed_at": "2021-07-08T01:18:14Z",
      "git_url": "git://github.com/logerrors/Hello-World.git",
      "ssh_url": "git@github.com:logerrors/Hello-World.git",
      "clone_url": "https://github.com/logerrors/Hello-World.git",
      "svn_url": "https://github.com/logerrors/Hello-World",
      "homepage": "",
      "size": 1,
      "stargazers_count": 0,
      "watchers_count": 0,
      "language": null,
      "has_issues": false,
      "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": 0,
      "license": null,
      "forks": 0,
      "open_issues": 0,
      "watchers": 0,
      "default_branch": "master"
    }
  },
  "base": {
    "label": "octocat:master",
    "ref": "master",
    "sha": "7fd1a60b01f91b314f59955a4e4d4e80d8edf11d",
    "user": {
      "login": "octocat",
      "id": 583231,
      "node_id": "MDQ6VXNlcjU4MzIzMQ==",
      "avatar_url": "https://avatars.githubusercontent.com/u/583231?v=4",
      "gravatar_id": "",
      "url": "https://api.github.com/users/octocat",
      "html_url": "https://github.com/octocat",
      "followers_url": "https://api.github.com/users/octocat/followers",
      "following_url": "https://api.github.com/users/octocat/following{/other_user}",
      "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
      "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
      "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
      "organizations_url": "https://api.github.com/users/octocat/orgs",
      "repos_url": "https://api.github.com/users/octocat/repos",
      "events_url": "https://api.github.com/users/octocat/events{/privacy}",
      "received_events_url": "https://api.github.com/users/octocat/received_events",
      "type": "User",
      "site_admin": false
    },
    "repo": {
      "id": 1296269,
      "node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5",
      "name": "Hello-World",
      "full_name": "octocat/Hello-World",
      "private": false,
      "owner": {
        "login": "octocat",
        "id": 583231,
        "node_id": "MDQ6VXNlcjU4MzIzMQ==",
        "avatar_url": "https://avatars.githubusercontent.com/u/583231?v=4",
        "gravatar_id": "",
        "url": "https://api.github.com/users/octocat",
        "html_url": "https://github.com/octocat",
        "followers_url": "https://api.github.com/users/octocat/followers",
        "following_url": "https://api.github.com/users/octocat/following{/other_user}",
        "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
        "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
        "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
        "organizations_url": "https://api.github.com/users/octocat/orgs",
        "repos_url": "https://api.github.com/users/octocat/repos",
        "events_url": "https://api.github.com/users/octocat/events{/privacy}",
        "received_events_url": "https://api.github.com/users/octocat/received_events",
        "type": "User",
        "site_admin": false
      },
      "html_url": "https://github.com/octocat/Hello-World",
      "description": "My first repository on GitHub!",
      "fork": false,
      "url": "https://api.github.com/repos/octocat/Hello-World",
      "forks_url": "https://api.github.com/repos/octocat/Hello-World/forks",
      "keys_url": "https://api.github.com/repos/octocat/Hello-World/keys{/key_id}",
      "collaborators_url": "https://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}",
      "teams_url": "https://api.github.com/repos/octocat/Hello-World/teams",
      "hooks_url": "https://api.github.com/repos/octocat/Hello-World/hooks",
      "issue_events_url": "https://api.github.com/repos/octocat/Hello-World/issues/events{/number}",
      "events_url": "https://api.github.com/repos/octocat/Hello-World/events",
      "assignees_url": "https://api.github.com/repos/octocat/Hello-World/assignees{/user}",
      "branches_url": "https://api.github.com/repos/octocat/Hello-World/branches{/branch}",
      "tags_url": "https://api.github.com/repos/octocat/Hello-World/tags",
      "blobs_url": "https://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}",
      "git_tags_url": "https://api.github.com/repos/octocat/Hello-World/git/tags{/sha}",
      "git_refs_url": "https://api.github.com/repos/octocat/Hello-World/git/refs{/sha}",
      "trees_url": "https://api.github.com/repos/octocat/Hello-World/git/trees{/sha}",
      "statuses_url": "https://api.github.com/repos/octocat/Hello-World/statuses/{sha}",
      "languages_url": "https://api.github.com/repos/octocat/Hello-World/languages",
      "stargazers_url": "https://api.github.com/repos/octocat/Hello-World/stargazers",
      "contributors_url": "https://api.github.com/repos/octocat/Hello-World/contributors",
      "subscribers_url": "https://api.github.com/repos/octocat/Hello-World/subscribers",
      "subscription_url": "https://api.github.com/repos/octocat/Hello-World/subscription",
      "commits_url": "https://api.github.com/repos/octocat/Hello-World/commits{/sha}",
      "git_commits_url": "https://api.github.com/repos/octocat/Hello-World/git/commits{/sha}",
      "comments_url": "https://api.github.com/repos/octocat/Hello-World/comments{/number}",
      "issue_comment_url": "https://api.github.com/repos/octocat/Hello-World/issues/comments{/number}",
      "contents_url": "https://api.github.com/repos/octocat/Hello-World/contents/{+path}",
      "compare_url": "https://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}",
      "merges_url": "https://api.github.com/repos/octocat/Hello-World/merges",
      "archive_url": "https://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}",
      "downloads_url": "https://api.github.com/repos/octocat/Hello-World/downloads",
      "issues_url": "https://api.github.com/repos/octocat/Hello-World/issues{/number}",
      "pulls_url": "https://api.github.com/repos/octocat/Hello-World/pulls{/number}",
      "milestones_url": "https://api.github.com/repos/octocat/Hello-World/milestones{/number}",
      "notifications_url": "https://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}",
      "labels_url": "https://api.github.com/repos/octocat/Hello-World/labels{/name}",
      "releases_url": "https://api.github.com/repos/octocat/Hello-World/releases{/id}",
      "deployments_url": "https://api.github.com/repos/octocat/Hello-World/deployments",
      "created_at": "2011-01-26T19:01:12Z",
      "updated_at": "2021-07-09T02:07:41Z",
      "pushed_at": "2021-07-07T23:48:42Z",
      "git_url": "git://github.com/octocat/Hello-World.git",
      "ssh_url": "git@github.com:octocat/Hello-World.git",
      "clone_url": "https://github.com/octocat/Hello-World.git",
      "svn_url": "https://github.com/octocat/Hello-World",
      "homepage": "",
      "size": 1,
      "stargazers_count": 1670,
      "watchers_count": 1670,
      "language": null,
      "has_issues": true,
      "has_projects": true,
      "has_downloads": true,
      "has_wiki": true,
      "has_pages": false,
      "forks_count": 1577,
      "mirror_url": null,
      "archived": false,
      "disabled": false,
      "open_issues_count": 534,
      "license": null,
      "forks": 1577,
      "open_issues": 534,
      "watchers": 1670,
      "default_branch": "master"
    }
  },
  "_links": {
    "self": {
      "href": "https://api.github.com/repos/octocat/Hello-World/pulls/997"
    },
    "html": {
      "href": "https://github.com/octocat/Hello-World/pull/997"
    },
    "issue": {
      "href": "https://api.github.com/repos/octocat/Hello-World/issues/997"
    },
    "comments": {
      "href": "https://api.github.com/repos/octocat/Hello-World/issues/997/comments"
    },
    "review_comments": {
      "href": "https://api.github.com/repos/octocat/Hello-World/pulls/997/comments"
    },
    "review_comment": {
      "href": "https://api.github.com/repos/octocat/Hello-World/pulls/comments{/number}"
    },
    "commits": {
      "href": "https://api.github.com/repos/octocat/Hello-World/pulls/997/commits"
    },
    "statuses": {
      "href": "https://api.github.com/repos/octocat/Hello-World/statuses/9f81418f73fbebff5974c33bd705eaa8957f1dfc"
    }
  },
  "author_association": "NONE",
  "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": true,
  "commits": 1,
  "additions": 1,
  "deletions": 0,
  "changed_files": 1
}

このうちPull Requestの最終Push日時は.head.repo.pushed_atの値となります。

% echo ${result} | jq -r .head.repo.pushed_at
2021-07-08T01:18:14Z

これでPull Requestの最終Push日時が取得できました。

別解

以前、前述のエンドポイントのバグ?で.head.repoに同Pull Requestの.base.repoと同じ値がなぜか入ってきて、うまく最終Push日時が取得できない時がありました。

その時には別解として以下のエンドポイントを代替で利用していました。

GET /repos/{owner}/{repo}/events

このエンドポイントを使用すると指定のRepositoryで発生したEventが取得できます。EventのType一覧は下記で確認できます。

しかしこのエンドポイントはRepository内の全てのEventが取得できてしまうため、その結果から指定のPull RequestのPushEventを下記のようなループ処理を実装するなどしてフィルターする必要があります。

page=1
pushed_at=$null
while [ -z "$pushed_at" ] && [ $page -le 5 ]; do
  echo "page="$page
  pushed_at=$(curl -sS -H "$ACCEPT_HEADER" -H "$AUTH_HEADER" "${GH_API_HOST}/events?per_page=100&page=${page}" | \
    jq --arg ref $pull_branch_ref '.[] | select(.payload.ref == $ref and .type == "PushEvent")' | \
    jq -rs '.[0].created_at' \
  )
  page=$((page+1))
done

しかし最初のGET /repos/{owner}/{repo}/pulls/{pull_number}が正常に使えるようになったため、上記の冗長な方法を取らずに済んで良かったです。

以上