chore(部署): push origin 就由开发机编好传上去,不再绕道海外,晚上也是一分钟左右上线
Deploy / deploy (push) Failing after 37s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-11-03 11:08:03 +08:00
1 parent bcadab40b2
commit f1c4e89ec5
6 files changed
+198 -38

No files matched your search

+124
View File
@@ -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"
+15 -18
View File
@@ -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:
+3 -3
View File
@@ -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(流程图): 「重新判题」从来没成功过,而且会把原来的评分清掉`。
+4 -4
View File
@@ -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' ' '))"
+2 -2
View File
@@ -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,自然不触发闸门。多环境共库时
+50 -11
View File
@@ -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 不可用时)