banner
innei

innei

写代码是因为爱,写到世界充满爱!
github
telegram
twitter

クロスリポジトリの全自動ビルドプロジェクトとサーバーへのデプロイ

最近,Vercel が再度価格を調整し、Hobby プランではますます不十分になったため、Vercel の使用をやめ、プライベートサーバーに移行して Next.js プロジェクトをデプロイすることにしました。

プライベートサーバーでのデプロイ体験は非常に不便です。第一に、Vercel のような完全自動デプロイがなく、迅速なロールバックもできません。第二に、Next.js プロジェクトのビルドには非常に大きなメモリと CPU リソースが必要で、一般的な軽量サーバーではビルド中にクラッシュするか、ダウンしてしまうことがあります。

目標#

  • GitHub を利用して、ビルド時の環境変数に影響されない汎用的な成果物を構築する。(後者については、一次構建多处部署 - Next.js Runtime Env という記事で詳しく知ることができます)
  • ビルド成果物をリモートサーバーにプッシュする方法
  • 異なるソースコードリポジトリでビルドのワークフローを実行する方法(この要件は、クローズドソースリポジトリに対して GitHub CI の時間やその他の制限があるためです;もう一つは、このようなワークフロー構成リポジトリをオープンソースにし、ソースコードリポジトリをクローズドソースにできることです)
  • ロールバックを実現する方法(便利ではないかもしれませんが、使用可能です)

プロセス#

上記の目標に基づいて、私たちが行うべきことを考え出すことができます。おおよそこのようなビルドプロセスです。

  1. ワークフローレポジトリではなく、ソースコードリポジトリからコードをチェックアウトすることが重要です。
  2. 通常のコードビルド
  3. バージョンを区別し、サーバーにプッシュする
  4. ビルドを完了する

ソースコードリポジトリにコードの変更があった場合、ワークフローレポジトリのパイプラインを再実行する必要があります。

image

2 つのリポジトリ#

上記のプロセスが明確になったので、CI 用に専用のリポジトリを作成する必要があります。これが上記で述べたワークフローレポジトリです。

次に、ワークフロー構成を作成します。

name: Build and Deploy

on:
  push:
    branches:
      - main

jobs:
  build:
    name: Build artifact
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [20.x]
    steps:
      - uses: actions/checkout@v4
        with:
          repository: innei-dev/shiroi # ここをあなたのプライベートソースコードリポジトリに変更
          token: ${{ secrets.GH_PAT }} # ここにはプライベートリポジトリにアクセスできるトークンが必要
          fetch-depth: 0
          lfs: true

      - name: Checkout LFS objects
        run: git lfs checkout
      - uses: pnpm/action-setup@v2
        with:
          version: ${{ env.PNPM_VERSION }}
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'pnpm'
      - name: Install dependencies
        run: pnpm install
      - uses: actions/cache@v4
        with:
          path: |
            ~/.npm
            ${{ github.workspace }}/.next/cache
          key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
          restore-keys: |
            ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-
      - name: Build project
        run: |
          sh ./ci-release-build.sh # ここはあなたのビルドスクリプト

上記のコメントの部分に注意が必要です。

ビルドとデプロイ#

ビルドとジョブ間での成果物共有#

次に、デプロイの作業構成を書きます。

ジョブ間での成果物共有には、Artifact を使用する必要があります。ビルドプロセスの中で、最終的な成果物を Artifact としてアップロードし、次のジョブで Artifact をダウンロードして使用します。

jobs:
  build:
    # ...
    - uses: actions/upload-artifact@v4
      with:
        name: dist # アップロード名
        path: assets/release.zip # ソースファイルパス
        retention-days: 7

  deploy:
    name: Deploy artifact
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Download artifact
        uses: actions/download-artifact@v4
        with:
          name: dist # ダウンロードするアップロードファイル

Important

この方法を使用すると、ビルド成果物が漏洩する可能性があります。リポジトリがオープンソースである場合、アップロードされた成果物は誰でも(GitHub にログインしている人)ダウンロードできます。

GitHub にサインインしていて、リポジトリに対する読み取りアクセス権を持っている人は、ワークフローのアーティファクトをダウンロードできます。

上記の方法は安全ではないため、ここでは CI キャッシュを使用して同じ機能を実現します。

jobs:
  build:
    # ...
    - name: Cache Build Artifacts
      id: cache-primes
      uses: actions/cache/save@v4
      with:
        path: assets
        key: ${{ github.run_number }}-release # ワークフロー番号をキーとして使用

  deploy:
    name: Deploy artifact
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Restore cached Build Artifacts
        id: cache-primes-restore
        uses: actions/cache/restore@v4
        with: # 成果物を復元
          path: |
            assets
          key: ${{ github.run_number }}-release

