# 機能設計書 35-GitHubバックエンド

## 概要

GitHub REST APIおよびGraphQL APIを使用したGitリポジトリ操作バックエンドについて記述する。Open Authoring・フォーク運用に対応し、エントリの読み書き・メディア管理・エディトリアルワークフローをGitHub上で実行する。

### 本機能の処理概要

GitHubバックエンドは、Decap CMSのImplementationインターフェースを実装するクラスで、GitHubリポジトリをCMSのデータストアとして使用する。REST APIとGraphQL APIの両方をサポートし、コンテンツファイルの読み書き、メディアファイルの管理、エディトリアルワークフロー（PR/ブランチベース）を提供する。

**業務上の目的・背景**：GitHubはソースコード管理の標準的プラットフォームであり、JAMstackサイトのコンテンツ管理にGitHubリポジトリを活用するユースケースが多い。GitHubバックエンドにより、非技術者もGitの知識なしでコンテンツを編集・管理できるようになる。

**機能の利用シーン**：config.ymlでbackend.name: 'github'と設定されている場合に使用。ログインからコンテンツ編集、メディア管理、エディトリアルワークフロー、デプロイプレビューまでの全CMSライフサイクルをカバーする。

**主要な処理内容**：
1. OAuth認証（トークンベース）とOpen Authoring対応（フォーク運用）
2. エントリのフォルダ/ファイルベース読み込み（カーソルベースページネーション付き）
3. エントリの保存・削除（Git Tree/Commit API経由）
4. メディアファイルの一覧取得・アップロード・削除
5. エディトリアルワークフロー（ブランチ作成・PR管理・ステータス遷移・マージ）
6. GitHub APIステータス監視
7. デプロイプレビューURL取得

**関連システム・外部連携**：GitHub REST API（api.github.com）、GitHub GraphQL API、GitHub Status API（githubstatus.com）

**権限による制御**：リポジトリへの書き込み権限チェック（hasWriteAccess）。Open Authoring時はフォーク先リポジトリで操作。Origin Maintainerかどうかで動作が分岐。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 1 | ログイン画面 | 主機能 | GitHub OAuth認証によるログイン |
| 2 | PKCE認証画面 | 主機能 | PKCE OAuthフローでのGitHub認証 |

## 機能種別

データ連携 / CRUD操作 / 外部API連携

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| config.backend.repo | string | Yes | GitHubリポジトリ（owner/repo形式） | 未設定時はエラー |
| config.backend.branch | string | No | ブランチ名（デフォルト: 'master'） | なし |
| config.backend.api_root | string | No | API Root（デフォルト: 'https://api.github.com'） | なし |
| config.backend.open_authoring | boolean | No | Open Authoring有効化 | editorial_workflow必須 |
| config.backend.always_fork | boolean | No | 常にフォーク運用 | なし |
| config.backend.squash_merges | boolean | No | スカッシュマージ使用 | なし |
| config.backend.use_graphql | boolean | No | GraphQL API使用 | なし |
| config.backend.cms_label_prefix | string | No | CMSラベルのプレフィックス | なし |
| config.backend.preview_context | string | No | デプロイプレビューのコンテキスト | なし |
| config.backend.base_url | string | No | OAuth認証のベースURL | なし |

### 入力データソース

- config.yml（バックエンド設定）
- GitHub API（リポジトリデータ）
- ユーザー入力（エントリコンテンツ、メディアファイル）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| entries | ImplementationEntry[] | エントリデータ（パス、コンテンツ、メタデータ） |
| media | MediaFile[] | メディアファイル一覧（id、name、path、displayURL） |
| user | User | 認証ユーザー情報（name、login、avatar_url、token） |
| status | object | API・認証ステータス |

### 出力先

- Reduxストア経由でUI表示
- GitHub API（保存時）

## 処理フロー

### 処理シーケンス

