使用 Github Actions 实现一个简单的 ci/cd

3,495 阅读5分钟

本文首发于itsuki.cn个人博客

前言

在自己的个人博客项目开发中, 每次完成一个新功能, 前后端都需要重新部署进行更新, 发现每次手动打包部署比较麻烦(我就是一个懒狗), 所以想想能不能使用 Github Actions 来实现一个每次 push 上去就自动打包构建部署, 这样子我就可以少做很多事情. 经过我的不断捣鼓, 终于实现了, 所以写一篇文章来记录一下, 一个 ci/cd 可以帮你节约很多时间.

前置条件

  1. 服务器 (花 💰).
  2. 服务器安装node (自行百度).
  3. 服务器安装pm2 (npm i -g pm2).
  4. 如果没有的话, 先看看有个印象.

Github Actions

我觉得官方文档就写得挺好的, 所以这一节的介绍, 我就直接翻译过来了.

引用官方文档来解释是个啥:

使用 GitHub Actions 自动化, 自定义和执行软件开发工作流程. 可以创建和共享操作以执行您想要的任何工作, 包括 CI/CD, 并将操作结合在完全自定义的工作流程中.

overview-actions-simple

Workflows

Workflows: Github Actions 有多个 Workflows, 工作流是一个可配置的自动化流程, 将运行一个或多个 Job. 工作流由签入到存储库的 yaml 文件定义, 并将在由存储库中的事件触发时运行, 也可以手动触发, 或按定义的时间表触发.

Event

Event: Git 仓库中的特定活动, 用来触发工作流. 例如, 某人 push 到 main 分支, push 到 main 分支就是一个 Event, 然后会执行工作流, 当然还有其他的操作也可以触发,比如说 pull request、tag 等操作.

Runner

Runner: Runner 是触发工作流程的服务器. 每个 Runner 一次都可以运行一个作业. GitHub 提供Ubuntu LinuxMicrosoft WindowsMacOS来运行工作流程; 每个工作流程运行均以新的, 新的虚拟机执行.

Jobs

Jobs: 工作流中在同一运行器上执行的一组步骤. 每个步骤要么是将要执行的 shell 脚本, 要么是将要运行的操作. 步骤按顺序执行, 并且相互依赖. 由于每个步骤都在同一个运行程序上执行, 因此您可以在一个步骤之间共享数据. 例如, 您可以有一个构建应用程序的步骤, 然后是一个测试所构建应用程序的步骤.

Actions

Actions: 执行复杂且经常重复任务的 GitHub Action 平台的自定义应用程序. 使用操作来帮助减少您在工作流文件中编写的重复代码的量. Actions 可以从 GitHub 中提取 Git 存储库, 为您的构建环境设置正确的工具链, 或为云提供商设置身份验证.

准备活动

创建一个项目

  1. 我平时使用next.js多一点, 就使用npx create-next-app silver-robot 来创建一个next.js项目.
  2. 包管理使用的yarn.

选择什么创建项目就是萝卜青菜各有所爱.

创建完成之后的目录:

ci-cd-project-directory

创建 Github Actions

首先我们在 Github 仓库中点击 Actions 这个 tab, 如果是 node 项目的话, 点击红框里面的 node.js.

ci-cd-node.js

然后就会在当前项目新建一个文件夹.github/workflows/node.js.yml.

# .github/workflows/node.js.yml
# 当前整个文件就是一个Workflows

name: Node.js CI # 工作流的名称

on: # 执行该工作流的事件, 上图的Events
  push:
    branches: [main] # 当main分支push之后就执行该工作流
  pull_request:
    branches: [main] # 当main分支pull_request就执行该工作流

jobs: # 执行的一组任务, 上图中的Jobs
  build:
    runs-on: ubuntu-latest # 在什么机器上执行, 上图中的Runner

    strategy: # 矩阵策略
      matrix:
        node-version: [12.x, 14.x, 16.x] # 等同于使用12.x、14.x、16.x三个不同node版本的job跑同一个工作流

    steps: # 要执行的步骤 上图中的Actions
      - uses: actions/checkout@v3 # 相当于git clone到当前运行的机器上
      - name: Use Node.js ${{ matrix.node-version }} # 显示在step上的别名
        uses: actions/setup-node@v3 # 设置node环境(可以使用npm、yarn, 选择指定的node版本)
        with: # 传入参数
          node-version: ${{ matrix.node-version }} # 指定什么版本 ${{}} 引用变量
          cache: 'npm' #
      - run: npm ci # 运行npm ci
      - run: npm run build --if-present # 运行npm run build 命令
      - run: npm test # 运行 npm test

