最近,Vercel 又又對價格進行了調整,讓 Hobby 越來越不夠用了,所以放棄了使用 Vercel,轉向私有伺服器部署 Next.js 專案。
對於私有伺服器的部署體驗是非常不友好的。第一,沒有 Vercel 這樣的全自動部署,也不能及時回滾。第二,Next.js 專案構建需要非常大的內存和 CPU 資源,一般的輕量伺服器可能在構建過程中不是爆堆就是宕機了。
目標#
- 利用 GitHub 去構建一個通用產物,不受構建時的環境變量影響。(後者你可以通過 一次構建多處部署 - Next.js Runtime Env
這篇文章了解更多) - 如何推送構建產物到遠程伺服器
- 如何跨源碼倉庫外運行構建的工作流(這個需求是因為對於閉源倉庫,GitHub CI 的時長和其他都有限制;另一個,這樣工作流配置倉庫可以開源,而源碼倉庫可以閉源)
- 如何實現回滾(可以不那麼方便但是可用)
流程#
根據上面的目標,我們可以構想出我們需要做的事,大概是這樣的一個構建流程。
- 從源碼倉庫檢出代碼,而不是工作流倉庫,這點很重要。
- 常規的代碼構建
- 區分版本,然後推送到伺服器
- 完成構建
當源碼倉庫發生代碼變動,需要重新執行工作流倉庫的流水線。
兩個倉庫#
明確了上面的流程,我們現在就需要創建一個倉庫專門供跑 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 }} # 這裡需要你可以訪問私有倉庫的 Token
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 # 這裡是你的構建腳本
上面的註釋的地方需要注意。
構建和部署#
構建和跨 Job 共享產物#
接下來我們來寫部署的工作配置。
在跨 Job 之間的產物共享,需要使用 Artifact。在構建的流程中,上傳最後的產物作為 Artifact,然後在下一個 Job 中下載 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)都可以下載。
People who are signed into GitHub and have read access to a repository can download workflow artifacts.
由於上面的方式並不安全,所以我們這裡使用 CI cache 去實現相同的功能。
jobs:
build:
# ...
- name: Cache Build Artifacts
id: cache-primes
uses: actions/cache/save@v4
with:
path: assets
key: ${{ github.run_number }}-release # 使用 工作流序號 作为 key
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 Workflow 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
相匹配的工作流。
防止重複版本的構建#
上面的配置基本已經可用,但是還有些地方我們需要判斷下。
例如,相同的 commit 被認為是重複的,他應該只會被構建和推送部署一次。如果重複命中,應該跳過整個構建和部署流程。
這裡我們利用每次的 commit hash 去判斷,保存上次成功部署的 hash,和正在進行的 commit hash 比對,如果一致就跳過。
我們可以用文件的方式記錄上次的 commit hash(你也可以用 artifact,至於為什麼我使用文件請看下節的內容)。
我們把每次構建完成的 commit hash 保存在當前倉庫下的 build_hash
文件下。
這裡我們需要好幾個流程去做這個事,首先讀取當前倉庫下的 build_hash
並保存在 GITHUB_OUTPUT
中供後續的流程讀取。
然後下個流程,檢出源碼倉庫,讀取源碼倉庫的 commit hash,和 build_hash
比對,輸出一個 boolean
值,同樣保存在 GITHUB_OUTPUT
中。
下個流程,利用 if
直接判斷是否應該退出整個流程(因為後續的流程都依賴這個,所以等於全部退出了)。
最後一個流程,在完成部署之後,保存當前的 commit hash 到 repo 中,我們使用了 Push action 去做。
然後因為這樣導致每次 Bot 會 push 一個新的 commit,這個也不應該跑這個工作流。所以在第一個流程中 if
根據 commit message 去做下守衛。
參考配置如下:
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 action 的限制,當一個倉庫在 3 個月內沒有活動時,工作流會被禁用。所以上一節中我們用了提交的方式去防止被禁用。在每次構建去上傳 hash,也是個很好的選擇。
完事了,以上就是全部的內容了。
完整的配置在這裡:
此文由 Mix Space 同步更新至 xLog
原始鏈接為 https://innei.in/posts/tech/automatically-build-projects-across-repositories-and-deploy-to-servers