見出し画像

GitHub GraphQL APIでPull Requestの作成

こんにちは。 Showcase Gigハブチーム所属のリョです。 最近日常開発を楽にするためいろいろ改善を試してみました。今日は主にGitHubのgraphqlを使ってpr作成のことを簡単に紹介させてください

やること

このPull Requestは単なるsemverに沿ってnpm package versionをあげるのです。リリースするたびにversion上げのPull Requestは忘れがちなので自動化したいのは理由です。

step

先にPull Requestの作成について簡単におさらいします。

  • branch Aを元にbranch Bを作成する。

  • branch Bのファイルを修正する。

  • branch Bをremoteにpushする。

  • branch Bを使ってbranch C向きのPull Requestを作成する。 手動なら上の4つのstepですが、自動化はすべてAPIでGitHubのresourceを修正するのでlocalの操作はないです。

  • remoteのbranch Aを元にremote branch Bを作成する。

  • remote branch Bのファイルを取って修正する。

  • 修正をcommitとしてremote branch Bにpushする。

  • remote branch Bを使ってremote branch C向きのPull Requestを作成する。

step-1: remoteのbranch Aを元にremote branch Bを作成

step-1-A: branch Aの情報を先に取得
graphqlはこうです。(使いやすいため関数まで)

const getHeadCommitQuery = ({
  repository,
  owner,
  branchName,
}: {
  repository: string;
  owner: string;
  branchName: string;
}): string => `{
  repository(name: "${repository}", owner: "${owner}") {
    id
    name
    ref(qualifiedName: "refs/heads/${branchName}") {
      id
      name
      target {
        ... on Commit {
          id
          history(first: 1) {
            edges {
              node {
                oid
              }
            }
          }
        }
      }
    }
  }
}`;

1つずつ説明します。

  • repositoryのidはbranch作成の時は使われます。

  • branchのprefixはrefs/heads/です。最初は自分も戸惑ってまして調べたら仕様だとわかりました。branchはprefix=heads付きのrefです。localのGitのdirectoryを見たらたしかにrefs/heads/下は全部branchです。

  •  1個目のcommitのoid(object id)を取得する。後のどbranch作成に使われます。step-1-B: branch Aの情報を元にbranch Bを作成
    graphqlはこうです。

export const getCreateBranchQuery = ({
  repositoryId,
  branchName,
  commitHash,
}: {
  repositoryId: string;
  branchName: string;
  commitHash: string;
}): string => `mutation create_ref{
  createRef(input: {repositoryId: "${repositoryId}", name: "refs/heads/${branchName}", oid: "${commitHash}"}
  ) {
    clientMutationId
    ref {
      id
      name
      prefix
    }
  }
}`;

説明:

  • repositoryIdはstep-1-Aで取得したrepositoryのid。

  • commitHashもstep-1-Aで取得したoidです。

  • branchNameはbranch Bのnameです。 これでbranch Bは作成できます。

step-2: remote branch Bのファイルを取って修正

graphqlはこれです。

export const getPackageVersionQuery = ({
  repositoryName,
  ownerName,
  branchName,
}: {
  repositoryName: string,
  ownerName: string,
  branchName: string,
}): string => `{
  repository(name: "${repositoryName}", owner: "${ownerName}") {
    object(expression: "${branchName}:package.json") {
      id
      __typename
      ... on Blob {
        id
        text
      }
    }
  }
}`;

説明:

  • branchName、repositoryNameとownerNameは言うまでもないです。

  • packageのversionを直したいので、file名はroot下のpackage.jsonです。 これでファイルの中身も取れましてファイルの修正もできます。

step-3: 修正をcommitとしてremote branch Bにpush

graphqlはこれです。

const getCreateCommitOnBranchQuery = ({
  branchName,
  repositoryNameWithOwner,
  message,
  headCommitHash,
  additions,
}: {
  branchName: string;
  repositoryNameWithOwner: string;
  message: string;
  headCommitHash: string;
  additions: Array<{ path: string, contents: string }>;
}): string => {
  let additionQuery = `[`;
  for (let i = 0; i < additions.length; i++) {
    const { path, contents } = additions[i];
    const encodedContents = Buffer.from(contents).toString("base64");
    additionQuery += `{path: "${path}", contents: "${encodedContents}"}`;
    if (i !== 0) {
      additionQuery += ",";
    }
  }
  additionQuery = additionQuery + "]";
  return `mutation create_commit_on_branch{
    createCommitOnBranch(input: {branch: {branchName: "${branchName}", repositoryNameWithOwner: "${repositoryNameWithOwner}"}, message: {headline: "${message}"}, expectedHeadOid: "${headCommitHash}", fileChanges: {additions: ${additionQuery}}}
    ) {
      clientMutationId
    }
  }`;
};

説明:

  • branchNameはbranch Bのnameです。

  • repositoryNameWithOwnerはowner/repositoryNameの形です。

  • messageはcommitのmessageです。

  • headCommitHashはstep-1-Aで取ったoidです。どのcommitの後ろにcommitを追加するか。

  • additions: 修正対象のファイルです。複数修正可能なので配列です。送信する前にbase64としてencodeする必要があります。

  • mutationなのでqueryの中にはmutationを書かないといけません。

step-4: remote branch Bを使ってremote branch C向きのPull Requestを作成

graphqlはこれです。

const getCreatePullRequestQuery = ({
  repositoryId,
  toBranchName,
  fromBranchName,
  title,
}: {
  repositoryId: string;
  toBranchName: string;
  fromBranchName: string;
  title: string;
}) => `mutation create_pull_request{
  createPullRequest(
    input: {repositoryId: "${repositoryId}", baseRefName: "${toBranchName}", headRefName: "${fromBranchName}", title: "${title}"}
  ) {
    clientMutationId
  }
}`

説明:

  • repositoryIdはstep-1-Aで取ってきたrepositoryのidです。

  • toBranchNameはbranch Cのnameです。

  • fromBranchNameはbranch Bのnameです。

  • titleはprのtitleです。

step-alpha: GitHub ocotokitの準備

GitHub octokitを使っているので初期の準備が必要です。

import { graphql } from "@octokit/graphql";
// TODO: 環境変数設定必要
const token = process.env.GITHUB_TOKEN;
const graphqlWithAuth = graphql.defaults({
  headers: {
    authorization: `token ${token}`,
  },
});
const query = getHeadCommitQuery = ({
  repository: "foo",
  owner: "bar",
  branchName: "baz",
});
graphqlWithAuth(query);

まとめ

特に複雑なものではないですが、勉強になったものをまとめると

  • branchはprefix=ref/heads付きのrefです。localの.gitフォルダで確認できます。

  • GitHubはfileをbase64にencodeしています。(webだから ?)

  • tree-sitterも触りました。tree-sitterはIDE界隈(nvimなど)でよく使われますがたまにはfileのast分析して中身を入れ替えてfile修正できます。tree-sitterはただ解析ツールなのでほかのastツールとは違ってcode -> astまではできますがast修正してからcode出力はできないです。

参考


この記事が気に入ったらサポートをしてみませんか?