From b42a70ff3bc257f82746a655b6426bf10be87f78 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Sun, 21 Jun 2026 11:39:28 +0800 Subject: [PATCH] =?UTF-8?q?ci:=20=E4=BF=AE=E5=A4=8D=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93=E5=A4=87=E4=BB=BD=20workflow=E2=80=94=E2=80=94?= =?UTF-8?q?=E6=94=B9=E7=94=A8=20mac=20runner=E3=80=81EC2=20=E7=AB=AF?= =?UTF-8?q?=E8=AF=BB=E5=AF=86=E7=A0=81=E3=80=81=E6=9C=AC=E6=9C=BA=E8=90=BD?= =?UTF-8?q?=E7=9B=98=E3=80=81=E6=AF=8F=E6=97=A5=E5=AE=9A=E6=97=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit backup.yml 原 runs-on ubuntu-latest 无对应 runner、落盘 NAS 路径在 mac runner 不存在,长期跑不起来。 改为 runs-on mac + scripts/ci/backup-db.sh:复用 lib-forgejo 的 ssh 封装连 EC2, 在 EC2 端从 production.env 读 DSN 密码 mysqldump(零额外 secret),流回本机 ~/jiu-db-backups gzip 落盘、保留 30 天、完整性校验,恢复每日 02:00 定时 + Telegram 成败通知。 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01YZ4DskSRKsSiheQonFtQvx --- .gitea/workflows/backup.yml | 44 ++++++++++++++++++------------------- scripts/ci/backup-db.sh | 41 ++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 23 deletions(-) create mode 100644 scripts/ci/backup-db.sh diff --git a/.gitea/workflows/backup.yml b/.gitea/workflows/backup.yml index fc4dbb9..1ffc8b9 100644 --- a/.gitea/workflows/backup.yml +++ b/.gitea/workflows/backup.yml @@ -1,37 +1,35 @@ name: DB Backup on: - # 定时备份已暂停(保留手动触发)。恢复时取消下面 schedule 的注释即可。 - # schedule: - # - cron: '0 18 * * *' # UTC 18:00 = 北京时间 02:00 + # 每日定时备份(北京时间 02:00)。手动触发亦可。 + schedule: + - cron: '0 18 * * *' # UTC 18:00 = 北京时间 02:00 workflow_dispatch: +concurrency: + group: db-backup + cancel-in-progress: false + jobs: backup: - runs-on: ubuntu-latest + runs-on: mac steps: - - name: Setup SSH - run: | - mkdir -p ~/.ssh - printf '%s' "${{ secrets.EC2_SSH_KEY }}" > ~/.ssh/ec2.pem - chmod 600 ~/.ssh/ec2.pem - ssh-keyscan -H ${{ secrets.EC2_HOST }} >> ~/.ssh/known_hosts + - uses: actions/checkout@v4 - - name: Dump MySQL to NAS + - name: Dump MySQL to local backup dir env: + EC2_SSH_KEY: ${{ secrets.EC2_SSH_KEY }} EC2_HOST: ${{ secrets.EC2_HOST }} EC2_USER: ${{ secrets.EC2_USER }} - DB_PASSWORD: ${{ secrets.DB_PASSWORD }} - run: | - BACKUP_DIR=/volume1/docker/backups/jiu-db - mkdir -p $BACKUP_DIR - FILENAME="jiu_db_$(date +%Y%m%d_%H%M%S).sql.gz" - ssh -i ~/.ssh/ec2.pem ${EC2_USER}@${EC2_HOST} \ - "docker exec jiu_mysql mysqldump -uroot -p${DB_PASSWORD} jiu_db" \ - | gzip > ${BACKUP_DIR}/${FILENAME} - find $BACKUP_DIR -name "*.sql.gz" -mtime +30 -delete - echo "Saved: ${BACKUP_DIR}/${FILENAME}" + run: sh scripts/ci/backup-db.sh - - name: Cleanup SSH key + - name: Notify (Telegram) if: always() - run: rm -f ~/.ssh/ec2.pem + env: + TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }} + TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} + run: | + if [ "${{ job.status }}" = "success" ]; then ICON="✅"; LABEL="数据库备份成功"; else ICON="❌"; LABEL="数据库备份失败"; fi + curl -f -X POST "https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage" \ + -d "chat_id=${TELEGRAM_CHAT_ID}" \ + --data-urlencode "text=${ICON} 岩美 ${LABEL}" > /dev/null || true diff --git a/scripts/ci/backup-db.sh b/scripts/ci/backup-db.sh new file mode 100644 index 0000000..a016749 --- /dev/null +++ b/scripts/ci/backup-db.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# backup-db.sh — dump the production MySQL (jiu_db) from EC2 and keep a gzip +# snapshot on the mac (host) runner's local disk (~/jiu-db-backups), retaining +# the last 30 days. The DB password is read on EC2 from production.env's +# DATABASE_DSN, so no DB-password secret is needed. +# +# Requires env (same as deploy-server): EC2_SSH_KEY, EC2_HOST, EC2_USER. +set -euo pipefail + +# shellcheck source=scripts/ci/lib-forgejo.sh +. "$(dirname "$0")/lib-forgejo.sh" + +BACKUP_DIR="${HOME}/jiu-db-backups" +mkdir -p "${BACKUP_DIR}" +FILENAME="jiu_db_$(date +%Y%m%d_%H%M%S).sql.gz" +DEST="${BACKUP_DIR}/${FILENAME}" + +setup_ssh +trap teardown_ssh EXIT + +echo "==> backup-db: dumping jiu_db from ${EC2_HOST}" +# On EC2: parse the DSN password from production.env, then mysqldump the +# container to stdout; stream back over ssh and gzip locally. The heredoc is +# single-quoted, so it runs verbatim on EC2 (no local expansion). ${SSH} carries +# no -t, keeping stdout a clean dump stream. +${SSH} "${EC2_USER}@${EC2_HOST}" 'bash -s' <<'ENDSSH' | gzip > "${DEST}" +set -euo pipefail +PW=$(python3 -c 'import re;e=open("/opt/jiu/config/production.env").read();d=re.search(r"DATABASE_DSN=(.*)",e).group(1).strip().strip(chr(34)).strip(chr(39));print(re.match(r"[^:]+:([^@]+)@",d).group(1))') +exec docker exec -e MYSQL_PWD="${PW}" jiu_mysql mysqldump -uroot --single-transaction --no-tablespaces jiu_db +ENDSSH + +# Integrity + sanity + retention. +gzip -t "${DEST}" +SIZE=$(stat -f%z "${DEST}" 2>/dev/null || stat -c%s "${DEST}") +if [ "${SIZE}" -lt 100000 ]; then + echo "==> backup-db: dump suspiciously small (${SIZE} bytes), aborting" >&2 + rm -f "${DEST}" + exit 1 +fi +find "${BACKUP_DIR}" -name '*.sql.gz' -mtime +30 -delete +echo "==> backup-db: saved ${DEST} (${SIZE} bytes)"