🚀 Rails + PostgreSQLで、ERD（SchemaSpy）を、GitHub Actions + Cloudflare Pagesを自動デプロイ！Basic 認証付きで安全に公開する方法

## 🔰 はじめに
Rails + PostgreSQL のデータベーススキーマを可視化する **SchemaSpy** を使って、自動でスキーマのドキュメントを生成し、 **Cloudflare Pages** にデプロイする方法を紹介します。

### **💡 この方法のメリット**
- GitHub Actions を活用して完全自動化
- SchemaSpy を使って ER 図を生成
- Cloudflare Pages にホスティング
- Cloudflare Functions で Basic 認証を追加し、公開範囲を制限
- GitHub Pages の制約を回避（Enterprise でなくても非公開運用が可能！）

---

## 🌍 使用するサービスの説明

### GitHub
GitHub はソースコードを管理するプラットフォームです。今回のワークフローでは、**GitHub Actions** を使ってデータベースのスキーマを取得し、Cloudflare Pages に自動デプロイします。

### GitHub Actions
GitHub Actions は **CI/CD（継続的インテグレーション / 継続的デリバリー）** を実現するための仕組みです。  
リポジトリの変更をトリガーにして、スクリプトを自動実行できます。

### GitHub Pages
GitHub Pages は GitHub が提供する静的サイトのホスティングサービスです。  
ただし、**無料プランでは「公開」しかできない（組織以下のリポジトリで、組織がエンタープライズプラン出ないと一般公開しかできないらしい）ため、非公開での利用は不可**。  
そのため、今回は GitHub Pages ではなく **Cloudflare Pages** にデプロイします。

### Cloudflare Pages + Functions
Cloudflare Pages は、GitHub リポジトリと連携してデプロイできるホスティングサービスです。  
また、Cloudflare Functions を活用すると **function機能を利用すれば Basic 認証をつけることが可能** なので、一応セキュアにスキーマ情報を管理できます。

---

## 📌 GitHub Actions の設定

以下の GitHub Actions ワークフローを `.github/workflows/schemaspy.yml` に作成します。

```.github/workflows/schemaspy.yml
name: Generate SchemaSpy Docs

on:
  push:
    branches:
      - main  # main ブランチに push された時
    paths:
      - 'db/schema.rb'  # db/schema.rb が変更された時のみ実行
  workflow_dispatch:  # 手動実行も可能

jobs:
  generate-docs:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:latest
        env:
          POSTGRES_DB: my_database
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
        ports:
          - 5432:5432
        options: >-
          --health-cmd "pg_isready -U postgres"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

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

      - name: Wait for PostgreSQL to be ready
        run: sleep 10s

      - name: Setup Ruby and Bundler
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: .ruby-version # .ruby-version がない場合には、3.4.1 とか記載してください
          bundler-cache: true

      - name: Install dependencies
        run: bundle install

      - name: Set up database schema
        env:
          RAILS_ENV: test
          DATABASE_URL: postgres://postgres:postgres@localhost:5432/my_database
        run: |
          bundle exec rails db:create
          bundle exec rails db:migrate

      - name: Install Graphviz (for ER diagrams)
        run: sudo apt-get update && sudo apt-get install -y graphviz

      - name: Install SchemaSpy and JDBC Driver
        run: |
          wget https://github.com/schemaspy/schemaspy/releases/latest/download/schemaspy-6.2.4.jar -O schemaspy.jar
          wget https://jdbc.postgresql.org/download/postgresql-42.6.0.jar -O postgresql-jdbc.jar

      - name: Generate SchemaSpy documentation
        run: |
          mkdir -p schemaspy-output
          java -jar schemaspy.jar \
            -t pgsql \
            -host localhost \
            -port 5432 \
            -db my_database \
            -u postgres \
            -p postgres \
            -s public \
            -o schemaspy-output \
            -dp postgresql-jdbc.jar

      - name: Copy _middleware.js to Cloudflare Functions
        run: |
          mkdir -p schemaspy-output/functions
          cp cloudflare/_middleware.js schemaspy-output/functions/_middleware.js

      - name: Deploy to GitHub Pages
        uses: JamesIves/github-pages-deploy-action@v4
        with:
          branch: gh-pages
          folder: schemaspy-output
          clean: true
```

### 🔐 Cloudflare Functions で Basic 認証を追加