所以node.js.yml文件的意思就是:

  1. 定义了一个 Node.js CI 的工作流.
  2. 当 main 分支有pushpull_request操作的时候就在ubuntu-latest机器上执行工作流.
  3. 使用三个不同的 node 版本, 传入不同的参数依次运行npm cinpm run buildnpm test命令.
  4. 结束.

等同于

// node 12.x
// 1. git clone xxxx
// 2. npm ci
// 3. npm run build --if-present
// 4. npm test

// node 14.x
// 1. git clone xxxx
// 2. npm ci
// 3. npm run build --if-present
// 4. npm test

// node 16.x
// 1. git clone xxxx
// 2. npm ci
// 3. npm run build --if-present
// 4. npm test

所以看看这个工作流的效果:

ci-cd-simple-demo-result

报错了, 原因也很简单, 因为npm ci它依赖于package-lock.json, 因为包管理使用的 yarn, 只有yarn.lock, 如果使用的 npm 就不会有这个问题啦.

实现一个简单的工作流

我们来实现一个全局安装@vue/cli, 查看全局安装之后的 vue 版本, 在刚刚的node.js.yml进行精简与修改.

name: vue/cli # 显示名字

on:
  push:
    branches: [main] # main分支push就执行该工作流

jobs:
  build:
    runs-on: ubuntu-latest # 在最新的ubuntu机器上

    steps:
      - uses: actions/checkout@v3 # git 克隆到当前机器上
      - name: Use Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 16.x # 使用16的node版本
          cache: 'yarn'
      - run: yarn global add @vue/cli # 全局安装@vue/cli
      - run: vue -V # 查看版本

效果如下图所示:

ci-cd-vue-cli

实现一个简单的 ci/cd

有了上面这个例子的基础, 我们就可以一点点的实现简单的 ci/cd, 我感觉就是用 Github Actions 来模仿我们手动构建的步骤.

  1. 打包.
  2. 将打包后的文件上传到服务器.
  3. 在服务器初始化然后运行.

我将从这三个步开始拆解说明.

打包

第一步就是要进行打包, 我们想一下在本地是怎么进行打包的呢?

  1. 通过git clone拉一个项目下来.
  2. yarn/npm install安装依赖.
  3. 最后再进行yarn build/npm run build.
name: Deploy

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3 # git 克隆到当前机器上
      - name: Use Node.js
        uses: actions/setup-node@v3 # 设置node环境
        with:
          node-version: 16.x # 指定版本
          cache: 'yarn'

      - run: yarn # 运行 yarn
      - run: yarn build # 运行 yarn build
      - run: ls -a # 查看打包后的目录文件

可以使用name来设置在jobssteps的名称显示, 更加清晰易懂, 所以再来修改一下.

# 省略...

steps:
  # 省略...
  - name: Install dependencies
    run: yarn # 运行 yarn

  - name: Run build
    run: yarn build # 运行 yarn build

  - name: view directory files
    run: ls -a # 查看打包后的目录文件

ok, 我们来看看运行的效果.

ci-cd-build-log

可以看到ls -a完美执行, 同时可以看到next.js打包后的文件: .next, 这第一步打包算是完成了.

上传服务器

第二步就是将打包好的文件上传到服务器, 我自己在本地部署的时候, 就是利用ssh连接远程服务器上传.

所以在使用 Github Actions 时, 可以使用scp-action这个库, 我们先来看看代码.

# 省略...

steps:
  # 省略...
  - name: Rename build folder
    run: mv .next build # 上传之前先重命名.next成build, 防止上传之后覆盖了.next
  - uses: appleboy/scp-action@master
    with:
      host: ${{ secrets.HOST }} # 服务器host
      username: ${{ secrets.USER }} # 服务器用户名
      password: ${{ secrets.PASSWORD }} # 服务器密码
      source: 'build,package.json,public' # 需要上传的文件, 多文件使用逗号隔开
      target: '~/demo' # 上传到服务器的什么位置

这里我们用到了${{}}以及secrets.HOST, 前一个好理解, 就是引用一个变量, 后一个是啥呢?

我们可以使用env进行环境变量的声明. 也可以使用 secrets 进行加密版环境变量的声明.

env 环境变量

我们先看 Github Actions 中的一个环境变量的例子.

name: Greeting on variable day

on: workflow_dispatch # 手动触发工作流

env:
  DAY_OF_WEEK: Monday # 定义整个工作流中的变量

jobs:
  greeting_job:
    runs-on: ubuntu-latest
    env:
      Greeting: Hello # 定义在jobs中的变量
    steps:
      - name: "Say Hello Mona it's Monday"
        run: echo "$Greeting $First_Name. Today is $DAY_OF_WEEK!"
        env:
          First_Name: Mona # 定义在steps中的变量

打印出来的结果是:

Run echo "$Greeting $First_Name. Today is $DAY_OF_WEEK!"

Hello Mona. Today is Monday!

