本篇是一个 GitHub Actions 使用示例,实现在每次提交代码时自动更新你的远端服务器正在运行的项目

但是我并不会从头开始向你介绍 GitHub Actions 是什么,而要求你事先对它有所了解

你可以去看官方文档,去 B 站找视频看,网上有很多高质量的参考资料

简单地说就是 GitHub 可以在你更新代码或合并分支的时候开一个虚拟机帮你完成一些事情

a054159e939a43e3ab5ff2406081ed6d

又或者,你也可以去问 ChatGPT,哈哈😄

那为什么说是“简单的”呢?因为本篇只会举一个很简单的例子,不会涉及 docker ,也没有 k3s 集群

本篇的例子可以在我的仓库找到:https://github.com/NX-Official/github-actions-test

为了将重点放在部署上,我以下面的 main.go 为例,它只显示一个简单的 hello world

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"github.com/gin-gonic/gin"
)

func main() {

r := gin.Default()

r.GET("/", func(c *gin.Context) {
c.String(200, "Hello World")
})

r.Run(":8080")
}

目前的项目结构如下:

1
2
3
4
./
├── go.mod
├── go.sum
└── main.go

我该如何部署?

现在来思考一个问题,我该部署到远程服务器

看了一圈助手里面的项目,大概有两种方案:

  1. 不在虚拟机上编译,直接 ssh 连接服务器,执行命令或一个脚本文件来自动拉取最新代码,编译文件并完成替换
  2. 直接在 GitHub 的虚拟机上完成编译,得到二进制文件,然后把文件送到我们的服务器上(直接转送可能很慢,我看有的项目是先放到 OSS 上,然后用一个 webhook 通知守护进程,去拉取文件并完成替换)

我两种都会演示一下,先来第一种,使用 ssh 直接连接服务器,并执行下面的步骤:

  • 使用 git pull 拉取最新代码

  • 运行一个自动编译与重新启动的脚本


服务器端的准备

现在来动手实践,首先我们先将项目克隆下来(这里当然是我自己的项目)

1
git clone https://github.com/NX-Official/github-actions-test.git

然后尝试编译并执行(当然啦,你要先安装 golang)

1
2
3
4
cd github-actions-test
go mod tidy
go build main.go
./main

看来是没问题的

现在,为了更方便地运行项目,我们可以将我们的程序注册为一个服务

1
2
cd /etc/systemd/system/
nano github-actions-test.service

然后写入下面的代码(你需要根据自己的项目来修改名称与路径)

1
2
3
4
5
6
7
8
9
[Unit]
Description=github-actions-test

[Service]
ExecStart=/root/github-actions-test/main
Restart=always

[Install]
WantedBy=multi-user.target

然后使用下面的命令运行与查询状态

1
2
systemctl start github-actions-test
systemctl status github-actions-tes

image-20221209222401325

没有问题,继续下面的步骤


定义 Actions secrets

要连接到服务器,必定需要用户名和密码(当然你也可以使用私钥之类的,但这里为了简单我就使用密码)

但是密码这种东西你肯定不能直接写在 build.yml 中,你肯定不想让别人翻你的项目的时候翻到你服务器的密码吧~

所以 GitHub 就有了 Actions secrets 这个东西,你可以在这里定义运行时的环境变量,并只有项目参与者才能修改

image-20221209224126982

在这里定义好三个变量,就可以在下面的脚本中使用了


编写脚本

为了方便,现在来为这个项目写一个重启服务的脚本,保存为 run.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/sh

serviceName="github-actions-test"

# 停止服务
systemctl stop $serviceName
rm main -f

# 重新编译
export PATH=$PATH:/usr/local/go/bin
go mod tidy
go build main.go

# 启动服务
systemctl start $serviceName
systemctl status $serviceName

现在在项目中创建 .github/workflow 目录 ,再在里面建一个 build.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
name: Build
on:
push:
branches:
- main
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: 直接在服务器执行命令
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
password: ${{ secrets.DEPLOY_SECRET }}
script: cd ~/github-actions-test && git pull && bash run.sh

这意味着将在 pushmain 分支的时候,自动连接服务器并执行最下面的命令

现在来对 main.go 做一些修改,测试一下自动化部署对效果

1
2
3
r.GET("/", func(c *gin.Context) {
c.String(200, "Hello World, I'm a Gin server")
})

然后提交并推送,观察发现脚本已经成功运行

image-20221209230812804

刷新页面,看见已经成功重新编译并启动服务

image-20221209230856708


另一种方案

下面来试一下另一种方案,为了区分开来,我新建了一个 v2 分支

然后修改脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
name: Build
on:
push:
branch:
- v2

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3

- name: Setup Golang
uses: actions/setup-go@v3
with:
go-version: '1.19'
cache: true



- name: Build
run: go build main.go

- name: Deploy
env:
SSH_USERNAME: ${{ secrets.DEPLOY_USER }}
SSH_PASSWORD: ${{ secrets.DEPLOY_SECRET }}
SSH_PORT: ${{ secrets.SSH_PORT }}
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}

run: |
# Stop the service
sshpass -p $SSH_PASSWORD ssh -o StrictHostKeyChecking=no $SSH_USERNAME@$DEPLOY_HOST -p $SSH_PORT "systemctl stop github-actions-test && rm -rf $DEPLOY_PATH/main"

# Copy the binary to the remote server
sshpass -p $SSH_PASSWORD scp -r -o StrictHostKeyChecking=no -P 47 main [email protected]:/root/github-actions-test

# Restart the service on the remote server
sshpass -p $SSH_PASSWORD ssh -o StrictHostKeyChecking=no $SSH_USERNAME@$DEPLOY_HOST -p $SSH_PORT "systemctl start github-actions-test"

Checkout 指的是拉取你的代码,然后后面设置 go 版本,编译,部署

同时修改一下 main.go ,这样可以对比出不同

然后提交,看看结果

image-20221210231722735

十分顺利

这里我的例子是将编译和部署写在一个文件中的,其实你可以拆分成两个,这样更加规整,这个就留给你来折腾了😄