diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index e70fe67..54d30b7 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -110,9 +110,26 @@ jobs: # 先把服务器上的代码切到这个提交再跑 update.sh:不然跑的是上一版的 update.sh,改了它要晚一次部署才生效。 # 重启会结束正在运行的学生程序,上课时间尽量不要推送 main。 + # + # update.sh 由 update-bg.sh 在服务器上后台跑,SSH 断了(开发机切代理、换网络)它照样跑完; + # 这边 ssh 返回 255(连接出错)就重连,用 --follow 接着看、拿退出码。 + # 75 = 服务器上这个提交的部署还没开始(断在 git fetch 那里),重新开始。 - name: 起服务 + timeout-minutes: 10 env: SHA: ${{ gitea.sha }} run: | - ssh -o BatchMode=yes -o ServerAliveInterval=15 -o ServerAliveCountMax=8 "$REMOTE" \ - "cd $REMOTE_DIR && git fetch --quiet origin && git reset --quiet --hard $SHA && deploy/update.sh $SHA --prebuilt" + remote() { ssh -o BatchMode=yes -o ConnectTimeout=20 -o ServerAliveInterval=15 -o ServerAliveCountMax=8 "$REMOTE" "$1"; } + start="cd $REMOTE_DIR && git fetch --quiet origin && git reset --quiet --hard $SHA && deploy/update-bg.sh $SHA --prebuilt" + follow="cd $REMOTE_DIR && deploy/update-bg.sh --follow $SHA" + code=0; remote "$start" || code=$? + for attempt in 1 2 3 4 5 6; do + case $code in + 255) echo "::warning::和服务器的 SSH 断了,部署在服务器上接着跑,10 秒后重连接着看" + sleep 10; code=0; remote "$follow" || code=$? ;; + 75) echo "::warning::服务器上的部署还没开始,重新开始" + code=0; remote "$start" || code=$? ;; + *) exit $code ;; + esac + done + exit $code diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68a7670..347453e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -100,4 +100,4 @@ jobs: - name: 部署(在服务器上拉代码、构建、重启) if: steps.check.outputs.configured == 'true' - run: ssh deploy-target "cd ${{ vars.DEPLOY_DIR || 'IOT_SIM' }} && deploy/update.sh ${{ github.sha }}" + run: ssh deploy-target "cd ${{ vars.DEPLOY_DIR || 'IOT_SIM' }} && deploy/update-bg.sh ${{ github.sha }}" diff --git a/deploy/README.md b/deploy/README.md index 2b24929..9e348cd 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -70,6 +70,7 @@ `.gitea/workflows/deploy.yml`:push 到 origin(git.xuyue.cc)的 main 时,**开发机上的 gitea-runner** 编 C# 运行时、网页和 `server.js`,跑服务端测试(含沙箱)、网页端测试和类型检查(有一项不过就不部署),把产物 rsync 到服务器的 `~/IOT_SIM/prebuilt/`,再在服务器上执行 `deploy/update.sh <提交号> --prebuilt`:拉代码、用现成产物构建镜像(只剩几条 COPY)、重启,等到"编译服务已就绪"才算成功。push 后一两分钟上线,进度在 git.xuyue.cc/xuyue/IOT_SIM/actions。 +- `update.sh` 由 `deploy/update-bg.sh` 在服务器上**后台**跑,SSH 断了(开发机切代理、换网络)它照样跑完,runner 重连后接着看输出、拿结果。以前它跑在 SSH 会话里,断线时正好在 `docker compose up` 中间,旧容器删了、新容器没启动,网站就停了。服务器上的部署输出在 `/var/tmp/iot_sim-update.log`。 - **重启会结束正在运行的学生程序**,上课时间尽量不要推送 main。 - **push 时开发机得开着。** 没开的话任务在 Gitea 里排队,开机后 runner 连上就接着跑;排队超过 24 小时会被作废,要在 Actions 页面重跑。 - 在 Gitea 的 Actions 页面点 Run workflow 可以手动重新部署(比如改了服务器上的 `.env` 之后)。 @@ -93,7 +94,7 @@ host 模式直接用开发机自己的东西,所以**不需要配任何 secret ### 备用:GitHub 手动触发 -`.github/workflows/ci.yml` 只在 PR 时跑测试,push 不触发(不然每次推送两边各部署一遍)。开发机不在、又急着上线时,在 github.com 的 Actions → CI/CD → Run workflow:测试通过后 SSH 到服务器执行 `deploy/update.sh <提交号>`,**服务器自己编**。这个提交要已经推到 origin(服务器从 origin 拉代码,没到时脚本会等最多约 100 秒)。 +`.github/workflows/ci.yml` 只在 PR 时跑测试,push 不触发(不然每次推送两边各部署一遍)。开发机不在、又急着上线时,在 github.com 的 Actions → CI/CD → Run workflow:测试通过后 SSH 到服务器执行 `deploy/update-bg.sh <提交号>`(后台跑 `update.sh`),**服务器自己编**。这个提交要已经推到 origin(服务器从 origin 拉代码,没到时脚本会等最多约 100 秒)。 一次性配置(在本机执行,需要已登录的 `gh`): diff --git a/deploy/update-bg.sh b/deploy/update-bg.sh new file mode 100755 index 0000000..70f48bd --- /dev/null +++ b/deploy/update-bg.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# 在服务器上执行:后台跑 deploy/update.sh,前台跟着输出,退出码和 update.sh 一样。 +# 用法:deploy/update-bg.sh <提交号> [--prebuilt] 开始部署这个提交并跟着看 +# deploy/update-bg.sh --follow <提交号> 接着看这个提交的部署(SSH 断线重连后用); +# 这个提交的部署还没开始过时退出码 75 +# +# 为什么要后台跑:以前 update.sh 直接跑在 SSH 会话里,SSH 一断(开发机网络切换、代理重启)就跟着被杀。 +# 2026-11-03 断在 docker compose up 中间:旧容器删了、新容器只创建没启动,网站停了 40 分钟。 +# 现在 update.sh 在新会话里跑(setsid),SSH 断了它照样跑完,开发机重连后用 --follow 拿结果。 +set -euo pipefail + +# 包在函数里:update.sh 会 git reset,运行中替换本文件,bash 先读完函数再执行,不受影响 +main() { + cd "$(dirname "$0")/.." + local state=/var/tmp/iot_sim-update + # state.sha:这次部署的提交 state.pid:后台进程 state.log:输出 state.exit:update.sh 的退出码(跑完才有) + + if [[ ${1:-} == --follow ]]; then + local sha=${2:?用法:deploy/update-bg.sh --follow <提交号>} + if [[ $(cat $state.sha 2>/dev/null || true) != "$sha" ]]; then + echo "服务器上没有 $sha 的部署记录(还没开始)" + exit 75 + fi + follow + fi + + local sha=${1:?用法:deploy/update-bg.sh <提交号> [--prebuilt]} + + # 上一次还在跑(比如它的 SSH 断了、又推了新的)就等它跑完,两次 compose up 不能同时跑 + local old + old=$(cat $state.pid 2>/dev/null || true) + if [[ -n $old ]] && kill -0 "$old" 2>/dev/null; then + echo "== 上一次部署($(cat $state.sha 2>/dev/null))还在跑,等它结束" + while kill -0 "$old" 2>/dev/null; do sleep 2; done + fi + + rm -f $state.pid $state.exit + echo "$sha" >$state.sha + # 后台进程自己写 pid($$),保证记下的就是会写退出码的那个进程 + setsid bash -c 'echo $$ >"$0.pid"; deploy/update.sh "$@"; echo $? >"$0.exit"' \ + "$state" "$@" >$state.log 2>&1 /dev/null || true) + if [[ -n $pid ]]; then + # update 进程结束后 tail 自己退出 + tail -n +1 -f --pid="$pid" $state.log + else + cat $state.log 2>/dev/null || true + fi + code=$(cat $state.exit 2>/dev/null || true) + if [[ -z $code ]]; then + echo "== 部署进程意外结束,没有留下退出码,看看 docker compose ps" + exit 1 + fi + exit "$code" +} + +main "$@"