9
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

設計リポジトリの設計書を実装リポジトリに同期したい

9
Last updated at Posted at 2025-12-12

導入

こんにちは、GxPの土田です。
この記事はグロースエクスパートナーズ Advent Calendar 2025の13日目です。

現状

私のプロジェクトでは、ひとつの設計リポジトリと数十のアプリ実装リポジトリがあります。
設計リポジトリにはER図(a5er)とDDLファイル・OpenAPIファイルなどがあり、詳細設計フェーズではこのリポジトリで作業を行います。

従来は、設計フェーズに設計リポジトリで作業を行い、実装フェーズではこの設計を参照しながら実装を進める、という流れで特に問題はありませんでした。
しかし近年はAIコーディング機能が発達し、それらを最大限活用するには、エディタで開くフォルダ内に設計書を格納しておく形が望ましくなりました。
例えばVS Codeでは Multi-root workspace という機能があり、複数のフォルダを同時に1ウィンドウで開くことはできます。しかしこれは各開発者の側でセットアップをする必要があるうえ、1リポジトリ1ウィンドウで開くときの利便性を捨てることになることは確かです。
GitHub Copilot coding agent に至っては他リポジトリを参照することはできないため、Issueで設計書を丸ごと渡さない限りは、同リポジトリ内に設計書を置いておくことが不可欠です。

理想

そこで、設計リポジトリのデフォルトブランチに設計書がマージ(push)されたときに、自動で実装リポジトリのデフォルトブランチにpushできればいいじゃないかという発想に至りました。※私のプロジェクトではデフォルトブランチをルールで保護していないためこれができますが、保護されていてもBypass機能を利用することでおそらく回避可能です。

要件:設計リポジトリのデフォルトブランチにpushされたとき、自動で実装リポジトリに設計書を配布する。

このうち、「デフォルトブランチにpushされたとき、自動で」は次のような設計リポジトリ側の GitHub Actions によって簡単に実現可能です。

name: Distribute spec files

on:
  push:
    branches:
      - main

jobs:
  sync-files:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout source repository
        uses: actions/checkout@v4

そして、「実装リポジトリにpush」は要するに「GitHub Actions が実行中のリポジトリとは別のリポジトリに書き込みをする」という事なのですが、これは実現するだけならPATを使うことで簡単に可能です。
しかしPATには 個人権限に依存する という問題があり、選択肢から外しました。

この個人権限に依存することの何が問題なのかというと、

  1. PAT発行者がアクセス権を持つ、全てのリポジトリにアクセスできる強力な権限を持ってしまうこと
    1. PATはリポジトリを絞って発行することができないということ
  2. PAT発行者がプロジェクトから外された場合、他のメンバーによって再発行と再設定が必要になってしまうこと

が挙げられます。

次いで考えたのが、最低限のリポジトリのみの権限を付けた共有管理ユーザを作成し、PATを発行させるという手段。
これであれば上記問題は完全に解消されますが、これまた次の問題があり外しました。

  1. このユーザをGitHubの組織に招待した分、追加でお金がかかること
  2. このユーザのID/Passwordを共有するため、プロジェクトから外れた人が引き続きこのユーザを使ってアクセスできてしまい、セキュリティリスクとなり得ること
    1. このリスクを回避するために、離任があるたびにパスワードを再設定する手間が発生すること

上記を除くの第三の選択肢として、GitHub Appsを使う方法を考案しました。

他のアプローチとしてGit Submodules(検討当初は「リポジトリ内にリポジトリを持てる機能」程度の理解)というものがありますが、これは他リポジトリの 特定コミットの状態 をリポジトリ内に持てる機能のようで、これをActionsで自動的に最新化するには、結局PATのような参照先リポジトリの権限を保証するものが必要でした。

実現

機能検証のために、無料プランの組織を作成しました。
同じく検証がしたい方は こちら から。

組織の作り方

適当な組織名を入力し
image.png

メンバー招待はSkip
image.png

Repositoriesから検証用のリポジトリを複数追加
※今回は設計リポジトリ1つと実装リポジトリ1つ
image.png

