notebook

都内でWEB系エンジニアやってます。

GitLabのGraphQL APIにzxを使ってクエリを投げてみる

ドキュメント読めば一通りわかるが一応やったこととか残しておく

今回はGitLabのMergeRequestの情報をAPIで取得する

一定期間内で、特定のリポジトリのMergeRequestの内容で、MergeRequestにひもづくコミットのshaと日時も欲しかったのでREST APIではなくGraphQLを使うことにした

GraphQLのExplorer

https://{GitLabのホスト}/-/graphql-explorerにアクセスすることでGraphQLのExplorerにアクセスできる

Explorerだと型の情報などサジェストしてくれるので、クエリ内容を考えるときはExplorerで試行錯誤しながら考えていくとサクサク進められる

否定の表現

MergeRequestに対して、特定のauthorUsername以外を取得したいといったケースがでてきたので調べてみた

GraphQL API style guide | GitLab

表現方法は上記ドキュメントに載っているとおりmergeRequests(not: {authUsername: "hoge"})という書き方で表現する

が、フィールドによってそもそもnotに対応していない場合があるので確認する必要がある

GraphQLのExplorerでMergeRequests(state: merged, not{})と書き、notにカーソルを置くとサジェストが出てくるProject.mergeRequests(not: MergeRequestsResolverNegatedParams)のでクリックして詳しく見てみる

こんな感じだった

labels,milestoneTitleしか指定できなさそう…

ということで断念した(取得後どこかでフィルタリングするという方針にした)

CLIから実行する

Get started with GitLab GraphQL API | GitLab

必要なheaderやTokenを把握する

GITLAB_TOKENは管理画面からパーソナルアクセストークンを取得して設定する

試しに適当なクエリをなげてみる

$ curl -v -XPOST "https://gitlab.example.com/api/graphql" --header "Authorization: Bearer $GITLAB_TOKEN" --header "Content-Type: application/json" --data-binary "@./sample.gql.json"
  • sample.gql.json
{
  "query": "query {currentUser {name}}",
  "variables": {}
}

--data-binaryには先頭に@を付けてファイルを指定することもできる

  • 結果
{
  "data": {
    "currentUser": {
      "name": "me"
    }
  }
}

クエリを投げられることがわかった

あとはクエリの中身を色々書いていくだけ

Variablesの渡し方

JSONで渡すパラメータを次のようにする

{
  "query": ".....",
  "variables": null
}

参考: GraphQL Filters Not Working in cURL POST Data - How to Use GitLab - GitLab Forum

話している内容は自分が求めていたものと違うが実際にクエリの投げ方などが書いてあったので参考になった

スクリプトを書く

ここまでの話を踏まえてスクリプトを書く

zxで書いてみる

  • request.mjs

GraphQLのクエリを別で記述しファイル内容を読み出して他のパラメータと一緒にJSON文字列として出力する(エスケープとか気にしなくてよいので)

また、100件以上必要になる場合があるので1度目のリクエストでendCursorを取得し次のページがある場合はhasNextPageをみて再帰で次のcursorを指定してリクエストを送るようにした

レスポンスのJSONはこの時点では変更を加えず、特定のディレクトリ以下にそのまま保存する、フィルタリングなり整形処理なりは別工程で行う

const args = process.argv.slice(3);
// 0 hoge/fuga
const fullPath = args[0];
const repo = fullPath.replace("/", "-");

if (!process.env.GITLAB_TOKEN) {
  throw "required environment GITLAB_TOKEN.";
}

const request = async (fullPath, cursor) => {
  const gqlQuery = fs.readFileSync('graphql/merge_requests.graphql', 'utf-8').toString();
  const body = {
    query: gqlQuery,
    variables: {
      fullPath: fullPath,
      cursor: cursor
    }
  };

  const res = await fetch("https://gitlab.example.com/api/graphql", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.GITLAB_TOKEN}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify(body)
  }).then(res => res.json());

  const hasNext = res.data.project.mergeRequests.pageInfo.hasNextPage;
  const endCursor = res.data.project.mergeRequests.pageInfo.endCursor;

  $`echo ${JSON.stringify(res)} > api/${repo}-${endCursor}.json`;

  await sleep(1000);

  if (hasNext) {
    await request(fullPath, endCursor)
  }
}

