如何使用Github Actions实现一个简单的ci/cd
2022-06-01 凌晨发布787 次点击0 条评论需阅读18分钟

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


前言

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

前置条件

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

Github Actions

我觉得官方文档就写得挺好的, 所以这一节的介绍, 我就直接翻译过来了.
引用官方文档来解释是个啥:
使用 GitHub Actions 自动化, 自定义和执行软件开发工作流程. 可以创建和共享操作以执行您想要的任何工作, 包括 CI/CD, 并将操作结合在完全自定义的工作流程中.
overview-actions-simple
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
ci-cd-project-directory

创建 Github Actions

首先我们在 Github 仓库中点击 Actions 这个 tab, 如果是 node 项目的话, 点击红框里面的 node.js.
ci-cd-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
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-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
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
ci-cd-sercet-where-step-1
点击右上角的 New repository secret.
ci-cd-sercet-where-step-2
ci-cd-sercet-where-step-2
添加对应的 key、value 即可添加 sercet 了.
ci-cd-sercet-where-step-3
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 源代码
如果本文对你有所帮助的话, 点个赞就是对作者最大的肯定!!!