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

两个仓库#

明确了上面的流程,我们现在就需要创建一个仓库专门供跑 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_dispatchtypes 相匹配的工作流。

image

防止重复版本的构建#

上面的配置基本已经可用,但是还有些地方我们需要判断下。

例如,相同的 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


加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。