841-biborokuWebフロントエンドの備忘録

Node.js×GitHub CLIでGitHubリポジトリとローカル環境を一括セットアップ

プロジェクトごとに複製元となるリポジトリや特定ブランチがどれだったっけ...🤔?と探すのを減らすために、Node.jsの対話型CLIライブラリ「inquirer」と、GitHub CLIを組み合わせて、スターターテンプレートのような仕組みを作ってみました。

※この記事内のテンプレートは、GitHubのTemplate Repository(テンプレートリポジトリ)を指していません。

どのような?

  • 使いたいテンプレート(リポジトリ&ブランチ)を選択したり対話形式で設定する
  • ローカルの任意パスにクローン&履歴リセット
  • 新しいGitHubリポジトリを作成&初回コミット・プッシュ

使い方イメージ

  1. 候補は templates.json で管理
  2. node index.jsで実行
  3. スクリプト実行でテンプレート一覧が表示されるので選択
  4. クローン先のローカルパスを指定
  5. GitHubリポジトリ名を指定して新規作成
  6. 任意で説明とウェブサイトURLの登録
  7. 自動でローカル初期化・GitHubへプッシュ完了

事前に準備しておくもの

GitHub CLIをインストールしてGitHubへログイン・認証しておくとスムーズです。

GitHub CLI は、すべての作業を 1 か所で行うことができるように、pull request、issues、GitHub Actions などの GitHub 機能をターミナルに集めたコマンドライン ツールです。

GitHub CLI について - GitHub Docs

https://cli.github.com/

使用するパッケージ

パッケージ名

用途

inquirer

対話的なCLIプロンプト作成

simple-git

Node.jsからGit操作を行う

chalk

ターミナルの色付き文字表示

index.js

#!/usr/bin/env node
import inquirer from "inquirer";
import { simpleGit } from "simple-git";
import { execSync } from "child_process";
import fs from "fs";
import path from "path";
import chalk from "chalk";

// theme.jsonのパス(スクリプトと同じ場所)
const themesPath = path.resolve(process.cwd(), "templates.json");

// テンプレート読み込み
let templates;
try {
  const data = fs.readFileSync(themesPath, "utf-8");
  templates = JSON.parse(data);
} catch (e) {
  console.error(chalk.red("❌ theme.json の読み込みに失敗しました"), e.message);
  process.exit(1);
}

const run = async () => {
  const {
    selectedTemplate,
    targetPath,
    githubRepoName,
    description,
    homepage,
  } = await inquirer.prompt([
    {
      type: "list",
      name: "selectedTemplate",
      message: "テンプレートを選択してください:",
      choices: templates.map((t, i) => ({
        name: `${t.name} (${t.branch})`,
        value: i,
      })),
    },
    {
      type: "input",
      name: "targetPath",
      message: "クローン先のパスを入力してください(例:./project-a):",
      validate: (input) => (input ? true : "パスは必須です"),
    },
    {
      type: "input",
      name: "githubRepoName",
      message: "新しく作成するGitHubリポジトリ名を入力してください:",
      validate: (input) => (input ? true : "リポジトリ名は必須です"),
    },
    {
      type: "input",
      name: "description",
      message: "リポジトリの説明(任意):",
    },
    {
      type: "input",
      name: "homepage",
      message: "ウェブサイトURL(任意):",
    },
  ]);

  const { repo, branch } = templates[selectedTemplate];
  const fullPath = path.resolve(process.cwd(), targetPath);

  // クローン
  fs.mkdirSync(fullPath, { recursive: true });
  process.chdir(fullPath);
  console.log(
    chalk.green(`${repo}${branch}${targetPath} にクローン中...`)
  );
  execSync(`git clone --depth 1 --branch ${branch} ${repo} .`, {
    stdio: "inherit",
  });

  // .git 削除
  fs.rmSync(path.join(fullPath, ".git"), { recursive: true, force: true });
  console.log(chalk.yellow("🧹 .git フォルダを削除しました(履歴リセット)"));

  // 新たに git init
  const git = simpleGit();
  await git.init(["--initial-branch=main"]);
  await git.add(".");
  await git.commit("initial commit");
  console.log(chalk.green("✅ ローカルリポジトリを初期化しました"));

  // GitHubに新規リポジトリ作成(GitHub CLI使用)
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

  const pushWithRetry = async (retries = 3, delay = 5000, simulate = false) => {
    for (let i = 0; i < retries; i++) {
      try {
        if (simulate) {
          console.log(chalk.cyan(`🔁 (テスト) push 実行 ${i + 1}回目`));
          if (i < retries - 1) throw new Error("Simulated push failure");
        } else {
          execSync(`git push -u origin main`, { stdio: "inherit" });
        }
        console.log(chalk.green("✅ 初回プッシュが完了しました"));
        return;
      } catch (err) {
        console.warn(chalk.yellow(`⚠ push に失敗しました(${i + 1}回目)`));
        if (i < retries - 1) {
          console.log(
            chalk.gray(`${delay / 1000}秒待ってリトライします...`)
          );
          await sleep(delay);
        } else {
          throw new Error("❌ push に3回連続で失敗しました");
        }
      }
    }
  };

  try {
    execSync(
      `gh repo create ${githubRepoName} --private --source=. --remote=origin` +
        (description ? ` --description "${description}"` : "") +
        (homepage ? ` --homepage "${homepage}"` : ""),
      { stdio: "inherit" }
    );

    console.log(
      chalk.green(`🚀 GitHubに ${githubRepoName} を作成しました。プッシュ中...`)
    );

    await sleep(2000); // 最初の待ち時間(反映待機)

    await pushWithRetry(); // リトライ
  } catch (e) {
    console.error(
      chalk.red("❌ GitHubリポジトリの作成またはプッシュに失敗しました")
    );
  }
};