image.png

image.png

GitHub Appsを組織に作成

組織に「Permissions > Contents > Read and Write」の設定でアプリを作成し「App ID」「private key」の2つを取得します。
(この文面で理解できる方は次の折り畳みは読み飛ばして大丈夫です。)

組織のGitHub Apps作成方法

組織の「Settings」から
image.jpg

左サイドバー一番下の「Developer settings > GitHub Apps」
image.jpg

「New GitHub App」から
image.jpg

新しいアプリの名前とHomepage URLを適当に入力
image.jpg

「Webhook > Active」のチェックは不要なので外してOK(外さないと「Webhook URL」が必須入力になります)
image.jpg

一番下の方の「Permissions > Repository permissions」をクリックして展開し
image.jpg

「Contents」を「Read and Write」に設定
image.jpg

一番下はそののまま「Create GitHub App」
image.jpg

するとアプリの画面に飛ばされるので、一番上の方にある「App ID」の値をメモしておく
image.jpg

下の方に行き「Generate a private key」をクリックするとキーを含んだファイルがダウンロードされるので取っておく
image.jpg

作成したアプリを組織内の必要なリポジトリにインストール

前の手順で作成したアプリを、組織内の必要なリポジトリ(設計書のある設計リポジトリと、設計書を同期したい先の実装リポジトリ)にインストールします。

アプリをリポジトリにインストール

作成したアプリの画面で左サイドバーから「Install App」
image.jpg

対象組織の「Install」を選択
image.jpg

「Only select repositories」から必要なリポジトリを選択し「Install」
image.jpg

設計リポジトリのGitHub Actions内でアプリにトークンを発行させる

設計リポジトリ側のGitHub Actions内で、次のようなstepでアプリにトークンを発行させることができます。
前手順で作成したアプリの 「App ID」と「private key」はここで使用します。(private keyの値は ダウンロードされたファイルの中身全て です)
特にprivate keyの方はセキュアな文字列なので、リポジトリのシークレットとして保存しておきましょう。

steps:

  # ~中略~

  - name: Generate token for GitHub App
    id: generate_token
    uses: actions/create-github-app-token@v1
    with:
      app-id: ${{ secrets.DISTRIBUTE_SPEC_APP_ID }}
      private-key: ${{ secrets.DISTRIBUTE_SPEC_APP_PRIVATE_KEY }}
      owner: htcd-test-org
リポジトリ環境変数の設定方法

設計リポジトリの「Settings > Secrets and Variables > Actions」の「Repository secrets」から「New repository secrets」
image.png

DISTRIBUTE_SPEC_APP_ID の方は「App ID」の値をそのまま
image.png

DISTRIBUTE_SPEC_APP_PRIVATE_KEY は前手順でダウンロードされたprivate keyファイルの中身を全てコピペ
image.png

2つのシークレットが追加されればOK
image.png

発行させたトークンを使って実装リポジトリに commit&push

    steps:
      - name: Setup git user
        run: |
          git config --global user.name "distribute-spec[bot]"
          git config --global user.email "dummy-address@gxp.co.jp"

      - name: Checkout dev-repo-1
        uses: actions/checkout@v4
        with:
          repository: htcd-test-org/dev-repo-1
          token: ${{ steps.generate_token.outputs.token }}
          path: dev-repo-1

      - name: Copy ER diagram to dev-repo-1
        run: |
          mkdir -p dev-repo-1/.spec
          cp er/ER図.drawio dev-repo-1/.spec/ER図.drawio

      - name: Commit and push to dev-repo-1
        run: |
          cd dev-repo-1
          git add .spec/ER図.drawio
          git diff --quiet && git diff --staged --quiet || (git commit -m "Update ER diagram from spec-repo" && git push)

発行したトークンは ${{ steps.generate_token.outputs.token }} から取り出すことができます。
これを使用して実装リポジトリをcloneします。
その後は同期したいファイルをコピーしてコミットするだけです。

