From f242308a3be90fedfa821a5fb1d9228f40fbf2e9 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Tue, 3 Nov 2026 13:32:21 +0800 Subject: [PATCH] =?UTF-8?q?=E9=83=A8=E7=BD=B2=EF=BC=9Aupdate.sh=20?= =?UTF-8?q?=E6=94=B9=E7=94=B1=20deploy/update-bg.sh=20=E5=9C=A8=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E5=99=A8=E4=B8=8A=E5=90=8E=E5=8F=B0=E8=B7=91=EF=BC=88?= =?UTF-8?q?setsid=EF=BC=89=EF=BC=8CSSH=20=E6=96=AD=E4=BA=86=E5=AE=83?= =?UTF-8?q?=E7=85=A7=E6=A0=B7=E8=B7=91=E5=AE=8C=E3=80=822026-11-03=20?= =?UTF-8?q?=E5=BC=80=E5=8F=91=E6=9C=BA=E4=B8=8A=20FlClash=20=E5=88=87?= =?UTF-8?q?=E6=8D=A2=E9=85=8D=E7=BD=AE=E9=87=8D=E5=BB=BA=E4=BA=86=20TUN=20?= =?UTF-8?q?=E7=BD=91=E5=8D=A1=EF=BC=8CSSH=20=E6=AD=A3=E5=A5=BD=E6=96=AD?= =?UTF-8?q?=E5=9C=A8=20docker=20compose=20up=20=E4=B8=AD=E9=97=B4=EF=BC=9A?= =?UTF-8?q?=E6=97=A7=E5=AE=B9=E5=99=A8=E5=88=A0=E4=BA=86=E3=80=81=E6=96=B0?= =?UTF-8?q?=E5=AE=B9=E5=99=A8=E5=8F=AA=E5=88=9B=E5=BB=BA=E6=B2=A1=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=EF=BC=8C=E7=BD=91=E7=AB=99=E5=81=9C=E4=BA=86=2040=20?= =?UTF-8?q?=E5=88=86=E9=92=9F=E3=80=82=E8=BE=93=E5=87=BA=E5=86=99=E5=88=B0?= =?UTF-8?q?=20/var/tmp/iot=5Fsim-update.log=EF=BC=8C--follow=20<=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E5=8F=B7>=20=E6=8E=A5=E7=9D=80=E7=9C=8B=E8=BE=93?= =?UTF-8?q?=E5=87=BA=E3=80=81=E9=80=80=E5=87=BA=E7=A0=81=E5=92=8C=20update?= =?UTF-8?q?.sh=20=E4=B8=80=E6=A0=B7=EF=BC=8C=E8=BF=99=E4=B8=AA=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E7=9A=84=E9=83=A8=E7=BD=B2=E8=BF=98=E6=B2=A1=E5=BC=80?= =?UTF-8?q?=E5=A7=8B=E6=97=B6=E8=BF=94=E5=9B=9E=2075=EF=BC=9B=E4=B8=8A?= =?UTF-8?q?=E4=B8=80=E6=AC=A1=E8=BF=98=E5=9C=A8=E8=B7=91=E5=B0=B1=E7=AD=89?= =?UTF-8?q?=E5=AE=83=E7=BB=93=E6=9D=9F=E3=80=82Gitea=20=E7=9A=84"=E8=B5=B7?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1"ssh=20=E8=BF=94=E5=9B=9E=20255=20=E5=B0=B1?= =?UTF-8?q?=2010=20=E7=A7=92=E5=90=8E=E9=87=8D=E8=BF=9E=20--follow?= =?UTF-8?q?=EF=BC=8C75=20=E5=B0=B1=E9=87=8D=E6=96=B0=E5=BC=80=E5=A7=8B?= =?UTF-8?q?=EF=BC=8C=E6=9C=80=E5=A4=9A=206=20=E6=AC=A1=E3=80=82GitHub=20?= =?UTF-8?q?=E5=A4=87=E7=94=A8=E9=83=A8=E7=BD=B2=E4=B9=9F=E6=94=B9=E7=94=A8?= =?UTF-8?q?=20update-bg.sh=E3=80=82=E9=83=A8=E7=BD=B2=E8=AF=B4=E6=98=8E?= =?UTF-8?q?=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .gitea/workflows/deploy.yml | 21 +++++++++++-- .github/workflows/ci.yml | 2 +- deploy/README.md | 3 +- deploy/update-bg.sh | 63 +++++++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 4 deletions(-) create mode 100755 deploy/update-bg.sh 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 "$@"