GitHub Actionsでブランチ単位でRedocをGitHub Pagesにホストする

published
2025-02-02

初めに

django-ninjaやFastAPIなど、OpenAPI定義を自動で生成してくれる便利なライブラリを使っているとき、OpenAPIの定義をGithub Pagesでブランチごと共有できたら便利だな、と思い調べてやってみたの会。

準備

プロジェクト構成は以前のdemoのall-authがないものに、django-ninjaの設定を加えたものとなります。

以前のはこちら

django-ninjaの設定は以下になります。

# demo/api.py
from ninja import Router

router = Router()

@router.get("/")
def test(request):
    return "test"
# config/api.py
from demo.api import router
from ninja import NinjaAPI

api = NinjaAPI()
api.add_router("", router)
# config/urls.py
from .api import api

urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/", api.urls)
]

また、ドキュメントには記載がない(と思うのですが)のですが、django-ninjaにはOpenAPIの定義をexportするコマンドがあるためそちらを利用します。

コマンド利用のため、INSTALLED_APPSninjaを追加します。

INSTALLED_APPS = [
    ...
    'ninja',
]

コマンドでexportされるか確認します。

$ (.env) > python manage.py export_openapi_schema --api config.urls.api 
{"openapi": "3.1.0", "info": {"title": "NinjaAPI", "version": "1.0.0", "description": ""}, "paths": {"/api/": {"get": {"operationId": "demo_api_test", "summary": "Test", "parameters": [], "responses": {"200": {"description": "OK"}}}}}, "components": {"schemas": {}}, "servers": []}

よさそう。

Github Actions設定

Github Actionsを用いてGithub Pagesをデプロイする方法は、以下公式を真似て定義していきます。

今回はdjango-ninjaでOpenAPIの定義を出力するpython設定と、出力された定義からRedocのHTMLに変換するnodeの設定が必要になる為、setupを以下を参考に定義します。

また、ブランチ名にある/等は適当に置換してurl-safeな形にします。

完成したymlファイルは以下となります。

on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      SAFE_REF_NAME: ${{ steps.safe-url.outputs.SAFE_REF_NAME }}
    steps:
      - name: Convert Branch Name To Safe Url
        id: safe-url
        run: |
          SAFE_REF_NAME=$(echo "${{ github.ref_name }}" | tr '/' '-' | tr -d '#')
          echo "SAFE_REF_NAME=${SAFE_REF_NAME}" >> $GITHUB_OUTPUT
      - name: Checkout Repo
        uses: actions/checkout@v4
      - name: Configure GitHub Pages
        uses: actions/configure-pages@v5
      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'
      - name: Install dependencies
        run: pip install -r requirements.txt
      - name: Generate OpenAPI
        run: python manage.py export_openapi_schema --api config.urls.api > open-api.json
      - name: Use Node.js 20
        uses: actions/setup-node@v4
        with:
          node-version: '20'
      - name: Parse To HTML
        run: |
          npm install -g redoc-cli
          redoc-cli bundle open-api.json -o .output/${{ steps.safe-url.outputs.SAFE_REF_NAME }}/index.html
        continue-on-error: true
      - name: Upload static files as artifact
        id: deployment
        uses: actions/upload-pages-artifact@v3
        with:
          path: .output
  deploy:
    # https://docs.github.com/ja/pages/getting-started-with-github-pages/using-custom-workflows-with-github-pages
    environment:
      name: github-pages
      url: ${{steps.deployment.outputs.page_url}}${{ needs.build.outputs.SAFE_REF_NAME }}
    permissions:
      pages: write
      id-token: write
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

今回はBranchベースではなくGitHub Actionsを用いてPagesをデプロイするので、Settings>Pages>Build and deploymentから設定をGitHub Actionsに変更します。
ci-githubactions

また、mainブランチ以外もデプロイできるようにDeployment branches and tagsNo restrictionに変更します。

django-ninja-openapi-ci-tags

確認

mainを適当にプッシュし、CIを走らせた後https://takap-sandbox.github.io/django-ninja-openapi-ci/main/にアクセスします。
ci-main

次に、別ブランチ(feature/test)を切り、Redocが別物だとわかるようにdjango-ninjaの値を変更した後、プッシュします。

# demo/api.py
from ninja import Router

router = Router()

@router.get("/")
def test(request):
    return "test"

@router.get("/change")
def change(request):
    return "change"

https://takap-sandbox.github.io/django-ninja-openapi-ci/feature-test/にアクセスします。

ci-test-buranch

一件これでうまくいっているように見えますが、再度main側のRedocを見ようとすると404エラーとなります。