SSH を使用して成果物をリモートサーバーに転送#

上記で成果物のビルドが完了したので、次にサーバーへのデプロイプロセスを書きます。

jobs:
  deploy:
    name: Deploy artifact
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Restore cached Build Artifacts
        id: cache-primes-restore
        uses: actions/cache/restore@v4
        with:
          path: |
            assets
          key: ${{ github.run_number }}-release
      - name: Move assets to root
        run: mv assets/release.zip release.zip

      - name: copy file via ssh password
        uses: appleboy/[email protected]
        with:
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USER }}
          password: ${{ secrets.PASSWORD }}
          key: ${{ secrets.KEY }}
          port: ${{ secrets.PORT }}
          source: 'release.zip'
          target: '/tmp/shiro'

      - name: Exec deploy script with SSH
        uses: appleboy/ssh-action@master

        with:
          command_timeout: 5m
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USER }}
          password: ${{ secrets.PASSWORD }}
          key: ${{ secrets.KEY }}
          port: ${{ secrets.PORT }}
          script_stop: true
          script: |
            set -e
            source $HOME/.bashrc
            workdir=$HOME/shiro/${{ github.run_number }}
            mkdir -p $workdir
            mv /tmp/shiro/release.zip $workdir/release.zip
            rm -r /tmp/shiro
            cd $workdir
            unzip -o $workdir/release.zip
            cp $HOME/shiro/.env $workdir/standalone/.env
            export NEXT_SHARP_PATH=$(npm root -g)/sharp
            # https://github.com/Unitech/pm2/issues/3054
            cd $workdir/standalone
            pm2 stop $workdir/standalone/ecosystem.config.js && pm2 delete $workdir/standalone/ecosystem.config.js && pm2 start $workdir/standalone/ecosystem.config.js
            rm $workdir/release.zip
            pm2 save
            echo "Deployed successfully"

ここでは、SSH + SCP の方法を使用してビルド成果物をサーバーにアップロードし、関連するスクリプトを直接実行します。

私たちは GitHub ワークフロー ID を現在のビルドの識別子として使用し、サーバーデプロイディレクトリで区別します。これにより、毎回デプロイされた成果物がサーバーに存在し、将来のロールバックが容易になります。ロールバックプロセスは比較的伝統的ですが、実現できたと言えます。

私は PM2 を使用してプロジェクトをホストしていますが、もちろん他の方法を使用することもできます。

クロスリポジトリでのワークフロー呼び出し#

ソースコードリポジトリに新しいコミットがあると、ワークフローレポジトリのパイプラインを再実行する必要があります。

ここでは API 呼び出しを使用できます。

ソースコードリポジトリに新しいワークフローを追加します。

name: Trigger Target Workflow

on:
  push:
    branches:
      - main

jobs:
  trigger:
    runs-on: ubuntu-latest

    steps:
      - name: Trigger Workflow in Another Repository
        run: |
          repo_owner="innei-dev"
          repo_name="shiroi-deploy-action"
          event_type="trigger-workflow"

          curl -L \
            -X POST \
            -H "Accept: application/vnd.github+json" \
            -H "Authorization: Bearer ${{ secrets.PAT }}" \
            -H "X-GitHub-Api-Version: 2022-11-28" \
            https://api.github.com/repos/$repo_owner/$repo_name/dispatches \
            -d "{\"event_type\": \"$event_type\", \"client_payload\": {}}"

ここでは、GitHub API を利用してワークフローレポジトリのパイプラインを再実行します。

次に、呼び出される側のワークフロー構成を変更する必要があります。

on:
  push:
    branches:
      - main

  repository_dispatch:
    types: [trigger-workflow]

permissions: write-all

types は呼び出し元が定義したもので、一致させる必要があります。

これで、ソースが main に更新されると、API インターフェースを介して repository_dispatch 内の types と一致するワークフローが呼び出されます。

image

重複バージョンのビルドを防ぐ#

上記の構成は基本的に使用可能ですが、いくつかの点を判断する必要があります。

例えば、同じコミットは重複と見なされ、ビルドとデプロイは一度だけ行われるべきです。重複が発生した場合、ビルドとデプロイのプロセス全体をスキップする必要があります。

ここでは、各コミットハッシュを使用して判断し、前回成功したデプロイのハッシュを保存し、現在進行中のコミットハッシュと比較します。一致すればスキップします。

前回のコミットハッシュをファイルの形式で記録します(アーティファクトを使用することもできますが、なぜファイルを使用するのかは次のセクションで説明します)。

