GitHub の Create or update file contents API の挙動を確認してみた

こんにちは。エンジニアの古矢です。

今回は、GitHub の Create or update file contents API の挙動について確認してみたという内容になります。

REST API endpoints for repository contents - GitHub Docs

困っていたこと

GitHub REST API の PUT /repos/{owner}/{repo}/contents/{path}(Octokit では repos.createOrUpdateFileContents)を使ってファイルを更新する処理を書いていたところ、疑問に思ったのが次の点です。

  • 最新のものと全く同じ content を送った場合、API はどんな挙動をするのか?
  • コミットがスキップされるのか、それとも空コミットができるのか?

公式ドキュメントには明確に書かれていないため、今回確認してみようと思いました。

実験

以下を実現するための簡易スクリプトを作成しました。

  1. あるブランチに対して repos.createOrUpdateFileContents を使って txt ファイルをコミット
  2. 全く同じ content を再度 repos.createOrUpdateFileContents に渡して実行
  3. コミット履歴を確認
import { Octokit } from "@octokit/rest";
import fs from "fs";

const pat = process.env.PAT;
const owner = process.env.OWNER;
const repo = process.env.REPO;
const branch = process.env.BRANCH;

const octokit = new Octokit({ auth: pat });

const path = "test.txt";

const fileContent = fs.readFileSync(path);
const encodedContent = fileContent.toString("base64");

const getRes = await octokit.request(
  `GET /repos/${owner}/${repo}/contents/${path}`,
  {
    owner,
    repo,
    path,
    branch,
    headers: {
      "X-GitHub-Api-Version": "2022-11-28",
    },
  }
);

const putReq = {
  owner,
  repo,
  path,
  branch,
  message: `update ${path}`,
  committer: {
    name: "test",
    email: "no-reply@github.com",
  },
  content: encodedContent,
  headers: {
    "X-GitHub-Api-Version": "2022-11-28",
  },
};

if (getRes.status === 200) {
  console.log(`${path} exists`);
  putReq.sha = getRes.data.sha;
} else {
  putReq.message = `add ${path}`;
}

await octokit.request(`PUT /repos/${owner}/${repo}/contents/${path}`, putReq);

既に同名ファイルがリモート上に存在しているときに repos.createOrUpdateFileContents を叩いて更新する場合、更新前のファイルの SHA が必要なので、ここでは GET API で sha を取得しています。今回の場合は cat text.txt | sha256sum のようなコマンドでローカルから SHA を計算しても問題ないです。

余談ですが、正しくない SHA を投げた場合 409 Conflict が返ってきました。なので sha は競合検知に使われていると推測できそうです。

結果

全く同じ content を渡した場合、空コミットとして新しいコミットが作成されました。

GitHub上で空コミットを確認

こういった空コミットを避けたい場合は、呼び出す前に対象ファイルの現在の SHA を取得して自前で content を比較し、変更があるときだけ API を叩く、という実装が必要になりそうです。

GitHub API 側としてはおそらく「呼び出されたら必ずコミットを作る」という一貫性を保つためにこのような挙動になっているのだと思われます。