mirror of
https://github.com/go-admin-team/go-admin.git
synced 2026-09-25 11:31:47 +00:00
Stopping this process takes drain + server + cleanup seconds: eight out of the box, and more for anyone who configures a drain window. Three places decide whether it gets that long, and none of them was written with it in mind. The release workflow stopped the previous container with the default deadline, which docker sets at ten seconds. The compose file - which the Makefile calls the first way to run this - set no stop_grace_period, so it took the same ten. Under either, a drain window over two seconds would have been cut off by SIGKILL part-way through the cleanup callbacks: this project's own deployments could not have run the capability it ships. The third was worse. `make run` removed the previous container with `docker rm -f`, and the force flag kills a running container outright - "uses SIGKILL", in docker's own words - with no grace at all. Restarting locally cut every shutdown short, so the drain window would never once have been reached on a developer's machine. It now stops with a deadline and then removes, which leaves what gets removed unchanged: on a container that has already stopped, stop is a no-op. So: --timeout 30 in the workflow, stop_grace_period: 30s on the compose service, and stop --timeout 30 before the removal in the Makefile. --timeout rather than --time, which docker still honours but has deprecated - it prints a warning on every use, and a deploy log that always carries a warning is one nobody reads. The three remaining `docker rm -f` calls in the workflow are left alone. Two remove containers that have already been stopped and one is the rollback path, and nothing static can tell those apart from a container that is still running - which is also why the check added next does not look at `rm -f` at all: a forced removal has no deadline to compare against. What keeps that path honest is the line above it, not a check. Thirty will drift the first time somebody raises a budget. The next commit is what notices, which is also why these comments name a check that does not exist yet.
154 lines
6.9 KiB
YAML
154 lines
6.9 KiB
YAML
name: Build
|
|
|
|
# Documentation-only changes 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.
|
|
on:
|
|
push:
|
|
branches: [ master ]
|
|
paths-ignore:
|
|
- '**.md'
|
|
- 'docs/**'
|
|
- 'LICENSE*'
|
|
- '.github/ISSUE_TEMPLATE/**'
|
|
pull_request:
|
|
branches: [ master ]
|
|
paths-ignore:
|
|
- '**.md'
|
|
- 'docs/**'
|
|
- 'LICENSE*'
|
|
- '.github/ISSUE_TEMPLATE/**'
|
|
|
|
# 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.26.5
|
|
|
|
- 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数据库以及重启服务器
|
|
#
|
|
# 配置从宿主机挂载,不使用镜像里的那份:演示站连的是托管数据库,
|
|
# 而 config/settings.demo.yml 会随仓库公开、也会打进镜像,凭据不能写在那里。
|
|
# 镜像里那份保持 sqlite,供 clone 仓库的人开箱即用。
|
|
#
|
|
# 路径本身走 secret:它不是凭据,但本仓库公开,没有理由把服务器的
|
|
# 目录结构一并公布。DEMO_CONFIG_PATH 指向宿主机上那份配置。
|
|
#
|
|
# 顺序是有意的:迁移先跑,跑不过就保持现有版本不动;
|
|
# 旧容器改名保留而不是删除,新容器不健康时能原样恢复。
|
|
# 健康检查两条都要过——HTTP 活着不代表数据库通了。
|
|
script: |
|
|
set -u
|
|
CFG="${{ secrets.DEMO_CONFIG_PATH }}"
|
|
IMG="${{ env.IMAGE_NAME_TAG }}"
|
|
NAME=go-admin-api
|
|
PREV="$NAME-prev"
|
|
|
|
test -f "$CFG" || { echo "宿主机配置缺失,中止部署"; exit 1; }
|
|
|
|
sudo docker login --username=${{ secrets.DOCKER_USERNAME }} registry.ap-northeast-1.aliyuncs.com --password=${{ secrets.DOCKER_PASSWORD }}
|
|
sudo docker pull "$IMG" || { echo "拉取镜像失败,中止部署"; exit 1; }
|
|
|
|
# 迁移用新镜像跑。失败时线上仍是旧版本配旧 schema,是自洽的;
|
|
# 硬切过去才会得到代码与表对不上的服务。
|
|
if ! sudo docker run --rm -v "$CFG":/config/settings.yml:ro "$IMG" \
|
|
/main migrate -c /config/settings.yml; then
|
|
echo "迁移失败,保持现有版本"; exit 1
|
|
fi
|
|
|
|
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 \
|
|
-v "$CFG":/config/settings.yml:ro \
|
|
--name "$NAME" "$IMG"
|
|
|
|
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
|
|
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
|