Cloudflare Pages で、Basic 認証を追加するための `functions/_middleware.js` を作成します。

```
      - name: Copy _middleware.js to Cloudflare Functions
        run: |
          mkdir -p schemaspy-output/functions
          cp cloudflare/_middleware.js schemaspy-output/functions/_middleware.js
```

この部分で、SchemaSpyが作成した schemaspy-output ディレクトリ以下に`functions/_middleware.js`をコピーしてます。

中身は↓

```functions/_middleware.js
/**
 * Shows how to restrict access using the HTTP Basic schema.
 * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication
 * @see https://tools.ietf.org/html/rfc7617
 *
 * A user-id containing a colon (":") character is invalid, as the
 * first colon in a user-pass string separates user and password.
 */
 
const BASIC_USER = 'admin'; // そのまま使わないで！
const BASIC_PASS = 'aBcDeFgHiJ'; // そのまま使わないで！

async function errorHandling(context) {
  try {
    return await context.next()
  } catch (err) {
    return new Response(`${err.message}\n${err.stack}`, { status: 500 })
  }
}

async function handleRequest({ next, request }) {
  // The "Authorization" header is sent when authenticated.
  if (request.headers.has("Authorization")) {
    const Authorization = request.headers.get('Authorization')
    // Throws exception when authorization fails.
    const [scheme, encoded] = Authorization.split(' ')
    // The Authorization header must start with Basic, followed by a space.
    if (!encoded || scheme !== 'Basic') {
      return new Response(`The Authorization header must start with Basic`, {
        status: 400,
      })
    }
    // Decodes the base64 value and performs unicode normalization.
    // @see https://datatracker.ietf.org/doc/html/rfc7613#section-3.3.2 (and #section-4.2.2)
    // @see https://dev.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/normalize
    const buffer = Uint8Array.from(atob(encoded), (character) =>
      character.charCodeAt(0)
    )
    const decoded = new TextDecoder().decode(buffer).normalize()
    // The username & password are split by the first colon.
    //=> example: "username:password"
    const index = decoded.indexOf(':')
    // The user & password are split by the first colon and MUST NOT contain control characters.
    // @see https://tools.ietf.org/html/rfc5234#appendix-B.1 (=> "CTL = %x00-1F / %x7F")
    if (index === -1 || /[\0-\x1F\x7F]/.test(decoded)) {
      return new Response('Invalid authorization value.', { status: 400 })
    }
    const user = decoded.substring(0, index);
    const pass = decoded.substring(index + 1);
    if (BASIC_USER !== user) {
      return new Response('Invalid credentials.', { status: 401 })
    }
    if (BASIC_PASS !== pass) {
      return new Response('Invalid credentials.', { status: 401 })
    }
    // Only returns this response when no exception is thrown.
    return await next()
  }
  // Not authenticated.
  return new Response('You need to login.', {
    status: 401,
    headers: {
      // Prompts the user for credentials.
      'WWW-Authenticate': 'Basic realm="my scope", charset="UTF-8"',
    },
  })
}
export const onRequest = [errorHandling, handleRequest]
```

参考：
https://www.to-r.net/media/cloudflare-pages-basic/


### 🚀 GitHub Actions → Cloudflare Pages のデプロイの流れ
	
1.	mainブランチで、db/schema.rb が変更されたら GitHub Actions が実行される。
1.	SchemaSpy でデータベーススキーマの HTML を生成。
1.	schemaspy-output/functions/_middleware.js をコピー（Basic 認証を追加）。
1.	GitHub Pages に push → Cloudflare Pages がビルドを検知し、自動でデプロイ。


### Cloudflare Pageの設定方法

こちらのページを参考にさせていただきました。
https://qiita.com/RyoWakabayashi/items/2d40b6e8ae7ddee092db

ちなみ 対象ブランチに関しては、 action で、指定している、 `gh-pages` を指定します。

```
      - name: Deploy to GitHub Pages
        uses: JamesIves/github-pages-deploy-action@v4
        with:
          branch: gh-pages
          folder: schemaspy-output
          clean: true
```

ちなみに、仮に今後Githubエンタープライズプランになれた時に、そのまま使えるので、 `uses: JamesIves/github-pages-deploy-action@v4` を使わせていただいております。__Github Page の設定で、対象ブランチに `gh-pages` を設定しないでください。（一般公開されちゃいます）__