```
1. 初期化（コンストラクタ）
   ├─ 設定値の読み込みとバリデーション
   ├─ Open Authoring設定の確認（editorial_workflow必須）
   └─ asyncLockの初期化
2. 認証
   ├─ トークン取得
   ├─ ブランチ未設定時はデフォルトブランチ名取得
   ├─ Open Authoring時: フォーク認証
   │   ├─ Origin Maintainer判定
   │   ├─ フォーク存在チェック → upstream merge / 新規フォーク作成
   │   └─ pollUntilForkExists
   ├─ API インスタンス生成（REST or GraphQL）
   ├─ 書き込み権限チェック
   └─ ユーザー情報返却
3. エントリ操作
   ├─ entriesByFolder: フォルダ内エントリ読み込み（ページネーション付き）
   ├─ entriesByFiles: ファイル指定エントリ読み込み
   ├─ getEntry: 単一エントリ読み込み
   ├─ persistEntry: エントリ保存（ロック付き）
   └─ deleteFiles: ファイル削除
4. メディア操作
   ├─ getMedia: メディアファイル一覧
   ├─ getMediaFile: 単一メディアファイル取得
   ├─ getMediaDisplayURL: 表示URL取得（セマフォ制御）
   ├─ persistMedia: メディアアップロード
   └─ deleteFiles: メディア削除
5. ワークフロー操作
   ├─ unpublishedEntries: 未公開エントリ一覧
   ├─ unpublishedEntry: 未公開エントリ取得
   ├─ updateUnpublishedEntryStatus: ステータス更新（ロック付き）
   ├─ deleteUnpublishedEntry: 未公開エントリ削除（ロック付き）
   └─ publishUnpublishedEntry: エントリ公開（ロック付き）
```

### フローチャート

```mermaid
flowchart TD
    A[GitHub Backend初期化] --> B{Open Authoring?}
    B -->|Yes| C[authenticateWithFork]
    B -->|No| D[authenticate]
    C --> E{Origin Maintainer?}
    E -->|Yes| F[originRepoを直接使用]
    E -->|No| G{フォーク存在?}
    G -->|Yes| H[merge-upstream]
    G -->|No| I[フォーク作成 → pollUntilForkExists]
    F --> J[API生成]
    H --> J
    I --> J
    D --> J
    J --> K{GraphQL?}
    K -->|Yes| L[GraphQLAPI]
    K -->|No| M[REST API]
    L --> N[hasWriteAccess確認]
    M --> N
    N --> O[ユーザー情報返却]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-35-01 | Open Authoring制約 | Open AuthoringにはEditorial Workflowが必須 | open_authoring=true時 |
| BR-35-02 | Origin Maintainer判定 | admin/writeパーミッションを持つユーザーはOrigin Maintainer | Open Authoring時 |
| BR-35-03 | always_fork | alwaysFork有効時はOrigin MaintainerチェックをスキップしてフォークS使用 | always_fork=true時 |
| BR-35-04 | 並行ダウンロード制限 | メディア表示URL取得は最大10並行（セマフォ制御） | getMediaDisplayURL時 |
| BR-35-05 | トランザクショナル操作 | persistEntry/updateStatus/delete/publishはasyncLockで排他制御 | 書き込み操作時 |
| BR-35-06 | ページサイズ | ファイル一覧のページサイズは20 | getCursorAndFiles時 |

### 計算ロジック

特になし

## データベース操作仕様

### 操作別データベース影響一覧

| 操作 | 対象 | 操作種別 | 概要 |
|-----|------|---------|------|
| persistEntry | GitHubリポジトリ | CREATE/UPDATE | Git Tree/Commitでファイルをリポジトリにコミット |
| deleteFiles | GitHubリポジトリ | DELETE | コミットメッセージ付きでファイル削除 |
| persistMedia | GitHubリポジトリ | CREATE | メディアファイルをリポジトリにコミット |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | 設定エラー | repoが未設定 | "The GitHub backend needs a 'repo'" エラー |
| - | 設定エラー | open_authoring有効でeditorial_workflow未設定 | エラーメッセージ表示 |
| - | 認証エラー | 書き込み権限なし | "Your GitHub user account does not have access" エラー |
| - | リポジトリエラー | リポジトリが見つからない | 詳細なエラーメッセージとガイダンス |
| - | ロック取得失敗 | asyncLockの取得に失敗 | "Failed to acquire persist entry lock" |

### リトライ仕様

- フォーク存在確認: 250msポーリングで存在するまでリトライ（pollUntilForkExists）

## トランザクション仕様

- asyncLock（runWithLock）によるCMSレベルの排他制御
- persistEntry, updateUnpublishedEntryStatus, deleteUnpublishedEntry, publishUnpublishedEntryがロック対象

## パフォーマンス要件

- MAX_CONCURRENT_DOWNLOADS = 10（セマフォによる並行ダウンロード制限）
- ページサイズ20のカーソルベースページネーション

## セキュリティ考慮事項

- OAuthトークンはAuthorization: token {token}ヘッダーで送信
- bypassWriteAccessCheckForAppTokensフラグによるアプリトークンの特別処理
- プライベートリポジトリでのメディア表示URLはgetMediaDisplayURLでトークン付きリクエスト

## 備考

- GraphQL APIはuse_graphql設定で有効化。パフォーマンス向上の目的。
- GitHub Status APIで運用状態を監視（'API Requests'と'Issues, Pull Requests, Projects'）

---

## コードリーディングガイド

本機能を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | API.ts | `packages/decap-cms-backend-github/src/API.ts` | Config interface（70-83行目）: API設定の型定義。TreeFile/TreeEntry（85-99行目）: Gitツリー操作用の型 |
| 1-2 | implementation.tsx | `packages/decap-cms-backend-github/src/implementation.tsx` | GitHubUser型（45行目）、ApiFile型（49行目）、GitHubStatusComponent型（56-60行目） |

**読解のコツ**: Implementationインターフェース（decap-cms-lib-utilから）の契約を満たすクラス。各メソッドの返値型を把握することが重要。

#### Step 2: エントリーポイント（implementation.tsx）

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | implementation.tsx | `packages/decap-cms-backend-github/src/implementation.tsx` | GitHubクラス（62-726行目）がImplementationインターフェースの実装 |

**主要処理フロー**:
1. **94-134行目**: コンストラクタ。設定値の読み込み、Open Authoring検証
2. **140-171行目**: status() - GitHub APIとauth状態の確認
3. **329-386行目**: authenticate() - トークン設定、ブランチ解決、API生成、権限確認
4. **279-327行目**: authenticateWithFork() - フォーク認証フロー
5. **423-452行目**: entriesByFolder() - フォルダ内エントリ読み込み
6. **538-545行目**: persistEntry() - runWithLock付きエントリ保存
7. **700-725行目**: updateUnpublishedEntryStatus/deleteUnpublishedEntry/publishUnpublishedEntry

#### Step 3: API層（API.ts / GraphQLAPI.ts）

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | API.ts | `packages/decap-cms-backend-github/src/API.ts` | REST APIの実装。ファイル操作、Git Tree/Commit、PR管理 |
| 3-2 | GraphQLAPI.ts | `packages/decap-cms-backend-github/src/GraphQLAPI.ts` | GraphQL APIの実装。queries.ts/mutations.ts/fragments.tsを使用 |

### プログラム呼び出し階層図

```
GitHub (implementation.tsx)
    │
    ├─ authenticate(state)
    │      ├─ [Open Authoring] authenticateWithFork()
    │      │      ├─ userIsOriginMaintainer()
    │      │      ├─ forkExists()
    │      │      └─ pollUntilForkExists()
    │      ├─ new API() / new GraphQLAPI()
    │      └─ api.hasWriteAccess()
    │
    ├─ entriesByFolder(folder, extension, depth)
    │      ├─ api.listFiles(folder, { repoURL, depth })
    │      ├─ getCursorAndFiles(filtered, 1)
    │      └─ entriesByFolder(listFiles, readFile, ...)
    │
    ├─ persistEntry(entry, options)
    │      └─ runWithLock(() => api.persistFiles(...))
    │
    ├─ getMedia(mediaFolder)
    │      └─ api.listFiles(mediaFolder)
    │
    └─ unpublishedEntries()
           └─ api.listUnpublishedBranches()
