最近,Vercel が再度価格を調整し、Hobby プランではますます不十分になったため、Vercel の使用をやめ、プライベートサーバーに移行して Next.js プロジェクトをデプロイすることにしました。
プライベートサーバーでのデプロイ体験は非常に不便です。第一に、Vercel のような完全自動デプロイがなく、迅速なロールバックもできません。第二に、Next.js プロジェクトのビルドには非常に大きなメモリと CPU リソースが必要で、一般的な軽量サーバーではビルド中にクラッシュするか、ダウンしてしまうことがあります。
目標#
- GitHub を利用して、ビルド時の環境変数に影響されない汎用的な成果物を構築する。(後者については、一次構建多处部署 - Next.js Runtime Env という記事で詳しく知ることができます)
- ビルド成果物をリモートサーバーにプッシュする方法
- 異なるソースコードリポジトリでビルドのワークフローを実行する方法(この要件は、クローズドソースリポジトリに対して GitHub CI の時間やその他の制限があるためです;もう一つは、このようなワークフロー構成リポジトリをオープンソースにし、ソースコードリポジトリをクローズドソースにできることです)
- ロールバックを実現する方法(便利ではないかもしれませんが、使用可能です)
プロセス#
上記の目標に基づいて、私たちが行うべきことを考え出すことができます。おおよそこのようなビルドプロセスです。
- ワークフローレポジトリではなく、ソースコードリポジトリからコードをチェックアウトすることが重要です。
- 通常のコードビルド
- バージョンを区別し、サーバーにプッシュする
- ビルドを完了する
ソースコードリポジトリにコードの変更があった場合、ワークフローレポジトリのパイプラインを再実行する必要があります。
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
と一致するワークフローが呼び出されます。
重複バージョンのビルドを防ぐ#
上記の構成は基本的に使用可能ですが、いくつかの点を判断する必要があります。
例えば、同じコミットは重複と見なされ、ビルドとデプロイは一度だけ行われるべきです。重複が発生した場合、ビルドとデプロイのプロセス全体をスキップする必要があります。
ここでは、各コミットハッシュを使用して判断し、前回成功したデプロイのハッシュを保存し、現在進行中のコミットハッシュと比較します。一致すればスキップします。
前回のコミットハッシュをファイルの形式で記録します(アーティファクトを使用することもできますが、なぜファイルを使用するのかは次のセクションで説明します)。
毎回ビルドが完了したコミットハッシュを現在のリポジトリの 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