毎回ビルドが完了したコミットハッシュを現在のリポジトリの build_hash ファイルに保存します。

これを実現するために、いくつかのプロセスが必要です。まず、現在のリポジトリの build_hash を読み取り、次のプロセスで読み取れるように GITHUB_OUTPUT に保存します。

次のプロセスでは、ソースコードリポジトリをチェックアウトし、ソースコードリポジトリのコミットハッシュを読み取り、build_hash と比較して、boolean 値を出力します。同様に、GITHUB_OUTPUT に保存します。

次のプロセスでは、if を利用して、全体のプロセスを終了すべきかどうかを判断します(後続のプロセスはこれに依存しているため、すべて終了することになります)。

最後のプロセスでは、デプロイが完了した後、現在のコミットハッシュをリポジトリに保存します。Push アクションを使用しました。

このため、毎回 Bot が新しいコミットをプッシュすることになり、これもこのワークフローを実行すべきではありません。したがって、最初のプロセスで if をコミットメッセージに基づいてガードします。

参考構成は以下の通りです。

name: Build and Deploy

on:
  push:
    branches:
      - main

permissions: write-all

env:
  PNPM_VERSION: 9.x.x
  HASH_FILE: build_hash

jobs:
  prepare:
    name: Prepare
    runs-on: ubuntu-latest
    if: ${{ github.event.head_commit.message != 'Update hash file' }}

    outputs:
      hash_content: ${{ steps.read_hash.outputs.hash_content }}

    - name: Read HASH_FILE content
      id: read_hash
      run: |
        content=$(cat ${{ env.HASH_FILE }}) || true
        echo "hash_content=$content" >> "$GITHUB_OUTPUT"


  check:
    name: Check Should Rebuild
    runs-on: ubuntu-latest
    needs: prepare
    outputs:
      canceled: ${{ steps.use_content.outputs.canceled }}

    steps:
      - uses: actions/checkout@v4
        with:
          repository: innei-dev/shiroi
          token: ${{ secrets.GH_PAT }}
          fetch-depth: 0
          lfs: true

      - name: Use content from prev job and compare
        id: use_content
        env:
          FILE_HASH: ${{ needs.prepare.outputs.hash_content }}
        run: |
          file_hash=$FILE_HASH
          current_hash=$(git rev-parse --short HEAD)
          echo "File Hash: $file_hash"
          echo "Current Git Hash: $current_hash"
          if [ "$file_hash" == "$current_hash" ]; then
            echo "Hashes match. Stopping workflow."
            echo "canceled=true" >> $GITHUB_OUTPUT
          else
            echo "Hashes do not match. Continuing workflow."
          fi


  build:
    name: Build artifact
    runs-on: ubuntu-latest
    needs: check
    if: ${{needs.check.outputs.canceled != 'true'}}

    # .... other build job config

  store:
    name: Store artifact commit version
    runs-on: ubuntu-latest
    needs: [deploy, build] # build と deploy プロセスに依存
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          persist-credentials: false
          fetch-depth: 0

      - name: Use outputs from build
        env:
          SHA_SHORT: ${{ needs.build.outputs.sha_short }}
          BRANCH: ${{ needs.build.outputs.branch }}
        run: |
          echo "SHA Short from build: $SHA_SHORT"
          echo "Branch from build: $BRANCH"
      - name: Write hash to file
        env:
          SHA_SHORT: ${{ needs.build.outputs.sha_short }}

        run: echo $SHA_SHORT > ${{ env.HASH_FILE }}
      - name: Commit files
        run: |
          git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git config --local user.name "github-actions[bot]"
          git add ${{ env.HASH_FILE }}
          git commit -a -m "Update hash file"
      - name: Push changes
        uses: ad-m/github-push-action@master
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          branch: ${{ github.ref }}

永遠に無効にならない Cronjob の実行#

このワークフローを定期的に実行するために、schedule を使用できます。

name: Build and Deploy

on:
  push:
    branches:
      - main
  schedule:
    - cron: '0 3 * * *'

  repository_dispatch:
    types: [trigger-workflow]

GitHub アクションの制限により、リポジトリが 3 か月間活動がない場合、ワークフローが無効になります。したがって、前のセクションでコミットを使用して無効化を防ぐ方法を使用しました。毎回ビルドしてハッシュをアップロードすることも良い選択です。

以上がすべての内容です。

完全な構成はここにあります:

この記事は Mix Space によって xLog に同期更新されました。原始リンクは https://innei.in/posts/tech/automatically-build-projects-across-repositories-and-deploy-to-servers

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。