利用 git hooks 和 github actions 远端部署

利用 git hooks 和 github actions 远端部署

能看到这个页面,大抵是部署成功了 #

关于 git 的使用,我还有很多地方要学习。 本篇记一次 git hooks 的使用。(以及一次 github actions 的使用。)

最近 lhw 正在用 rust 写一个新的模块,用于计算橙汁角色间的战斗。
lhw 打算利用 web 接口来方便其他程序对接这个模块,我实在佩服他的学习能力。
模块做完后,羽希大概会将其对接到机器人和网站上。

我希望 lhw 的源码更新可以实时部署在服务器上。
我的第一念头是 github pages。毕竟前不久刚用它部署了 Matrix 的贴纸服务
但是 github pages 只能用于静态页面,这显然满足不了 rust-web-api ,作罢。除非 lhw 现在去写 js。

其次的想法便是利用 git 了。

说来惭愧,我虽然将 git 用作不少个人项目的仓库和版本管理,却从未将其自动化部署。
blog 和一些网站是额外写的 bash,在仓库更新后我手动运行。SoraBot 就更离谱了,每次甚至是我亲自打包。
我希望达成的目的是:lhw 每次 git push 完,服务器上就自动构建其 rust 程序并运行。 既然现在 lhw 的模块还没有写好,我对 rust 也不甚熟悉(不然也不会用基于 go 的 blog 了),那么这篇就先写一半好了。

先实现本 blog 在 git push 后的自动部署。

blog 的自动更新 #

用于托管服务的仓库与普通的 git 仓库有所不同,它不包含工作区。 我的 blog 在 sumika 的工作目录如下。

blog
├── blog.task   ## 个人习惯,一些其他的自动化任务或者笔记会写在这里
├── instance    ## 服务目录,或者叫做生产环境?,这是一个标准的 hugo 目录
│   ├── archetypes
│   ├── config.toml
│   ├── content
│   ├── resources
│   ├── task    ## 内部任务
│   └── themes
└── repo        ## 空的 git 项目
    ├── branches
    ├── config
    ├── description
    ├── HEAD
    ├── hooks   ## git 钩子,用于根据事件触发各种任务
    ├── index
    ├── info
    ├── objects
    └── refs

其中 repo 是这次我们要新建的。hooks 是我们要使用的。

# 在服务器(sumika)上
# 新建仓库
git init --bare repo
# 编辑钩子
touch repo/hools/post-receive
vim post-receive

hooks 目录用于存放各种钩子,其中包含不少官方的 sample 实例。
这里使用的 post-receive 会在远端 push 操作后触发,编辑如下。

# 指定代码检出目录
DIR=PATH/TO/blog/instance
git --work-tree=${DIR} clean -fd
# 强制检出
git --work-tree=${DIR} checkout --force

# 后面还可以写一些诸如「start hugo server」之类的东西
# 由于 hugo 自动进行目录更新,服务已经启动,我便不必写在这里了。

这个钩子比较简单,一部分原因是 hugo 的托管不必在此费心。
我一般使用 suguri 或者 suguriv2 写 blog ,之后 push 到这个 repo 上就会自动将文件检出到 instance,完成 blog 的自动更新。

其他项目的自动更新 #

这里大概要写的是自动构建和部署 lhw 的 rust-web-api 。
先放着吧。

Github Actions 自动部署 #

我改主意了(,尝试过后感觉 github actions 也挺好用的。
所以 lhw 的 api 使用 github actions 来自动部署。
(好处大概是能少推一次仓库。省去了推到 sumika 上的过程。)

羽希创建的 Github Actions 内容如下。

name: Build and Deploy

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

env:
  CARGO_TERM_COLOR: always

jobs:
  build:
    name: build
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v2

      - name: Install Rust
        uses: actions-rs/toolchain@v1
        with:
          toolchain: stable

      - name: Build binary
        uses: actions-rs/cargo@v1
        with:
          command: build
          args: --release --all-features

      - name: Deploy to Staging server
        uses: easingthemes/ssh-deploy@main
        env:
          SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
          ARGS: "-rlgoDzvc -i"
          REMOTE_HOST: ${{ secrets.REMOTE_HOST }}
          REMOTE_USER: ${{ secrets.REMOTE_USER }}
          REMOTE_PORT: ${{ secrets.REMOTE_PORT }}
          SOURCE: "./target/release/ojbattle"
          TARGET: ${{ secrets.REMOTE_TARGET }}

在 main 分支触发 push 后,在 github actions 提供的 ubuntu 容器里,构建 release,然后利用 sftp 将 release 推送到 sumika 的指定目录去。
其中涉及 5 个 secrets 值,分别对应远端服务器的基本信息。SOURCE 对应容器里的原文件,TARGET 对应远端文件地址。

但是这个 actions 只涉及文件推送,不涉及执行,所以还需要在 sumika 上进行监听,在监听到推送后,重启服务。
这么做比直接在 after script 执行会安全一点点。(只有一点点。)

在 sumika 上监听推送,用到的是 inotify-tools 中的 inotifywait

在推送目录下创建了以下脚本: watch.sh

#!/bin/bash
#指定需要监视的文件夹
#指定输出信息的文件
#需要执行的脚本文件
dir=path/to/ojbattle
log_file=path/to/watch.log
rsync_file=path/to/run_ojbattle.sh

#监视文件的close动作
while
inotifywait -r $dir -o $log_file -e close --timefmt '%d/%m/%y %H:%M' --format '%T %w %f %e';
do
    # 先把旧进程杀了
    pid=$(ps -ef | grep ojbattle | grep -v grep | awk '{print $2}')
    if [ ! -z "$pid" ]; then
        kill $pid
    fi
    bash rsync_file
done

然后运行 nohup bash watch.sh > /dev/null 2>&1 &

watch.sh 的逻辑如下:监听文件变动,在文件变动后,运行该文件。但是在此之前,要把先前运行的进程杀掉。

这样,lhw 的仓库的 main 分支每次更新后,会触发 github actions 的自动构建,在构建完成后,会被推送到 sumika,sumika在监听到文件变动后,会重启该进程,达成自动部署的目的。