同期するファイルを増やしたい場合は - name: Copy ER diagram to dev-repo-1 を、
同期先リポジトリを増やしたい場合は - name: Checkout dev-repo-1 以降を増やすことで対応ができます。

yaml全文
name: Distribute spec files

on:
  push:
    branches:
      - main

jobs:
  sync-files:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout source repository
        uses: actions/checkout@v4

      - name: Generate token for GitHub App
        id: generate_token
        uses: actions/create-github-app-token@v1
        with:
          app-id: ${{ secrets.DISTRIBUTE_SPEC_APP_ID }}
          private-key: ${{ secrets.DISTRIBUTE_SPEC_APP_PRIVATE_KEY }}
          owner: htcd-test-org

      - name: Setup git user
        run: |
          git config --global user.name "distribute-spec[bot]"
          git config --global user.email "dummy-address@gxp.co.jp"

      - name: Checkout dev-repo-1
        uses: actions/checkout@v4
        with:
          repository: htcd-test-org/dev-repo-1
          token: ${{ steps.generate_token.outputs.token }}
          path: dev-repo-1

      - name: Copy ER diagram to dev-repo-1
        run: |
          mkdir -p dev-repo-1/.spec
          cp er/ER図.drawio dev-repo-1/.spec/ER図.drawio

      - name: Commit and push to dev-repo-1
        run: |
          cd dev-repo-1
          git add .spec/ER図.drawio
          git diff --quiet && git diff --staged --quiet || (git commit -m "Update ER diagram from spec-repo" && git push)

並列化と同期ファイル設定の別ファイル切り出し

ここからはオプションです。

上記のように対象の数分stepをべた書きするのでは、順列で処理が進むため非常に長い時間がかかります。
また、何をどこに同期するのかが見てわかりづらくなってしまいます。
これを解消していきます。

まず並列化ですが、GitHub Actionsの機能にmatrixというものがあります。
これはざっくり次のような形になるのですが、

name: Distribute spec files

on:
  push:
    branches:
      - main

jobs:
  sync-files:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        include:
          - repo: dev-repo-1
            files:
              - source: er/ER図.drawio
                dest: .spec/ER図.drawio
              - source: api/system1.openapi.yaml
                dest: .spec/api/system1.openapi.yaml
          - repo: dev-repo-2
            files:
              - source: er/ER図2.drawio
                dest: specification/er.drawio

    steps:
      - name: Checkout source repository
        uses: actions/checkout@v4

      - name: Generate token for GitHub App
        id: generate_token
        uses: actions/create-github-app-token@v1
        with:
          app-id: ${{ secrets.DISTRIBUTE_SPEC_APP_ID }}
          private-key: ${{ secrets.DISTRIBUTE_SPEC_APP_PRIVATE_KEY }}
          owner: htcd-test-org

      - name: Setup git user
        run: |
          git config --global user.name "distribute-spec[bot]"
          git config --global user.email "dummy-address@gxp.co.jp"

      - name: Checkout ${{ matrix.repo }}
        uses: actions/checkout@v4
        with:
          repository: htcd-test-org/${{ matrix.repo }}
          token: ${{ steps.generate_token.outputs.token }}
          path: target-repo

      - name: Copy files to ${{ matrix.repo }}
        run: |
          echo '${{ toJSON(matrix.files) }}' | jq -r '.[] | @json' | while IFS= read -r file; do
            source=$(echo "$file" | jq -r '.source')
            dest=$(echo "$file" | jq -r '.dest')
            mkdir -p "target-repo/$(dirname "$dest")"
            cp "$source" "target-repo/$dest"
            echo "Copied $source to $dest"
          done

      - name: Commit and push to ${{ matrix.repo }}
        run: |
          cd target-repo
          git add .
          git diff --quiet && git diff --staged --quiet || (git commit -m "Update spec files from spec-repo" && git push)
  1. checkout(clone)から並列化するため、1回だけcloneすればよい設計リポジトリも毎回cloneしてしまう
    1. これを回避するにはArtifact化する等が必要だが、その分費用がかかる
  2. 対象ファイルが増えるほどこのyamlが肥大化する
  3. 単純にyaml内のスクリプトは読みづらい