run();

templates.json

//記載のrepoは例で実在しません
[
  {
    "name": "Sample React Template",
    "repo": "https://github.com/sample-org/sample-react-template.git",
    "branch": "main"
  },
  {
    "name": "Sample Vue Template (dev branch)",
    "repo": "https://github.com/sample-org/sample-vue-template.git",
    "branch": "develop"
  },
  {
    "name": "Sample Next.js Template",
    "repo": "https://github.com/sample-org/sample-next-template.git",
    "branch": "main"
  },
  {
    "name": "Sample Node API Template",
    "repo": "https://github.com/sample-org/sample-node-api.git",
    "branch": "master"
  },
  {
    "name": "Sample Static Site Template",
    "repo": "https://github.com/sample-org/sample-static-site.git",
    "branch": "main"
  }
]

Push失敗時の自動リトライ処理

GitHub CLIで新しいリポジトリを作成した直後、GitHub側のリポジトリ作成が完了する前に push が走ってしまうためまれに git push が失敗することがあります。

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

const pushWithRetry = async (retries = 3, delay = 5000) => {
//処理
}
  • sleep() 関数でリトライ間隔を制御
  • 最大 3回 までリトライ
    失敗が続いた場合はエラーをスロー
  • chalk を使ってメッセージを視覚的にわかりやすく表示

Organization にリポジトリを作成する場合

GitHub CLI (gh) を使ってリポジトリを作成する場合、通常は「自分のアカウントの所有リポジトリ」として作成されます。しかし、会社やチームなどの Organization に作成したい 場合は、オプションで --org を指定する必要があります。

const org = "my-company-org"; // 自分のOrganization名を指定
execSync(
  `gh repo create ${githubRepoName} --private --source=. --remote=origin --org ${org}`,
  { stdio: "inherit" }
);
  • your-org-name には、自分が権限を持っているOrganization名 を指定します。
  • オーナー権限や、リポジトリ作成権限を持っていないとエラーになります。
  • Organization に所属していても、デフォルトでは gh repo create は個人アカウント側に作られるため、--org を付けないと意図しない場所に作成されてしまうことがあります。

GitHub APIを使った試み😞

最初はGitHub APIでテンプレートリポジトリを有効にしJSONを取得し、質問形式で選択肢を作ろうとしました。

デフォルトブランチに設定されている場合は取得できたのですが、それ以外のブランチから実行することができず断念しました。(他に方法があるかもしれません...)

リリースからタグを指定して取得も試みましたがうまくいきませんでした。

最終的にGitHub CLIを使う判断に

GitHub CLI を使うことで、対話的かつローカルで完結した操作が可能になり、リポジトリの作成から初回プッシュまでをスムーズに自動化できました。

特に今回のように「テンプレートからプロジェクトを素早く立ち上げたい」ケースでは、gh repo create などのコマンドで 手元の作業と GitHub の操作できる点がスムーズだと感じました。

シェア