Compare commits

..

7 Commits

Author SHA1 Message Date
wangjia f8e870fd6a fix(ci): backup 大小 sanity 阈值 50k→1k(小库压缩后 <50k 被误删)
远端 .backup 出的库 gzip 后可能 <50KB,原 >50k 检查把有效备份当'异常小'删了。
空 gzip 仅 ~20 字节,>1k 足以区分空/断流与有效备份。
2026-07-06 15:34:18 +08:00
wangjia 84b042394d ci(backup): 备份步骤输出 tee 到 NAS _last-run.log(便于排障) 2026-07-06 15:32:10 +08:00
wangjia e557a94ed0 fix(ci): lib-ssh 加 IdentitiesOnly+BatchMode(修 Too many auth failures)
CI ssh 到 pangolin1 报 Too many authentication failures + 回落密码提示:ssh 把
容器默认/agent key 也一并递上,撞 MaxAuthTries。加 IdentitiesOnly=yes 只用 -i key,
BatchMode=yes 纯非交互。影响 deploy-server / backup 等所有走 lib-ssh 的部署。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 15:05:44 +08:00
wangjia e1a17acbe4 feat(ci): SQLite 每日备份到 NAS(异地容灾,#26/F2)
backup-db.sh:ssh pangolin1 → sqlite3 online .backup 取 WAL 一致快照 + 远端
integrity_check → gzip 流回 → 存 NAS /volume1/docker/backups/pangolin,保留 30 天。
backup.yml:每日 02:00(北京)cron + 手动,ubuntu-latest,复用 DEPLOY_SSH_KEY,
Telegram 通知。异地=家里 NAS≠VPS 机房。不在 pangolin1 装/常驻任何东西。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 14:55:11 +08:00
wangjia 3efd67297e ci(server): 编译/测试直接在 runner 跑,去嵌套 docker(修 DinD;go 工具链自动下载)
compile-backend/test.sh 原在 golang:1.25 容器里跑,job 容器内 $PWD 在宿主不存在
→ DinD 挂载失败。改直接跑;go.mod 要求 1.25.10,靠 Go 工具链经 goproxy.cn 自动拉取。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 14:40:09 +08:00
wangjia f6e565ffdc ci(site): 官网构建/部署直接在 runner 跑,去掉嵌套 docker(修 DinD 挂载)
Deploy Site / deploy-site (push) Successful in 1m23s
job 容器内 $PWD(/workspace/...)在宿主不存在,docker run -v $PWD 挂载失败。
runner 镜像 catthehacker/ubuntu:act-latest 自带 node/npx,直接 bash 跑即可。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 14:34:12 +08:00
wangjia ba5f71b716 ci: runs-on nas → ubuntu-latest(匹配 nas runner 实际 label)
Deploy Site / deploy-site (push) Failing after 1m36s
nas 上的 forgejo-act-runner 注册 label 是 ubuntu-latest(非 nas),导致所有
runs-on: nas 的工作流无匹配 runner。改为 ubuntu-latest 对齐 runner 现状。
(备选:给 runner 加 nas 标签保约定,未采用。)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 13:33:27 +08:00
7 changed files with 128 additions and 40 deletions
+39
View File
@@ -0,0 +1,39 @@
name: DB Backup
on:
# 每日北京 02:00(= UTC 18:00)自动备份;也可手动触发。
schedule:
- cron: '0 18 * * *'
workflow_dispatch:
concurrency:
group: db-backup
cancel-in-progress: false
jobs:
backup:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Backup pangolin1 SQLite → NAS
env:
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
run: |
mkdir -p /volume1/docker/backups/pangolin
bash scripts/ci/backup-db.sh 2>&1 | tee /volume1/docker/backups/pangolin/_last-run.log
exit "${PIPESTATUS[0]}"
- name: Notify (Telegram)
if: always()
env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
run: |
. scripts/ci/notify.sh
if [ "${{ job.status }}" = "success" ]; then
notify_ok "pangolin1 SQLite 备份成功(NAS)"
else
notify_fail "pangolin1 SQLite 备份失败"
fi
+12 -11
View File
@@ -21,7 +21,7 @@ jobs:
# ── Job 1: Lint (shellcheck) ───────────────────────────────────────────── # ── Job 1: Lint (shellcheck) ─────────────────────────────────────────────
lint: lint:
name: Lint — shellcheck name: Lint — shellcheck
runs-on: nas runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -54,12 +54,13 @@ jobs:
/mnt/scripts/ci/compile-backend.sh \ /mnt/scripts/ci/compile-backend.sh \
/mnt/scripts/ci/release-server.sh \ /mnt/scripts/ci/release-server.sh \
/mnt/scripts/ci/deploy-server.sh \ /mnt/scripts/ci/deploy-server.sh \
/mnt/scripts/ci/test.sh /mnt/scripts/ci/test.sh \
/mnt/scripts/ci/backup-db.sh
# ── Job 2: OpenAPI Sync Check ──────────────────────────────────────────── # ── Job 2: OpenAPI Sync Check ────────────────────────────────────────────
openapi-check: openapi-check:
name: OpenAPI Sync Check name: OpenAPI Sync Check
runs-on: nas runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -76,7 +77,7 @@ jobs:
# ── Job 3: Redline Word Scan (脱敏) ────────────────────────────────────── # ── Job 3: Redline Word Scan (脱敏) ──────────────────────────────────────
redline-scan: redline-scan:
name: Redline Scan — 脱敏 (UI 文案) name: Redline Scan — 脱敏 (UI 文案)
runs-on: nas runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -87,7 +88,7 @@ jobs:
# ── Job 4: Flutter 客户端(分析 + 单测/组件测试)──────────────────────── # ── Job 4: Flutter 客户端(分析 + 单测/组件测试)────────────────────────
flutter-client: flutter-client:
name: Flutter — analyze + test name: Flutter — analyze + test
runs-on: nas runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -110,7 +111,7 @@ jobs:
# 规则与豁免见 ci/scan-portable-sql.sh 头注 + docs/dev-conventions.html 支柱 3。 # 规则与豁免见 ci/scan-portable-sql.sh 头注 + docs/dev-conventions.html 支柱 3。
portable-sql-scan: portable-sql-scan:
name: Portable SQL — 可移植性 (mysql/sqlite) name: Portable SQL — 可移植性 (mysql/sqlite)
runs-on: nas runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -123,7 +124,7 @@ jobs:
# (改了 design/colors_and_type.css 没重生成,或手改了生成物)。 # (改了 design/colors_and_type.css 没重生成,或手改了生成物)。
codegen-drift: codegen-drift:
name: Codegen Drift — token 生成物未漂移 name: Codegen Drift — token 生成物未漂移
runs-on: nas runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -140,7 +141,7 @@ jobs:
# -tags integration(需 docker 起 mysql/redis),见 go-integration job。 # -tags integration(需 docker 起 mysql/redis),见 go-integration job。
go-server: go-server:
name: Go — build + test name: Go — build + test
runs-on: nas runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -166,7 +167,7 @@ jobs:
# 详见 scripts/e2e-smoke.sh + server/test/e2e/。 # 详见 scripts/e2e-smoke.sh + server/test/e2e/。
e2e-smoke: e2e-smoke:
name: E2E Smoke — L4 进程级端到端 name: E2E Smoke — L4 进程级端到端
runs-on: nas runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -190,7 +191,7 @@ jobs:
# · -p 1 串行:一次只起一个 mysql 容器,避免并发把 Docker Desktop 压垮/端口资源争用。 # · -p 1 串行:一次只起一个 mysql 容器,避免并发把 Docker Desktop 压垮/端口资源争用。
go-integration: go-integration:
name: Go — integration (mysql/redis testcontainers) name: Go — integration (mysql/redis testcontainers)
runs-on: nas runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -205,7 +206,7 @@ jobs:
# tablet/desktop-stats golden 与 stats-overhaul 工作区耦合,待其合并后并入本 job。 # tablet/desktop-stats golden 与 stats-overhaul 工作区耦合,待其合并后并入本 job。
golden: golden:
name: Golden — 视觉回归 (components + auth) name: Golden — 视觉回归 (components + auth)
runs-on: nas runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
+5 -8
View File
@@ -12,18 +12,15 @@ concurrency:
jobs: jobs:
deploy-server: deploy-server:
runs-on: nas runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Compile (Go 控制面, golang:1.25 容器内构建) # 直接在 runner 跑(不嵌套 docker,避免 DinD 挂载失败)。runner 镜像的 go
run: | # 若旧于 go.mod 要求(1.25.10),Go 工具链会经 GOPROXY=goproxy.cn 自动下载对应版本。
mkdir -p "$HOME/.cache/pangolin-ci/gomod" "$HOME/.cache/pangolin-ci/gobuild" - name: Compile (Go 控制面)
docker run --rm -v "$PWD:/w" -w /w \ run: bash scripts/ci/compile-backend.sh
-v "$HOME/.cache/pangolin-ci/gomod:/go/pkg/mod" \
-v "$HOME/.cache/pangolin-ci/gobuild:/root/.cache/go-build" \
golang:1.25 bash scripts/ci/compile-backend.sh
- name: Test (go test) - name: Test (go test)
run: bash scripts/ci/test.sh server run: bash scripts/ci/test.sh server
+8 -10
View File
@@ -12,22 +12,20 @@ concurrency:
jobs: jobs:
deploy-site: deploy-site:
runs-on: nas runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Compile (Astro 官网, node:20 容器内构建) # runner 镜像 catthehacker/ubuntu:act-latest 自带 node/npx,直接跑;
run: | # 不用嵌套 docker run(job 容器内的 $PWD 在宿主上不存在,DinD 挂载会失败)。
docker run --rm -v "$PWD:/w" -w /w \ - name: Compile (Astro 官网)
-e SITE_URL=https://pangolin.yanmeiai.com \ env:
node:20 bash scripts/ci/compile-site.sh SITE_URL: https://pangolin.yanmeiai.com
run: bash scripts/ci/compile-site.sh
- name: Deploy → Cloudflare Pages - name: Deploy → Cloudflare Pages
env: env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: | run: bash scripts/ci/deploy-site.sh
docker run --rm -v "$PWD:/w" -w /w \
-e CLOUDFLARE_API_TOKEN -e CLOUDFLARE_ACCOUNT_ID \
node:20 bash scripts/ci/deploy-site.sh
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
# backup-db.sh — pangolin1 控制面 SQLite 每日备份到 NAS(异地容灾,#26 / F2)。
#
# 经 ssh 在 pangolin1 上用 sqlite3 online `.backup` 取 WAL 一致快照(不阻塞服务、
# 不像裸 cp 那样可能拿到半写状态),远端先跑 PRAGMA integrity_check 确认没坏,再把
# 快照 gzip 流回 runner,落到 NAS 备份盘(默认 /volume1/docker/backups/pangolin,
# 该盘已挂进 runner 容器)。保留 30 天。异地 = 家里 NAS ≠ VPS 机房,构成容灾。
#
# 不在 pangolin1 上安装/常驻任何东西:调度在 Gitea Actions(backup.yml),每次经
# ssh 临时跑一次 sqlite3。需环境:DEPLOY_SSH_KEY(secret)。从 repo 根调用。
set -euo pipefail
# shellcheck source=scripts/ci/lib-ssh.sh
. scripts/ci/lib-ssh.sh
BACKUP_DIR="${BACKUP_DIR:-/volume1/docker/backups/pangolin}"
mkdir -p "$BACKUP_DIR"
# 时间戳文件名(bash 内建 strftime,免 $() 命令替换)。
printf -v TS '%(%Y%m%d_%H%M%S)T' -1
DEST="${BACKUP_DIR}/pangolin_${TS}.db.gz"
# setup_ssh 内部先注册 EXIT trap 清理临时私钥,再写 key/known_hosts,导出 $SSH。
setup_ssh
echo "==> backup-db: sqlite3 .backup pangolin1(${DEPLOY_HOST})-> ${DEST}"
# 远端单引号 heredoc:$BK/$$ 在 pangolin1 上展开。以 pangolin 用户(DB 属主)跑
# online .backup + integrity_check;校验通过才 cat 回流;临时文件用完即删。
# integrity_check 失败 → 远端非零退出 → 本地 pipefail 令整条命令失败 → set -e 中止。
$SSH "root@${DEPLOY_HOST}" 'bash -s' <<'ENDSSH' | gzip > "$DEST"
set -euo pipefail
BK="/tmp/pangolin-bk-$$.db"
trap 'rm -f "$BK"' EXIT
runuser -u pangolin -- sqlite3 "/var/lib/pangolin/pangolin.db" ".backup ${BK}"
if ! runuser -u pangolin -- sqlite3 "$BK" "PRAGMA integrity_check;" | grep -q '^ok$'; then
echo "backup-db(remote): integrity_check 失败" >&2
exit 1
fi
cat "$BK"
ENDSSH
# 本地二次校验:gzip 完好 + 非空 sanity。空 gzip 仅 ~20 字节;真备份(即便很小的库)
# gzip 后也远大于 1KB,故用 >1k 区分「空/断流」与「有效备份」(不用 50k:小库压缩后可能 <50k)。
gzip -t "$DEST"
if ! find "$DEST" -size +1k -print -quit | grep -q .; then
echo "==> backup-db: 备份异常小(<1k,疑似空/断流),中止" >&2
rm -f "$DEST"
exit 1
fi
# 保留最近 30 天。
find "$BACKUP_DIR" -name 'pangolin_*.db.gz' -mtime +30 -delete
echo "==> backup-db: 备份完成 ${DEST}"
+6 -2
View File
@@ -56,8 +56,12 @@ setup_ssh() {
# abort the deploy. # abort the deploy.
ssh-keyscan -p "${DEPLOY_PORT}" -H "${DEPLOY_HOST}" >> "${SSH_KNOWN_HOSTS_FILE}" 2>/dev/null || true ssh-keyscan -p "${DEPLOY_PORT}" -H "${DEPLOY_HOST}" >> "${SSH_KNOWN_HOSTS_FILE}" 2>/dev/null || true
SSH="ssh -i ${SSH_KEY_FILE} -p ${DEPLOY_PORT} -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=${SSH_KNOWN_HOSTS_FILE}" # IdentitiesOnly=yes:只用上面 -i 指定的 key,不把 agent/默认 key 也递上去
RSYNC_SSH="ssh -i ${SSH_KEY_FILE} -p ${DEPLOY_PORT} -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=${SSH_KNOWN_HOSTS_FILE}" # (否则会触发服务器 MaxAuthTries「Too many authentication failures」)。
# BatchMode=yes:纯非交互,认证失败即退出,不回落到密码提示卡住。
_ssh_opts="-i ${SSH_KEY_FILE} -p ${DEPLOY_PORT} -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=${SSH_KNOWN_HOSTS_FILE}"
SSH="ssh ${_ssh_opts}"
RSYNC_SSH="ssh ${_ssh_opts}"
export SSH RSYNC_SSH SSH_KEY_FILE SSH_KNOWN_HOSTS_FILE DEPLOY_HOST DEPLOY_PORT export SSH RSYNC_SSH SSH_KEY_FILE SSH_KNOWN_HOSTS_FILE DEPLOY_HOST DEPLOY_PORT
echo "==> setup_ssh: key written to ${SSH_KEY_FILE}, known_hosts pinned (accept-new) for ${DEPLOY_HOST}:${DEPLOY_PORT}" echo "==> setup_ssh: key written to ${SSH_KEY_FILE}, known_hosts pinned (accept-new) for ${DEPLOY_HOST}:${DEPLOY_PORT}"
} }
+4 -9
View File
@@ -21,15 +21,10 @@ TARGET="${1:-}"
case "$TARGET" in case "$TARGET" in
server) server)
mkdir -p "$HOME/.cache/pangolin-ci/gomod" "$HOME/.cache/pangolin-ci/gobuild" # 直接在 runner 跑(不嵌套 docker,避免 DinD 挂载失败)。go.mod 要求的
echo "==> test: go test ./... (golang:1.25 容器)" # go 1.25.x 若高于 runner 自带版本,Go 工具链会经 GOPROXY(_env.sh)自动下载。
docker run --rm \ echo "==> test: go test ./..."
-v "$PWD/server:/app" -w /app \ ( cd server && go test ./... )
-v "$HOME/.cache/pangolin-ci/gomod:/go/pkg/mod" \
-v "$HOME/.cache/pangolin-ci/gobuild:/root/.cache/go-build" \
-e GOPROXY="$GOPROXY" \
golang:1.25 \
go test ./...
;; ;;
client) client)
mkdir -p "$HOME/.cache/pangolin-ci/pubcache" mkdir -p "$HOME/.cache/pangolin-ci/pubcache"