From f1c4e89ec5202397acaaa510825181d193e29bc2 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Tue, 3 Nov 2026 11:08:03 +0800 Subject: [PATCH] =?UTF-8?q?chore(=E9=83=A8=E7=BD=B2):=20push=20origin=20?= =?UTF-8?q?=E5=B0=B1=E7=94=B1=E5=BC=80=E5=8F=91=E6=9C=BA=E7=BC=96=E5=A5=BD?= =?UTF-8?q?=E4=BC=A0=E4=B8=8A=E5=8E=BB=EF=BC=8C=E4=B8=8D=E5=86=8D=E7=BB=95?= =?UTF-8?q?=E9=81=93=E6=B5=B7=E5=A4=96=EF=BC=8C=E6=99=9A=E4=B8=8A=E4=B9=9F?= =?UTF-8?q?=E6=98=AF=E4=B8=80=E5=88=86=E9=92=9F=E5=B7=A6=E5=8F=B3=E4=B8=8A?= =?UTF-8?q?=E7=BA=BF?= 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 | 124 +++++++++++++++++++++++++++++++++++ .github/workflows/deploy.yml | 33 +++++----- CLAUDE.md | 6 +- docker/deploy.sh | 8 +-- docs/database.md | 4 +- docs/deploy.md | 61 +++++++++++++---- 6 files changed, 198 insertions(+), 38 deletions(-) create mode 100644 .gitea/workflows/deploy.yml diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..add2d33 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,124 @@ +# push 到 origin(git.xuyue.cc)的 main 就部署:在开发机上编好产物 → rsync 到服务器 → +# 服务器上跑 docker/deploy.sh --prebuilt 起栈。 +# +# 跑在哪:开发机上的 gitea-runner(host 模式,标签 oj2-deploy),不在服务器上。 +# - 服务器只有 2 核,编一次要 5 分钟左右、光前端就 160 秒,编的时候判题会排队。 +# - 原来在 GitHub 上编,海外 → 服务器晚高峰只有十几 KB/s,rsync 一次要五六分钟; +# 开发机 → 服务器实测 5MB 约 1 秒。 +# 代价:**push 时开发机得开着。** 没开的话任务在 Gitea 里排队,开机后 runner 连上就接着跑。 +# runner 的装法和配置见 docs/deploy.md「部署 runner」。 +# +# host 模式直接用开发机自己的东西:PATH 里的 bun、~/.ssh/config 里的 `debian`(root@服务器) +# 和 git.xuyue.cc 的 SSH 访问。所以这里**不需要配任何 secret**,也不用 actions/checkout +# (它要去 GitHub 拉 action,在国内反而是最慢的一步)。 +# +# 有了 .gitea/workflows 之后,Gitea 不再读 .github/workflows;GitHub 那份改成了只能手动 +# 触发的备用路线(开发机不在时用),push 不会在两边各部署一次。 +name: Deploy + +on: + push: + branches: + - main + # 手动触发时可以放行破坏性迁移。push 触发拿不到这个 input,值恒为空 —— + # 也就是说**自动部署永远不会执行 DROP TABLE 之类的迁移**,只会停在闸门上把 + # 工作流判红。这是有意的:那种改动得有人先确认备份。 + workflow_dispatch: + inputs: + allow_destructive: + description: "放行破坏性迁移(DROP TABLE / DROP COLUMN / ALTER TYPE …)。勾之前先确认已备份数据库。" + type: boolean + default: false + +# 两次 push 挨得近时,别让两个 deploy.sh 在服务器上同时起栈。 +# runner 的 capacity 也是 1,这里再保一道。 +concurrency: + group: deploy + cancel-in-progress: false + +env: + REMOTE: debian # 开发机 ~/.ssh/config 里的主机名 + REMOTE_DIR: /root/OJDeploy/OJ2 + +jobs: + deploy: + runs-on: oj2-deploy + timeout-minutes: 20 + steps: + # 工作目录可能是上一次留下的,所以 fetch 之后强制切过去再清掉没跟踪的文件 + # (node_modules 留着,bun install 会对齐它)。 + - name: 取代码 + env: + SHA: ${{ gitea.sha }} + REPO: ${{ gitea.repository }} + run: | + git init -q . + git fetch -q --depth 1 "git@git.xuyue.cc:$REPO.git" "$SHA" + git checkout -q --force FETCH_HEAD + git clean -ffdxq -e node_modules + git log --oneline -1 + + # 单二进制把运行时一起打进去:开发机的 bun 是几,线上跑的就是几。 + # 开发机的 bun 是 pacman 装的,会跟着系统升级走,对不上就别部署。 + - name: 核对 bun 版本 + run: | + want=$(sed -nE 's/.*"packageManager": *"bun@([^"]+)".*/\1/p' package.json) + have=$(bun --version) + echo "package.json 要 $want,本机是 $have" + [ "$want" = "$have" ] || { echo "::error::bun 版本对不上:要么把本机的 bun 换成 $want,要么照 CLAUDE.md 升级 packageManager 那三处"; exit 1; } + + - name: 装依赖 + run: bun install --frozen-lockfile + + # 输出到 dist/oj2-api,正是 Dockerfile 里 artifacts-prebuilt 阶段要的位置。 + - name: 编后端 + run: bun run --filter '@oj2/api' build + + # 默认 production 模式,读 apps/web/.env.production(在版本库里)。 + - name: 编前端 + run: cd apps/web && bun run build + + # 格式、lint、前后端类型检查、路由遮蔽、AST 节点,有一项不过就不部署。 + # 必须排在「编前端」之后:前端类型检查要 unplugin 生成的 auto-imports.d.ts / + # components.d.ts,它们不进版本库,vite build 时才生成。 + - name: 检查 + run: bunx vp run verify + + # 排除项和 --no-owner --no-group 的理由见 .github/workflows/deploy.yml 同名一步, + # 一条都不能少(少了 data 就是删库,少了 docker/.env* 下一步自检直接 die)。 + # -c 按内容比:每次 checkout 出来的 mtime 都是「刚才」,按 mtime 比会把 + # 600 多个没改的文件逐个走一遍增量。 + # --delay-updates 全部传完才一起换上去,中途断了服务器上不会留下半新半旧的源码。 + # --timeout 60 秒没有数据进出就算断了,交给循环重试,半截文件接着用。 + - name: 同步到服务器 + timeout-minutes: 10 + run: | + for attempt in 1 2 3; do + rsync -a -c --no-owner --no-group --delete-delay --delay-updates \ + --compress --compress-choice=zstd \ + --timeout=60 --stats --human-readable \ + -e "ssh -o BatchMode=yes -o ServerAliveInterval=15 -o ServerAliveCountMax=4" \ + --exclude node_modules \ + --exclude .git \ + --exclude data \ + --exclude 'docker/.env*' \ + ./ "$REMOTE:$REMOTE_DIR/" && exit 0 + echo "::warning::rsync 第 $attempt 次没传完,10 秒后重试" + sleep 10 + done + exit 1 + + # deploy.sh 自检不过 / 迁移被拦 / 容器没起来 / 冒烟发现题目数是 0,都会非零退出。 + # 破坏性迁移会被拦下并退出 4,除非手动触发时勾了 allow_destructive。 + # + # 放行与否在 shell 里按字符串 "true" 判:Gitea 的 inputs 是布尔还是字符串, + # "false" 这个非空字符串都不会被当成放行(GitHub 那份踩过 github.event.inputs 的坑)。 + - name: 起栈 + env: + ALLOW_INPUT: ${{ inputs.allow_destructive }} + run: | + allow='' + [ "$ALLOW_INPUT" = "true" ] && allow=1 + echo "放行破坏性迁移:${allow:-否}" + ssh -o BatchMode=yes -o ServerAliveInterval=15 -o ServerAliveCountMax=8 "$REMOTE" \ + "cd $REMOTE_DIR && OJ2_ALLOW_DESTRUCTIVE='$allow' docker/deploy.sh --prebuilt" diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 93c21ef..fb8f86e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,19 +1,20 @@ -# 在 runner 上编好产物 → rsync 到服务器 → 服务器上跑 docker/deploy.sh 起栈。 +# 备用部署路线:**只能手动触发**(Actions 页面 → Deploy → Run workflow)。 +# 平时的部署是 push origin 时由开发机上的 gitea-runner 跑 .gitea/workflows/deploy.yml, +# 这份只在开发机不在、又急着上线时用。 # -# 为什么产物在这里编:服务器性能差,镜像 builder 阶段(1238 个包的 bun install + -# bun compile + vite build)首次约 5 分钟,光前端就 160s。runner 编完传过去, -# 服务器那边的镜像构建只剩几条 COPY。 +# 为什么不再 push 就跑:GitHub 的 runner 在海外,到服务器晚高峰只有十几 KB/s, +# rsync 一次要五六分钟(后端改一行,--bytecode 那段约 6MB 字节码就整段重写,增量省不掉)。 +# 而 origin 会镜像到 github,两边都 push 触发的话每次会部署两遍,两边的并发锁又不通。 +# +# 流程:在 runner 上编好产物 → rsync 到服务器 → 服务器上跑 docker/deploy.sh 起栈。 +# 产物不在服务器上编,是因为服务器性能差,镜像 builder 阶段(1238 个包的 bun install + +# bun compile + vite build)首次约 5 分钟,光前端就 160s。 # # 服务器自己编的能力**没有砍掉**:docker/deploy.sh 不带 --prebuilt 就是原来的 -# 行为(ARTIFACTS=build),只要机器上有 docker 就能手动部署,不依赖这个工作流。 +# 行为(ARTIFACTS=build),只要机器上有 docker 就能手动部署,不依赖任何工作流。 # -# 触发的是 push 到 **github** 这个 remote: -# -# git push github main -# -# origin(git.xuyue.cc)是 Gitea,仓库里没有 .gitea/workflows 时它的 Actions 会退回来 -# 读这份 .github/workflows,每次 push origin 都会把部署再跑一遍 —— 那边不需要。 -# 所以下面 job 上卡了 server_url:只在 GitHub 上跑,Gitea 上直接跳过。 +# job 上卡了 server_url:只在 GitHub 上跑。仓库有了 .gitea/workflows 之后 Gitea 本来就 +# 不再读这份,这道闸留着防万一。 # # 仓库 Settings → Secrets 需要两个: # HOST 服务器地址 @@ -21,12 +22,8 @@ name: Deploy on: - push: - branches: - - main - # 手动触发时可以放行破坏性迁移。push 触发拿不到这个 input,值恒为空 —— - # 也就是说**自动部署永远不会执行 DROP TABLE 之类的迁移**,只会停在闸门上把 - # 工作流判红。这是有意的:那种改动得有人先确认备份。 + # 手动触发时可以放行破坏性迁移;不勾就停在闸门上把工作流判红。 + # 那种改动得有人先确认备份。 workflow_dispatch: inputs: allow_destructive: diff --git a/CLAUDE.md b/CLAUDE.md index 820a6c6..59e28a4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -85,9 +85,9 @@ references 的壳,真正的配置在 `tsconfig.app.json`),而 `vite build` ## 提交 -**直接在 `main` 上干活,不开特性分支。** 注意 push 到 `github` 这个 remote 的 `main` -会触发 CI 部署;而 `origin` 会镜像到 `github`,**平时 push `origin` 通常几秒后也就部署了** -(偶尔不跟,要 `git ls-remote github` 核实)。见 `docs/deploy.md`。 +**直接在 `main` 上干活,不开特性分支。** 注意 **push `origin` 的 `main` 就会部署**: +开发机上的 gitea-runner 跑 `.gitea/workflows/deploy.yml`,进度在 git.xuyue.cc 的 Actions 页面。 +GitHub 那份只剩手动触发的备用路线。见 `docs/deploy.md`。 提交信息用中文,格式 `type(范围): 从用户视角描述现象`,例如 `fix(流程图): 「重新判题」从来没成功过,而且会把原来的评分清掉`。 diff --git a/docker/deploy.sh b/docker/deploy.sh index 6658dee..c7a5f16 100755 --- a/docker/deploy.sh +++ b/docker/deploy.sh @@ -12,8 +12,8 @@ # 默认(不带 --prebuilt)是**服务器自己编**:只要有 docker 就能跑,不依赖 CI, # 也不依赖别处传产物过来。代价是这台机器性能差,builder 阶段首次约 5 分钟。 # -# --prebuilt 则要求 dist/oj2-api 和 apps/web/dist 已经在这个目录里(GitHub -# Actions 编好后 rsync 过来的),镜像构建只剩几条 COPY。产物缺失会在自检就拦下。 +# --prebuilt 则要求 dist/oj2-api 和 apps/web/dist 已经在这个目录里(开发机上的 +# gitea-runner 编好后 rsync 过来的),镜像构建只剩几条 COPY。产物缺失会在自检就拦下。 # # 代码怎么上到服务器不归它管。没有 git remote 的话,在本机: # @@ -25,7 +25,7 @@ # 而 .dockerignore 末尾为 prebuilt 开的例外(`!apps/web/dist`)让它照样进构建上下文 # ——于是本机重编一次前端,服务器上 `COPY apps/web` 那层的缓存键就变了,明明没改 # 源码也要重编 160s。CI 那条路(--prebuilt)反过来必须传产物,它自己的 rsync 在 -# .github/workflows/deploy.yml 里,别照抄这条。 +# .gitea/workflows/deploy.yml 里,别照抄这条。 # # 前提:docker/.env 已经填好(照 docker/.env.example 拷一份再填,拓扑见 docs/deploy.md)。 @@ -67,7 +67,7 @@ if [ "${ARTIFACTS:-build}" = prebuilt ]; then for f in dist/oj2-api apps/web/dist/index.html; do [ -e "$f" ] || die "--prebuilt 需要现成产物,但 $f 不存在。 -要么让 CI 把产物 rsync 过来(.github/workflows/deploy.yml), +要么让 CI 把产物 rsync 过来(.gitea/workflows/deploy.yml), 要么去掉 --prebuilt 让这台机器自己编(慢,但不依赖任何外部环节)。" done ok "用现成产物($(du -sh dist/oj2-api apps/web/dist 2>/dev/null | tr '\n' ' '))" diff --git a/docs/database.md b/docs/database.md index 162147e..19602fc 100644 --- a/docs/database.md +++ b/docs/database.md @@ -47,8 +47,8 @@ CREATE INDEX CONCURRENTLY "xxx_idx" ON "submission" USING btree ("language"); 掉数据,拦了只会让人习惯性带上放行开关。 1. 服务器上手工部署:`OJ2_ALLOW_DESTRUCTIVE=1 docker/deploy.sh`。 -2. CI(`.github/workflows/deploy.yml`):**必须先手工触发**并在 `workflow_dispatch` 上勾 - `allow_destructive`。push 触发拿不到这个 input,值恒为空 —— 也就是说**自动部署永远 +2. CI(`.gitea/workflows/deploy.yml`,备用的 `.github/workflows/deploy.yml` 同理): + **必须先手工触发**并在 `workflow_dispatch` 上勾 `allow_destructive`。push 触发拿不到这个 input,值恒为空 —— 也就是说**自动部署永远 不会执行破坏性迁移**,只会停在闸门上把工作流判红。这是有意的:那种改动得有人先确认备份。 3. 先单跑迁移把结构推到位,再 push 代码:迁移一旦记进 `drizzle.__drizzle_migrations` 就不会再跑,后续自动部署里它已不是 pending,自然不触发闸门。多环境共库时 diff --git a/docs/deploy.md b/docs/deploy.md index a19b3d0..9396418 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -55,23 +55,62 @@ docker exec -it oj-postgres psql -U onlinejudge -d postgres -c '\password online ## 上线 -### 服务器:push 就部署 +### 服务器:push origin 就部署 -`.github/workflows/deploy.yml` —— 在 runner 上编好产物、跑一遍 `bunx vp run verify` -(格式、lint、前后端类型检查、路由遮蔽、AST 节点,**不过就不部署**)、rsync 到服务器、 -在服务器上跑 `docker/deploy.sh --prebuilt`。触发的是 push 到 **github** 这个 remote -(`origin` 是 `git.xuyue.cc`,它会镜像到 github,所以**平时 push `origin` 通常几秒后 -也就部署了**,偶尔不跟、要 `git ls-remote github` 核实。Gitea 自己不跑这份工作流 —— 没有 -`.gitea/workflows` 时它的 Actions 会退回来读这份工作流,所以 job 上卡了 -`github.server_url == 'https://github.com'`,在 Gitea 上直接跳过): +`.gitea/workflows/deploy.yml` —— push 到 `origin`(`git.xuyue.cc`,Gitea)的 `main` 时, +由**开发机上的 gitea-runner** 编好产物、跑一遍 `bunx vp run verify`(格式、lint、前后端类型 +检查、路由遮蔽、AST 节点,**不过就不部署**)、rsync 到服务器、在服务器上跑 +`docker/deploy.sh --prebuilt`。 ```bash -git push github main +git push origin main # 进度在 git.xuyue.cc/xuyue/OJ2/actions ``` -产物在 runner 上编是因为服务器性能差(首次构建约 5 分钟,光前端就 160s)。 +**push 时开发机得开着。** 没开的话任务在 Gitea 里排队,开机后 runner 连上就接着跑。 + +为什么在开发机上编、不在服务器或 GitHub 上编: + +| 在哪编 | 问题 | +|---|---| +| 服务器 | 只有 2 核,首次构建约 5 分钟、光前端就 160s,编的时候判题排队 | +| GitHub | runner 在海外,到服务器晚高峰只有十几 KB/s。后端改一行,`--bytecode` 那段约 6MB 字节码就整段重写,一次 rsync 要五六分钟 | +| 开发机 | 16 核,编加检查不到一分钟;到服务器实测 5MB 约 1 秒 | + **服务器自己编的能力没有砍掉**:不带 `--prebuilt` 就是原来的行为,只要有 docker -就能手动部署,不依赖 CI。 +就能手动部署,不依赖任何 runner。 + +#### 备用:GitHub 手动触发 + +`.github/workflows/deploy.yml` 还在,但**只能手动触发**(github.com 上 Actions → Deploy → +Run workflow),开发机不在、又急着上线时用。它不再 push 就跑,因为 `origin` 会镜像到 +`github`,两边都自动的话每次会部署两遍,两边的并发锁又不通。需要的 secret(`HOST` / `KEY`) +写在那份文件头部。 + +#### 部署 runner + +开发机上的 runner 是 host 模式:直接用本机的 bun、`~/.ssh/config` 里的 `debian`(root@服务器) +和 git.xuyue.cc 的 SSH 访问,所以工作流里**不需要任何 secret**,也不用 `actions/checkout`。 + +| | | +|---|---| +| 程序 | `pacman -S gitea-runner`(系统级的 `gitea-runner.service` 不开,它用单独的账号跑) | +| 服务 | `~/.config/systemd/user/gitea-runner.service`,`systemctl --user status gitea-runner` | +| 配置 | `~/.local/share/gitea-runner/config.yaml`(标签 `oj2-deploy:host`,capacity 1) | +| 注册 | 同目录的 `.runner`;只给 `xuyue/OJ2` 这一个仓库用 | +| 日志 | `journalctl --user -u gitea-runner -f`;每次部署的日志在 Gitea 的 Actions 页面 | + +**bun 版本要对得上**:开发机的 bun 是 pacman 装的,会跟着系统升级走;单二进制会把运行时 +打进去,所以工作流第一步就核对 `bun --version` 和 `packageManager`,对不上直接判红。 + +换一台机器跑 runner:装 `gitea-runner`,在服务器上生成注册令牌,再照上面那份配置注册: + +```bash +ssh debian 'docker exec -u git gitea gitea --config /data/gitea/conf/app.ini actions generate-runner-token --scope xuyue/OJ2' +gitea-runner register --no-interactive --config config.yaml --instance https://git.xuyue.cc --token <令牌> --name <名字> +``` + +那台机器还得能 `ssh debian`、能 `git fetch git@git.xuyue.cc:xuyue/OJ2.git`(都不能要密码)。 +旧的 runner 在 Gitea 仓库设置 → Actions → Runners 里删掉。 ### 手工部署(机房、或 CI 不可用时)