これは、actions/upload-pages-artifact@v3が前回の成果物を上書きしてしまい結果、最新のRedocだけとなるため、404となっています。
(actions/upload-pages-artifact@v3はtar形式でアップロードする為、差分更新的なオプションもなさそうでした。)

一度Pagesの成果物をダウンロードできれば良いのですが、そのようなワークフローやオプション等もなさそうでしたので、何か良い方法はないかなぁと以下模索してみました。

ダメ: cacheで誤魔化す

actions/cacheを用いて誤魔化そうと思いましたが結果としてダメでした。
これはブランチ間で異なるキャッシュが生成される為、今回ケースのような場合にはマッチしませんでした。
(まぁそもそもそんな用途でcache使わないでね...はそれはそう)

また、もし上記がクリアできたとしても、制約として7日間以上アクセスがないキャッシュは削除されるようなので、運用しようとなるとスケジュールで適宜再生成する等のワークフローが別途必要になるのであまりよくはないですね。

一応上記cacheを用いて頑張っていた時のymlおいておきます。

成功: download-artifactで取得

Pagesの成果物は行えませんが、普通に成果物をupload-artifactでアップロードし、次回そのartifactをdownloadすれば、上記を満たせるのではと思いました。

こちらは、結果として実現できたのですがdownload-artifact異なるワークフロー間のartifactの取得する場合、対象のワークフローrun_idの指定が必要となり、そのrun_idの取得が少し大変でした。

結果としては以下のようなymlとなりました。

on: [push]

env:
  API: 'https://api.github.com/repos/takap-sandbox/django-ninja-openapi-ci/actions/workflows/test.yml/runs?status=success'

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      SAFE_REF_NAME: ${{ steps.safe-url.outputs.SAFE_REF_NAME }}
    steps:
      - name: Convert Branch Name To Safe Url
        id: safe-url
        run: |
          SAFE_REF_NAME=$(echo "${{ github.ref_name }}" | tr '/' '-' | tr -d '#')
          echo "SAFE_REF_NAME=${SAFE_REF_NAME}" >> $GITHUB_OUTPUT
      - name: Checkout Repo
        uses: actions/checkout@v4
      - name: Fetch Prev Run Id
        run: |
          RUN_ID=$(curl -L \
            -H "Accept: application/vnd.github+json" \
            -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
            -H "X-GitHub-Api-Version: 2022-11-28" \
            ${{env.API}} | jq -r '.workflow_runs[0] | select(.name == ".github/workflows/test.yml") | .id')
          echo "RUN_ID=$RUN_ID" >> $GITHUB_ENV
      - name: Download Prev Artifact
        continue-on-error: true
        uses: actions/download-artifact@v4
        with:
          name: output-artifact
          path: .output
          github-token: ${{ secrets.GITHUB_TOKEN }}
          run-id: ${{ env.RUN_ID }}
      - name: Configure GitHub Pages
        uses: actions/configure-pages@v5
      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'
      - name: Install dependencies
        run: pip install -r requirements.txt
      - name: Generate OpenAPI
        run: python manage.py export_openapi_schema --api config.urls.api > open-api.json
      - name: Use Node.js 20
        uses: actions/setup-node@v4
        with:
          node-version: '20'
      - name: Parse To HTML
        run: |
          npm install -g redoc-cli
          redoc-cli bundle open-api.json -o .output/${{ steps.safe-url.outputs.SAFE_REF_NAME }}/index.html
        continue-on-error: true
      - name: Upload Artifact
        uses: actions/upload-artifact@v4
        with:
          name: output-artifact
          path: .output
          compression-level: 0
          include-hidden-files: true
      - name: Upload static files as artifact
        id: deployment
        uses: actions/upload-pages-artifact@v3
        with:
          path: .output
  deploy:
    # https://docs.github.com/ja/pages/getting-started-with-github-pages/using-custom-workflows-with-github-pages
    environment:
      name: github-pages
      url: ${{steps.deployment.outputs.page_url}}${{ needs.build.outputs.SAFE_REF_NAME }}
    permissions:
      pages: write
      id-token: write
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

異なるワークフロー間のartifact取得は以下を参考に行いました。

また、successをクエリパラメータに付与することで、成功したworkflowのみを対象にしています。

再度これをもとに各ブランチをプッシュすると、output-artifactをもとにactions内でmergeした.outputでPagesをデプロイしてくれるため、複数ブランチに対応したRedocを表示することが出来ました。

まとめ

ただ単純にgithub pagesにデプロイするだけなのであればすごく快適なのですが、こういったケースを行おうとすると途端に大変になるなと思いました...

またもしブランチごとに行うのだとしても、deploy from a branchで行うほうがまだ快適に行えそうかなと思いました まる

今回のリポジトリはこちらに置いておきました。