secrets

比如说有一些登陆、数据库的账号和密码、或者说一些第三方提供的密钥要使用的话, 使用环境变量无疑全部都暴露出来了, 所以我们不妨使用secrets来定义这些秘密变量.

首先在这里找到 secrets 的定义:

ci-cd-sercet-where-step-1

点击右上角的 New repository secret.

ci-cd-sercet-where-step-2

添加对应的 key、value 即可添加 sercet 了.

ci-cd-sercet-where-step-3

是不是很简单, 这样子你就可以使用${{secrets.xxxx}}引用秘密变量了, 所以上面的代码中用到了HOSTUSERPASSWORD三个密码变量需要在 sercets 中定义.

在服务器运行命令

如果是@vue/clicreate-react-app打包出来的静态文件, 只需要上传到服务器的指定文件夹就可以了, 就不需要进行这一步. 但是如果还需要在服务器把打包后的项目(比如说 ssr 项目)再次运行起来的话, 就需要进行当前这一步了.

在本地的时候就是上传到服务器之后, 然后在服务器进行一些操作就可以部署成功, 所以在使用 Github Actions 时, 可以使用ssh-action这个库, 我们先来看看代码.

# 省略...
steps:
  - name: Run Deploy
    uses: appleboy/ssh-action@master
    with:
      command_timeout: 4m
      host: ${{ secrets.HOST }}
      username: ${{ secrets.USER }}
      password: ${{ secrets.PASSWORD }}
      script: | # 运行多行命令
        echo "[deploy] start deployment..."
        # 进到当前文件夹
        cd ~/demo

        # 停止服务
        pm2 stop demo
        pm2 delete demo

        # 删除之前的文件
        rm -rf .next
        rm -rf node_modules

        # 将上传的文件的文件进行重命名 build -> .next
        mv build .next

        # 安装依赖
        yarn --prod

        # 启动服务
        yarn pm2

        echo "[deploy] end deployment..."
        echo "[deploy] success"

所以我们还需要在package.json中写一个pm2的运行命令.

{
  "scripts": {
    "start": "next start",
    "pm2": "pm2 start yarn --name 'demo' --interpreter bash -- start "
  },
  "dependencies": {
    // ...
  }
}

最终代码

.github/workflows/node.js.yml

name: Node.js CI

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3 # git 克隆到当前机器上
      - name: Use Node.js
        uses: actions/setup-node@v3 # 设置node环境
        with:
          node-version: 16.x # 指定版本
          cache: 'yarn'

      - name: Install dependencies
        run: yarn # 运行 yarn

      - name: Run build
        run: yarn build # 运行 yarn build

      - name: View directory files
        run: ls -a # 查看打包后的目录文件

      - name: Rename build folder
        run: mv .next build # 重命名.next成build
      - uses: appleboy/scp-action@master # 使用scp-action进行文件上传
        with:
          host: ${{ secrets.HOST }} # 服务器host
          username: ${{ secrets.USER }} # 服务器用户名
          password: ${{ secrets.PASSWORD }} # 服务器密码
          source: 'build,package.json,public' # 需要上传的文件
          target: '~/demo' # 上传到服务器的什么位置

      - name: Run Deploy
        uses: appleboy/ssh-action@master
        with:
          command_timeout: 4m
          host: ${{ secrets.HOST }} # 服务器host
          username: ${{ secrets.USER }} # 服务器用户名
          password: ${{ secrets.PASSWORD }} # 服务器密码
          script: | # 运行多行命令
            echo "[deploy] start deployment..."
            # 进到当前文件夹
            cd ~/demo

            # 停止服务
            pm2 stop demo
            pm2 delete demo

            # 删除之前的文件
            rm -rf .next
            rm -rf node_modules

            # 将上传的文件的文件进行重命名 build -> .next
            mv build .next

            # 安装依赖
            yarn --prod

            # 启动服务
            yarn pm2

            echo "[deploy] end deployment..."
            echo "[deploy] success"

package.json

{
  "scripts": {
    "start": "next start",
    "pm2": "pm2 start yarn --name 'demo' --interpreter bash -- start "
  },
  "dependencies": {
    // ...
  }
}

小结

以上就完成一个简单的 ci/cd, 每当 main 分支 push、pull_request 的时候就会运行该工作流, 经过我的使用感觉挺不错的, 当然这只是比较简单的. 后面自己捣鼓的话, 可以尝试加上actions-cache加快 Github Actions 的流程、使用几个job进行解耦合, 在不同的job进行分享数据、进行测试/ci 等操作, 这个完全取决于你自己怎么玩.

  1. Github Actions
  2. scp-action
  3. ssh-action
  4. demo 源代码

如果本文对你有所帮助的话, 点个赞就是对作者最大的肯定!!!