mirror of
https://github.com/go-admin-team/go-admin.git
synced 2026-10-14 17:19:21 +00:00
Stop mounting a host configuration and run migrate inside the new container before the server starts, so the migration applies to the database the server actually uses. A failed migration fails the health check and rolls back.
194 lines
9.4 KiB
YAML
194 lines
9.4 KiB
YAML
name: Build
|
|
|
|
# Documentation-only changes, and changes confined to the Kubernetes
|
|
# manifests, skip this workflow entirely.
|
|
#
|
|
# A push to master here does not just build - it pushes an image, runs the
|
|
# migrations and restarts the demo container, so the site takes a short outage.
|
|
# Paying that for a README edit is waste at best; at worst a deploy fails for a
|
|
# reason unrelated to anything in the change. Code coverage is unaffected,
|
|
# because go.yml still builds every push and pull request.
|
|
#
|
|
# scripts/k8s holds deploy.yml, storage.yml and prerun.sh, and the deploy below
|
|
# reads none of them - it is an ssh into one host that runs docker, building the
|
|
# Dockerfile at the repository root. Those manifests are for people deploying to
|
|
# a cluster of their own. The pattern is scripts/k8s/** rather than scripts/**
|
|
# because scripts/Dockerfile is a build input: go.yml builds the release image
|
|
# from it on a tag.
|
|
#
|
|
# A file outside these patterns still runs the workflow even when the rest of
|
|
# the change is ignorable: paths-ignore skips only when every changed path
|
|
# matches. Editing this file is one such case, on purpose - a deploy script
|
|
# that is never exercised by the change that broke it is worse than an outage.
|
|
on:
|
|
push:
|
|
branches: [ master ]
|
|
paths-ignore:
|
|
- '**.md'
|
|
- 'docs/**'
|
|
- 'LICENSE*'
|
|
- '.github/ISSUE_TEMPLATE/**'
|
|
- 'scripts/k8s/**'
|
|
pull_request:
|
|
branches: [ master ]
|
|
paths-ignore:
|
|
- '**.md'
|
|
- 'docs/**'
|
|
- 'LICENSE*'
|
|
- '.github/ISSUE_TEMPLATE/**'
|
|
- 'scripts/k8s/**'
|
|
|
|
# One deploy at a time. Two merges seconds apart raced here: both runs did
|
|
# docker rm -f then docker run, the second removed the container the first had
|
|
# just created, and the first's docker run then failed on a name conflict -
|
|
# leaving the demo on the older image with a red deploy.
|
|
concurrency:
|
|
group: deploy-${{ github.ref }}
|
|
cancel-in-progress: false
|
|
|
|
env:
|
|
IMAGE_NAME: registry.ap-northeast-1.aliyuncs.com/go-admin/go-admin-api # 镜像名称
|
|
TAG: ${{ github.sha }}
|
|
IMAGE_NAME_TAG: registry.ap-northeast-1.aliyuncs.com/go-admin/go-admin-api:${{ github.sha }}
|
|
|
|
jobs:
|
|
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
|
|
- name: Set up Go
|
|
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
|
with:
|
|
go-version: 1.27.1
|
|
|
|
- name: Tidy
|
|
run: go mod tidy
|
|
|
|
- name: Build
|
|
run: env CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -tags "sqlite3,json1" --ldflags "-extldflags -static" -o main .
|
|
|
|
# 以下推镜像与重启步骤仅在 master 收到 push 时执行。
|
|
# pull_request 事件同样会触发本工作流,若不加限制,任何指向 master 的
|
|
# PR 一经创建就会把 PR 分支的镜像推上仓库,并直接重启线上 API 服务,
|
|
# 且发生在合并之前。构建与编译校验不受影响,PR 仍会执行。
|
|
- name: Build the Docker image and push
|
|
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
|
run: |
|
|
docker login --username=${{ secrets.DOCKER_USERNAME }} registry.ap-northeast-1.aliyuncs.com --password=${{ secrets.DOCKER_PASSWORD }}
|
|
echo "************ docker login end"
|
|
docker build -t go-admin-api:latest .
|
|
echo "************ docker build end"
|
|
docker tag go-admin-api ${{ env.IMAGE_NAME_TAG }}
|
|
echo "************ docker tag end"
|
|
docker images
|
|
echo "************ docker images end"
|
|
docker push ${{ env.IMAGE_NAME_TAG }} # 推送
|
|
echo "************ docker push end"
|
|
|
|
- name: Restart server # 第五步,重启服务
|
|
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
|
uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5
|
|
env:
|
|
GITHUB_SHA_X: ${GITHUB_SHA}
|
|
with:
|
|
host: ${{ secrets.SSH_HOST }} # 下面三个配置与上一步类似
|
|
username: ${{ secrets.SSH_USERNAME }}
|
|
key: ${{ secrets.DEPLOY_KEY }}
|
|
# 重启的脚本,根据自身情况做相应改动,一般要做的是migrate数据库以及重启服务器
|
|
#
|
|
# 演示站用 sqlite,配置与数据库都是镜像里自带的 config/settings.demo.yml 和
|
|
# go-admin-db.db,不挂载宿主机文件。演示模式不允许用户改数据,所以数据不需要
|
|
# 留存:每次部署换新容器,就是回到干净状态。
|
|
#
|
|
# 迁移在新容器里、服务启动之前跑:数据库留在容器内,一次性容器里跑的迁移
|
|
# 改的是用完即删的副本,对服务没有意义。迁移失败则容器退出、健康检查不过,
|
|
# 走下面的回滚。旧容器改名保留而不是删除,新容器不健康时能原样恢复。
|
|
# 健康检查两条都要过——HTTP 活着不代表数据库通了。
|
|
script: |
|
|
set -u
|
|
IMG="${{ env.IMAGE_NAME_TAG }}"
|
|
NAME=go-admin-api
|
|
PREV="$NAME-prev"
|
|
|
|
# Old images of this repository are removed here and nowhere else.
|
|
# Every deployment pulls one tagged with its commit and nothing ever
|
|
# removed the previous one, so they only accumulated: 68 of them
|
|
# filled the disk and the next deployment could not pull.
|
|
#
|
|
# Three are kept so a release can be re-run by tag by hand.
|
|
#
|
|
# Only this repository's images are listed, because the host runs
|
|
# other services whose images are not this script's business. The
|
|
# image the live container is on is excluded by id rather than by
|
|
# position, so it survives even if the listing order is not what
|
|
# it looks like. With no container to ask, the function returns
|
|
# rather than running the pipeline on an empty id - which would
|
|
# also delete nothing, but by way of grep -v matching every line,
|
|
# which reads like the opposite of what it does. No -f, so an image
|
|
# any container still holds - including the one kept for rollback -
|
|
# is refused rather than taken away from it.
|
|
prune_old_images() {
|
|
REPO="${IMG%:*}"
|
|
LIVE=$(sudo docker inspect -f '{{.Image}}' "$NAME" 2>/dev/null | sed 's/^sha256://' | cut -c1-12)
|
|
[ -n "$LIVE" ] || return 0
|
|
sudo docker images "$REPO" --format '{{.ID}} {{.Repository}}:{{.Tag}}' \
|
|
| grep -v "^$LIVE" \
|
|
| tail -n +3 \
|
|
| awk '{print $2}' \
|
|
| xargs -r -n1 sudo docker rmi >/dev/null 2>&1 || true
|
|
}
|
|
|
|
sudo docker login --username=${{ secrets.DOCKER_USERNAME }} registry.ap-northeast-1.aliyuncs.com --password=${{ secrets.DOCKER_PASSWORD }}
|
|
# Before the pull, not only after a successful deploy. The pull
|
|
# is the first thing here that needs space and it is where a full
|
|
# disk stops this script, so a cleanup that only runs afterwards
|
|
# never runs on the host that needs it: rerunning the workflow
|
|
# fails at the same pull, and the disk has to be cleared by hand.
|
|
prune_old_images
|
|
sudo docker pull "$IMG" || { echo "拉取镜像失败,中止部署"; exit 1; }
|
|
|
|
if sudo docker ps -a --format '{{.Names}}' | grep -qx "$NAME"; then
|
|
sudo docker rm -f "$PREV" >/dev/null 2>&1 || true
|
|
sudo docker rename "$NAME" "$PREV"
|
|
# --timeout, because the default is 10 seconds and the process
|
|
# spends drain + server + cleanup from extend.shutdown before it
|
|
# exits - 8 seconds out of the box, and more for anyone who
|
|
# configures a drain window. Past the deadline docker sends
|
|
# SIGKILL and the cleanup callbacks are cut off part-way through.
|
|
# checksilent's docker-stop-cuts-shutdown-short check compares
|
|
# this number against config/settings.yml.
|
|
sudo docker stop --timeout 30 "$PREV" >/dev/null
|
|
fi
|
|
|
|
sudo docker run -d -p 8000:8000 --name "$NAME" "$IMG" \
|
|
sh -c '/main migrate -c /config/settings.yml && exec /main server -c /config/settings.yml'
|
|
|
|
ok=0
|
|
for i in $(seq 1 20); do
|
|
sleep 3
|
|
code=$(curl -s -o /dev/null -w '%{http_code}' -m 5 http://127.0.0.1:8000/api/v1/captcha 2>/dev/null || true)
|
|
if [ "$code" = "200" ] && sudo docker logs "$NAME" 2>&1 | grep -q 'connect success'; then
|
|
ok=1; echo "健康检查通过(第 $i 次探测)"; break
|
|
fi
|
|
done
|
|
|
|
if [ "$ok" = "1" ]; then
|
|
sudo docker rm -f "$PREV" >/dev/null 2>&1 || true
|
|
|
|
# Again, so the image this deployment replaced falls out of the
|
|
# window rather than waiting for the next deployment to notice.
|
|
prune_old_images
|
|
else
|
|
echo "健康检查失败,回滚到上一版本"
|
|
sudo docker logs --tail 40 "$NAME" 2>&1 || true
|
|
sudo docker rm -f "$NAME" >/dev/null 2>&1 || true
|
|
if sudo docker ps -a --format '{{.Names}}' | grep -qx "$PREV"; then
|
|
sudo docker rename "$PREV" "$NAME"
|
|
sudo docker start "$NAME" >/dev/null
|
|
echo "已恢复"
|
|
fi
|
|
exit 1
|
|
fi
|