# 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 是哪个,线上跑的就是哪个。 # # ⚠️ **不能用开发机 PATH 里的 bun。** pacman 装的 bun 是 Arch 自己编的,动态链接系统的 # ICU(libicui18n.so.78),编出来的 oj2-api 到容器里就报找不到这个库 —— 2026-11-03 # 第一次跑就撞上了,好在迁移那步先炸、容器没动。官方发布的 bun 自带 ICU, # 只依赖 libc,和 GitHub 上 setup-bun 装的是同一个东西。 # # 所以这里按 packageManager 的版本下载官方二进制(核 sha256),缓存在 # ~/.cache/oj2-deploy/ 下,同一个版本只下一次;再把它放到 PATH 最前面, # 后面的 bun install / bunx 都走它。 - name: 装 bun run: | ver=$(sed -nE 's/.*"packageManager": *"bun@([^"]+)".*/\1/p' package.json) [ -n "$ver" ] || { echo "::error::package.json 里读不到 packageManager 的 bun 版本"; exit 1; } dir="$HOME/.cache/oj2-deploy/bun-v$ver" if [ ! -x "$dir/bun" ]; then tmp=$(mktemp -d) base="https://github.com/oven-sh/bun/releases/download/bun-v$ver" curl -fsSL --retry 3 -o "$tmp/bun-linux-x64.zip" "$base/bun-linux-x64.zip" curl -fsSL --retry 3 -o "$tmp/SHASUMS256.txt" "$base/SHASUMS256.txt" (cd "$tmp" && grep ' bun-linux-x64.zip$' SHASUMS256.txt | sha256sum -c -) unzip -q "$tmp/bun-linux-x64.zip" -d "$tmp" mkdir -p "$dir" install -m755 "$tmp/bun-linux-x64/bun" "$dir/bun" rm -rf "$tmp" fi [ "$("$dir/bun" --version)" = "$ver" ] || { echo "::error::$dir/bun 的版本不是 $ver"; exit 1; } echo "$dir" >> "$GITHUB_PATH" echo "用 $dir/bun($ver)" - name: 装依赖 run: bun install --frozen-lockfile # 输出到 dist/oj2-api,正是 Dockerfile 里 artifacts-prebuilt 阶段要的位置。 # 编完先看它链接了什么:官方 bun 编出来的只依赖 libc 那几个,多出任何一个库 # (比如 libicu)都说明用错了 bun,到容器里一定起不来。在这里拦下, # 别等服务器上构建完镜像、迁移时才炸 —— 那时 oj2-api:latest 已经被换成坏镜像了。 - name: 编后端 run: | bun run --filter '@oj2/api' build ldd dist/oj2-api extra=$(ldd dist/oj2-api | awk '{print $1}' | grep -vE '^(linux-vdso\.so\.1|libc\.so\.6|libpthread\.so\.0|libdl\.so\.2|libm\.so\.6|/lib64/ld-linux-x86-64\.so\.2)$' || true) [ -z "$extra" ] || { echo "::error::dist/oj2-api 多链接了这些库,容器里没有:$extra"; exit 1; } # 默认 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"