```

### データフロー図

```
[入力]                    [処理]                         [出力]

config.yml            ─▶ GitHub constructor           ─▶ GitHub instance
  backend:                                                ├─ api (API/GraphQLAPI)
    name: github                                          ├─ branch
    repo: owner/repo                                      └─ token

OAuth Token           ─▶ authenticate()              ─▶ User { name, login, token }
                          ├─ API instance生成
                          └─ hasWriteAccess()

folder, extension     ─▶ entriesByFolder()           ─▶ entries[] + cursor
                          ├─ api.listFiles()
                          └─ api.readFile()

entry, options        ─▶ persistEntry()              ─▶ GitHub commit
                          └─ api.persistFiles()
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| implementation.tsx | `packages/decap-cms-backend-github/src/implementation.tsx` | ソース | GitHubバックエンドのImplementation実装 |
| API.ts | `packages/decap-cms-backend-github/src/API.ts` | ソース | GitHub REST API操作 |
| GraphQLAPI.ts | `packages/decap-cms-backend-github/src/GraphQLAPI.ts` | ソース | GitHub GraphQL API操作 |
| queries.ts | `packages/decap-cms-backend-github/src/queries.ts` | ソース | GraphQLクエリ定義 |
| mutations.ts | `packages/decap-cms-backend-github/src/mutations.ts` | ソース | GraphQLミューテーション定義 |
| fragments.ts | `packages/decap-cms-backend-github/src/fragments.ts` | ソース | GraphQLフラグメント定義 |
| AuthenticationPage.js | `packages/decap-cms-backend-github/src/AuthenticationPage.js` | ソース | GitHub認証画面コンポーネント |
| index.ts | `packages/decap-cms-backend-github/src/index.ts` | ソース | パッケージエントリーポイント |