await request(fullPath, null);

※(実際のホスト名は違うホスト名)

GraphQLのクエリ

  • graphql/merge_requests.graphql

createdAfterの指定が固定値にしてしまっていたりするがほかは外から渡せるようにした

$cursorがnull許容なのは初回リクエスト時のため(初回リクエスト時は指定しない)

query ($fullPath: ID! $cursor:String) {
  project(fullPath: $fullPath) {
    mergeRequests(state: merged, createdAfter: "2021-01-01T00:00:00", after: $cursor) {
      pageInfo {
        endCursor
        hasNextPage
      }
      edges {
        cursor
        node {
          iid
          state
          createdAt
          mergedAt
          title
          webUrl
          diffStatsSummary {
            additions
            deletions
            fileCount
          }
          assignees {
            edges {
              node {
                name
                username
              }
            }
          }
          author {
            name
            username
          }
          commitCount
          commitsWithoutMergeCommits(last: 1) {
            nodes {
              sha
              author {
                id
                name
                username
              }
              authoredDate
            }
          }
        }
      }
    }
  }
}

commitsWithoutMergeCommits(last: 1)でひもづく初回のコミットを取得するようにしている

いくつかのケースで確認したがこれで問題なさそう(もしかしたら後日修正しているかも)

  • 実行
zx request.mjs hoge/fuga

api/以下にAPIのレスポンスがファイル出力された

実際の中身はこんな感じ

{
  "data": {
    "project": {
      "mergeRequests": {
        "pageInfo": {
          "endCursor": "hogehoge",
          "hasNextPage": false
        },
        "edges": [
          {
            "cursor": "hogefuga1",
            "node": {
              "iid": "2323",
              "state": "merged",
              "createdAt": "2022-06-15T09:26:48Z",
              "mergedAt": "2022-06-15T09:28:56Z",
              "title": "title1",
              "webUrl": "https://gitlab.example.com/hoge/fuga/-/merge_requests/2323",
              "diffStatsSummary": {
                "additions": 86,
                "deletions": 0,
                "fileCount": 14
              },
              "assignees": {
                "edges": []
              },
              "author": {
                "name": "ユーザー1",
                "username": "user1"
              },
              "commitCount": 18,
              "commitsWithoutMergeCommits": {
                "nodes": [
                  {
                    "sha": "sha",
                    "author": {
                      "id": "gid://gitlab/User/111",
                      "name": "ユーザー1",
                      "username": "user1"
                    },
                    "authoredDate": "2022-06-02T17:50:06+09:00"
                  }
                ]
              }
            }
          },
          {
            "cursor": "hogefuga2",
            "node": {
              "iid": "2322",
              "state": "merged",
              "createdAt": "2022-06-14T11:47:44Z",
              "mergedAt": "2022-06-22T05:32:28Z",
              "title": "title2",
              "webUrl": "https://gitlab.example.com/hoge/fuga/-/merge_requests/2322",
              "diffStatsSummary": {
                "additions": 47,
                "deletions": 30,
                "fileCount": 16
              },

ばっちり

まとめ

  • GitLabのAPIにGraphQLがあったので試した
  • Explorerで型情報を把握できるのでクエリを考えるときはExplorerが便利
  • zxを使ってちょっとしたスクリプトを書いた
    • 結構便利、READMEさらっと読んだが追加でライブラリを入れずとも、YAMLが読めたりSleepがあったりするのでzxだけでこと足りるケースがそれなりにありそう、しばらく色々な用途で使ってみようと思っている