Node.jsでGitHubリポジトリのIssueとコメントを全て取得して保存するスクリプト
Markdown形式にしたものをブランチにpushしてIssueをバージョン管理したりするのに使う。
必要なものを入れて実行するとdataディレクトリに保存される。
npm install octokit gray-matter
/**
* ローカルで実行する場合
*
* - 用意したアクセストークンをGITHUB_TOKENに設定
* - リポジトリをGITHUB_REPOSITORYに設定 (例: "user/repo")
*/
import { existsSync, mkdirSync, rmdirSync, writeFileSync } from "node:fs";
import { stringify } from "gray-matter";
import { Octokit } from "octokit";
const DATA_DIR = "./data";
const ISSUES_DIR = `${DATA_DIR}/issues`;
const ISSUE_FILE = "issue.md";
const ISSUE_COMMENTS_DIR = "comments";
const issueDirPath = ({ issueNumber }: { issueNumber: number }) => `${ISSUES_DIR}/${issueNumber}` as const;
const issueFilePath = ({ issueNumber }: { issueNumber: number }) =>
`${issueDirPath({ issueNumber })}/${ISSUE_FILE}` as const;
const issueCommentsDirPath = ({ issueNumber }: { issueNumber: number }) =>
`${ISSUES_DIR}/${issueNumber}/${ISSUE_COMMENTS_DIR}` as const;
const issueCommentFilePath = ({ issueNumber, commentId }: { issueNumber: number; commentId: number }) =>
`${issueCommentsDirPath({ issueNumber })}/${commentId}.md` as const;
const octokit = new Octokit({
auth: process.env.GITHUB_TOKEN,
});
const saveIssues = async () => {
const issuesIterator = octokit.paginate.iterator(octokit.rest.issues.listForRepo, {
owner: (process.env.GITHUB_REPOSITORY ?? "").split("/")[0],
repo: (process.env.GITHUB_REPOSITORY ?? "").split("/")[1],
// 全てのissueを取得
state: "all",
per_page: 100,
});
for await (const { data: issues } of issuesIterator) {
for (const issue of issues) {
// PRはスキップ
if (issue.pull_request) continue;
console.log("Issue #%d: %s", issue.number, issue.title);
const { body, ...issueData } = issue;
mkdirSync(issueDirPath({ issueNumber: issue.number }), { recursive: true });
// yaml形式にする
const yamlContent = stringify(
// biome-ignore lint/style/noNonNullAssertion: bodyは必ず存在する
body!,
issueData,
);
// ファイルを書き込む
writeFileSync(issueFilePath({ issueNumber: issue.number }), yamlContent);
}
}
};
const saveIssueComments = async () => {
const issueCommentsIterator = octokit.paginate.iterator(octokit.rest.issues.listCommentsForRepo, {
owner: (process.env.GITHUB_REPOSITORY ?? "").split("/")[0],
repo: (process.env.GITHUB_REPOSITORY ?? "").split("/")[1],
per_page: 100,
});
for await (const { data: comments } of issueCommentsIterator) {
for (const comment of comments) {
// PRのコメントはスキップ
// html_url: 'https://github.com/user/repo/pull/1#issuecomment-123456'
// /でsplitして最後から2番目が"pull"の場合はPRのコメント
if (comment.html_url.split("/").slice(-2)[0] === "pull") continue;
console.log("Comment #%d", comment.id);
const { body, ...commentData } = comment;
const issueNumber = Number(comment.issue_url.split("/").pop());
mkdirSync(issueCommentsDirPath({ issueNumber }), { recursive: true });
// yaml形式にする
const yamlContent = stringify(
// biome-ignore lint/style/noNonNullAssertion: bodyは必ず存在する
body!,
commentData,
);
// ファイルを書き込む
writeFileSync(issueCommentFilePath({ issueNumber, commentId: comment.id }), yamlContent);
}
}
};
const main = async () => {
// 削除されたものが残らないように、前のデータを削除
if (existsSync(ISSUES_DIR)) rmdirSync(ISSUES_DIR, { recursive: true });
await saveIssues();
await saveIssueComments();
};
main();