などの問題点があります。
そのため、並列化と並列内処理は別ファイルにスクリプトとして切り出し、同期対象ファイルの設定はjsonファイルとして切り出し、という対応を行います。

yamlの内容は、

  1. 設計リポジトリclone
  2. トークンの発行
  3. スクリプトの呼び出し

だけにします。

name: Distribute spec files

on:
  push:
    branches:
      - main

jobs:
  sync-files:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout source repository
        uses: actions/checkout@v4

      - name: Generate token for GitHub App
        id: generate_token
        uses: actions/create-github-app-token@v1
        with:
          app-id: ${{ secrets.DISTRIBUTE_SPEC_APP_ID }}
          private-key: ${{ secrets.DISTRIBUTE_SPEC_APP_PRIVATE_KEY }}
          owner: htcd-test-org

      - name: Setup Python
        uses: actions/setup-python@v4
        with:
          python-version: "3.13"

      - name: Setup git user
        run: |
          git config --global user.name "distribute-spec[bot]"
          git config --global user.email "dummy-address@gxp.co.jp"

      - name: Sync files to all repositories
        env:
          GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
        run: python .github/workflows/distribute-spec.py

同階層に distribute-spec.py を配置します。
このスクリプトは、トークンと設定ファイルをもとに同期先リポジトリへのcommit&pushを担当します。
(shで書けばPythonのセットアップが不要になるため、得意な方はshで書いた方が良いかと思います。)

import os
import subprocess
import shutil
import tempfile
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
import json

def sync_repo(repo, token, sha):
    with tempfile.TemporaryDirectory() as temp_dir:
        target = Path(temp_dir) / 'target'

        subprocess.run(f'git clone https://x-access-token:{token}@github.com/{repo['repository']}.git {target}', shell=True, check=True)

        for f in repo['files']:
            dest = target / f['dest']
            dest.parent.mkdir(parents=True, exist_ok=True)
            shutil.copy2(f['src'], dest)

        subprocess.run('git add .', cwd=target, shell=True)
        subprocess.run(f'git commit -m "Update spec (sha:{sha})" || true', cwd=target, shell=True)
        subprocess.run('git push', cwd=target, shell=True)

        print(f'{repo['repository']}')


def main():
    with open('.github/workflows/distribute-spec-config.json', encoding='utf-8') as f:
        repos = json.load(f)

    token = os.environ['GITHUB_TOKEN']
    sha = os.environ['GITHUB_SHA']

    with ThreadPoolExecutor(max_workers=5) as executor:
        list(executor.map(lambda repo: sync_repo(repo, token, sha), repos))

    print('Done')

if __name__ == '__main__':
    main()

さらに同階層に distribute-spec-config.json を配置し、同期対象ファイルの設定を記述します。
src は設計リポジトリ内のパス、dest は同期先リポジトリ内でのパスです。

[
    {
        "repository": "htcd-test-org/dev-repo-1",
        "files": [
            {
                "src": "./er/ER図.drawio",
                "dest": "./.spec/ER図.drawio"
            },
            {
                "src": "./api/system1.openapi.yaml",
                "dest": "./.spec/openapi.yaml"
            }
        ]
    },
    {
        "repository": "htcd-test-org/dev-repo-2",
        "files": [
            {
                "src": "./er/ER図.drawio",
                "dest": "./.spec/ER図.drawio"
            }
        ]
    }
]

これで並列化しつつ、各ファイルはシンプルにまとめることができます。

まとめ

要点をまとめると、

  1. 組織にGitHub Appsを作成
  2. アプリを設計リポジトリと実装リポジトリにインストール
  3. 設計リポジトリの任意のタイミングでActionsを発火
  4. アプリにトークンを発行させる
    1. この時の権限はアプリをインストールしたリポジトリのみに絞られる
  5. 発行したトークンを使って、実装リポジトリに設計書を反映する

という流れになります。

少しでもAI活用の役に立てれば幸いです!

9
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?