feat(editor-api): 线上容器切「CNB 主 + Gitea 备」双推;备份补 fetch 步骤
用户定案:「editor-api 容器 采用 cnb主仓 gitea备份仓 openlist文件备份」。
三层各有各的失效模式,不能互相替代:
① 主仓 origin = CNB —— 源码 + 触发构建,依赖 CNB 平台
② 代码辅仓 gitea = 自建 Gitea —— 不受第三方平台规则约束,可 clone / 按提交追溯
③ 离线备份 = F50/OpenList —— 单一加密文件,平台全挂也能恢复
一、线上容器实测切双推(国内机 119.29.215.187)
先探测出两个关键事实:
· 国内机**能**直连自建 Gitea(/api/v1/version → 200,0.38s)
· 国内机**够不到**家里的 OpenList(192.168.0.1 私有地址,6s 超时)
→ 「文件备份」这一层只能由家庭机发起,国内机做不到(下面的 fetch 修复正因此
改动:
· /srv/blog 停在 432cf5e3,落后 origin/main 10 个提交且是其后代
→ git merge --ff-only 追平到 448b898e,**未强推**
· 挂 gitea 远端(凭据编在 URL 里,同本机做法)
· .env:PUSH_REMOTES=origin → origin,gitea;追加 GITEA_URL/USER/PASS
· docker-compose.yml 默认值同步为 ${PUSH_REMOTES:-origin,gitea}
· docker compose up -d --build 重建
验证(全部实测通过):
· 启动日志 [editor-api] 分支 main 推送远端 origin, gitea
· curl /health → "remotes":["origin","gitea"]
· 容器内推 origin ✅ / 推 gitea ✅(推临时分支 → ls-remote 确认 → 删分支,无残留)
· 三处 main 对齐 448b898e(本地 / origin / gitea);工作区干净
二、备份侧:修掉一个静默漏洞 —— 打包前必须先 fetch
git bundle create --all 取的是**本地已知** ref,其中 refs/remotes/origin/main
停在上次 fetch/pull 的位置。而这份备份跑在**家里**那台机器上,工作区并不会
随写作后台(editor-api)的发布自动更新。所以原先是:后台新发的文章**一篇都不
在备份里**,而备份照样报「成功」—— 失败是静默的。
· backup-bundle.mjs 打包前插入 git fetch --all --tags --prune
- 失败**不致命**(离线也得出得来备份)→ 降级为「用本地已有 ref 打包」+ 显著告警
- 新增 --no-fetch 可跳过;步骤号 1/6..6/6 → 0/7..6/7
- 新增一行 `快照 origin/main = <sha> <时间> <标题>` —— 恢复时第一件要确认的就是它
· backup-run.mjs:头部补三层说明;失败邮件的「常见原因」加上 fetch 降级这一条
验证:fetch 3s 拉完 origin+gitea → 打包 3.6s → 加密 1.7s → 上传 19.2s(31.5MB/s)
→ 读回 sha256 一致,快照 = 448b898e
三、bootstrap.sh:修掉另一个静默陷阱
脚本每次都会**重写** .env,而 .env 是 GITEA_PASS 的唯一落点 →
「重跑一次但没现给 GITEA_PASS」会把辅仓推送**静默关掉**(PUSH_REMOTES 降级成
origin),不报错、不提示,直到要恢复时才发现 Gitea 早就没在同步。
· 改为:命令行没给就从旧 .env 里捡回来(显式传新值仍优先)
· 并把 GITEA_URL/USER/PASS 也写进 .env(容器不读这几个键,仅作复用锚点)
· 该逻辑用 4 个用例离线验证(有/无 .env、显式覆盖、.env 里没 PASS),
并确认 set -euo pipefail 下不会被 [ -n ] && cmd 这类写法误触发退出
四、文档
· 架构总览.md:§1.2 地址地图加 editor-api 行;§2 旅程图补「两条推送入口同一套语义」;
§5.2 加「线上容器的推送目标」段(含验证方法与 pull 源只有 origin 的铁律);
§5.6 加「打包前必须 git fetch」+「覆盖范围」表(诚实列出够不到的部分);
§6 待办 #4/#10 更新、新增 #12
· editor-api/README.md:新增「落盘:写一次,存三处」
· README.md:推送说明补线上后台那一路与 fetch 说明;脚本表更新
This commit is contained in:
1 parent
4308dbe8ea
commit
d3da972c3b
6 files changed
+187
-29
No files matched your search
@@ -98,8 +98,13 @@ gitea → 自建 Gitea(23.254.236.47:3001) 代码同步辅仓:只推不
|
|||||||
|
|
||||||
- **推送**:`git pushall` —— 依次推 `origin`(CNB 主仓,触发构建)与 `gitea`(自建 Gitea 代码同步辅仓)
|
- **推送**:`git pushall` —— 依次推 `origin`(CNB 主仓,触发构建)与 `gitea`(自建 Gitea 代码同步辅仓)
|
||||||
;只推主仓用 `git push origin main`
|
;只推主仓用 `git push origin main`
|
||||||
|
- 线上写作后台(国内机 `editor-api` 容器)走同一套:`PUSH_REMOTES=origin,gitea`,
|
||||||
|
**主仓成功即算发布成功**,辅仓失败只警告不阻断
|
||||||
|
- ★ **两边同一条铁律:任何提交都必须先落到 CNB** —— pull 源只有 `origin`,
|
||||||
|
只推 `gitea` 的提交后台看不见,下次发布会因 non-fast-forward 被拒
|
||||||
- **备选异地备份**:**不走 git 远端**。本机计划任务每天 03:30 把整仓 bundle
|
- **备选异地备份**:**不走 git 远端**。本机计划任务每天 03:30 把整仓 bundle
|
||||||
加密后传到中兴 F50 上的 OpenList(见 `架构总览.md` §5.6)
|
(**打包前先 `git fetch --all`**,否则备的是过期快照)加密后传到中兴 F50 上的
|
||||||
|
OpenList(见 `架构总览.md` §5.6)
|
||||||
- **触发**:推送到 `main`;另有每日 `0 9 * * *`(北京时间)定时构建
|
- **触发**:推送到 `main`;另有每日 `0 9 * * *`(北京时间)定时构建
|
||||||
- **密钥**:全部来自 CNB 密钥仓库 `zqlit/blog-secrets`,经 `.cnb.yml` 的 `imports` 注入,
|
- **密钥**:全部来自 CNB 密钥仓库 `zqlit/blog-secrets`,经 `.cnb.yml` 的 `imports` 注入,
|
||||||
**仓库里没有任何明文密钥**
|
**仓库里没有任何明文密钥**
|
||||||
@@ -137,7 +142,7 @@ Hugo + Ying 主题。`hugo.toml` 控站点信息、永久链接、Artalk 地址
|
|||||||
| `send_mail.js` | 构建结果邮件通知(零依赖 SMTP) | **CI** |
|
| `send_mail.js` | 构建结果邮件通知(零依赖 SMTP) | **CI** |
|
||||||
| `setup-cnb-remotes.sh` | 切换/重建 git 远端(CNB 主仓 + 自建 Gitea 辅仓;顺手清掉退役远端) | 本机 |
|
| `setup-cnb-remotes.sh` | 切换/重建 git 远端(CNB 主仓 + 自建 Gitea 辅仓;顺手清掉退役远端) | 本机 |
|
||||||
| `backup-run.mjs` | 备份的推荐入口:跑 `backup-bundle` + **失败时发告警邮件** | 本机/计划任务 |
|
| `backup-run.mjs` | 备份的推荐入口:跑 `backup-bundle` + **失败时发告警邮件** | 本机/计划任务 |
|
||||||
| `backup-bundle.mjs` | 整仓 bundle → AES-256-GCM 加密 → WebDAV 传中兴 F50(见 架构总览 §5.6) | 本机/计划任务 |
|
| `backup-bundle.mjs` | `git fetch --all` → 整仓 bundle → AES-256-GCM 加密 → WebDAV 传中兴 F50(见 架构总览 §5.6) | 本机/计划任务 |
|
||||||
| `backup-task.cmd` | 上面的计划任务入口(每天 03:30;**内容必须全 ASCII**) | 计划任务 |
|
| `backup-task.cmd` | 上面的计划任务入口(每天 03:30;**内容必须全 ASCII**) | 计划任务 |
|
||||||
| `optimize_images.js` | 图片批量压缩优化 | 本机 |
|
| `optimize_images.js` | 图片批量压缩优化 | 本机 |
|
||||||
| `generate_circle_data.js` | 抓友链 RSS 生成朋友圈数据 | 本机 |
|
| `generate_circle_data.js` | 抓友链 RSS 生成朋友圈数据 | 本机 |
|
||||||
|
|||||||
@@ -23,7 +23,9 @@
|
|||||||
# GITEA_URL 默认 http://23.254.236.47:3001/zqlit/blog.git(自建 Gitea 辅仓)
|
# GITEA_URL 默认 http://23.254.236.47:3001/zqlit/blog.git(自建 Gitea 辅仓)
|
||||||
# GITEA_USER 默认 zqlit
|
# GITEA_USER 默认 zqlit
|
||||||
# GITEA_PASS 可选:Gitea 的密码 / 访问令牌。**给了才会把 gitea 加进推送远端**,
|
# GITEA_PASS 可选:Gitea 的密码 / 访问令牌。**给了才会把 gitea 加进推送远端**,
|
||||||
# 否则 PUSH_REMOTES 自动降级成只推 origin(CNB 主仓)
|
# 否则 PUSH_REMOTES 自动降级成只推 origin(CNB 主仓)。
|
||||||
|
# ★ 不传时会**自动复用 $APP_DIR/.env 里上次写下的值** ——
|
||||||
|
# 免得「重跑一次」把辅仓推送静默关掉(见脚本内注释)。
|
||||||
# BLOG_DIR /srv/blog 仓库工作区
|
# BLOG_DIR /srv/blog 仓库工作区
|
||||||
# APP_DIR /srv/editor-api compose + .env
|
# APP_DIR /srv/editor-api compose + .env
|
||||||
# TRASH_DIR /srv/editor-trash 删除文章的回收目录(故意放在仓库外)
|
# TRASH_DIR /srv/editor-trash 删除文章的回收目录(故意放在仓库外)
|
||||||
@@ -55,8 +57,6 @@ die() { printf '\033[1;31m!! %s\033[0m\n' "$*" >&2; exit 1; }
|
|||||||
: "${ADMIN_PASS:?必须设置 ADMIN_PASS(后台登录密码,账号固定 admin)}"
|
: "${ADMIN_PASS:?必须设置 ADMIN_PASS(后台登录密码,账号固定 admin)}"
|
||||||
|
|
||||||
CNB_URL="${CNB_URL:-https://cnb.cool/zqlit/blog.git}"
|
CNB_URL="${CNB_URL:-https://cnb.cool/zqlit/blog.git}"
|
||||||
GITEA_URL="${GITEA_URL:-http://23.254.236.47:3001/zqlit/blog.git}"
|
|
||||||
GITEA_USER="${GITEA_USER:-zqlit}"
|
|
||||||
BLOG_DIR="${BLOG_DIR:-/srv/blog}"
|
BLOG_DIR="${BLOG_DIR:-/srv/blog}"
|
||||||
APP_DIR="${APP_DIR:-/srv/editor-api}"
|
APP_DIR="${APP_DIR:-/srv/editor-api}"
|
||||||
TRASH_DIR="${TRASH_DIR:-/srv/editor-trash}"
|
TRASH_DIR="${TRASH_DIR:-/srv/editor-trash}"
|
||||||
@@ -66,14 +66,6 @@ GIT_AUTHOR_EMAIL="${GIT_AUTHOR_EMAIL:-editor@usj.cc}"
|
|||||||
BLOG_BASE="${BLOG_BASE:-}"
|
BLOG_BASE="${BLOG_BASE:-}"
|
||||||
PUSH_REMOTES="${PUSH_REMOTES:-origin,gitea}"
|
PUSH_REMOTES="${PUSH_REMOTES:-origin,gitea}"
|
||||||
|
|
||||||
# 没给 Gitea 凭据就只推主仓 —— 宁可少推一个辅仓,
|
|
||||||
# 也不要让每次发布都对着一个认证不过的远端空转一遍。
|
|
||||||
# (这里是有意直接覆盖成 origin:默认列表就是 origin,gitea,
|
|
||||||
# 没有 gitea 凭据等于只剩 origin;要推别的远端请连同 gitea 一起显式写全。)
|
|
||||||
if [ -z "${GITEA_PASS:-}" ]; then
|
|
||||||
PUSH_REMOTES="origin"
|
|
||||||
fi
|
|
||||||
|
|
||||||
command -v docker >/dev/null || die "没有 docker"
|
command -v docker >/dev/null || die "没有 docker"
|
||||||
docker compose version >/dev/null 2>&1 || die "没有 docker compose"
|
docker compose version >/dev/null 2>&1 || die "没有 docker compose"
|
||||||
|
|
||||||
@@ -84,6 +76,32 @@ BLOG_DIR="$(cd "$BLOG_DIR" && pwd)"
|
|||||||
APP_DIR="$(cd "$APP_DIR" && pwd)"
|
APP_DIR="$(cd "$APP_DIR" && pwd)"
|
||||||
TRASH_DIR="$(cd "$TRASH_DIR" && pwd)"
|
TRASH_DIR="$(cd "$TRASH_DIR" && pwd)"
|
||||||
|
|
||||||
|
# ---- 复用上一次部署的 Gitea 配置(★ 别删这段)----
|
||||||
|
# 为什么必须有:本脚本每次都会**重写** .env,而 .env 是 GITEA_PASS 的唯一落点。
|
||||||
|
# 于是「重跑一次 bootstrap.sh 但没现给 GITEA_PASS」会走到下面那个降级分支,
|
||||||
|
# 把 PUSH_REMOTES 静默改成 origin —— 不报错、不提示、看着一切正常,
|
||||||
|
# 直到某天要恢复时才发现 Gitea 辅仓早就没在同步了。
|
||||||
|
# 现在改成:命令行没给就从上次的 .env 里捡回来(显式传新值仍然优先)。
|
||||||
|
if [ -f "$APP_DIR/.env" ]; then
|
||||||
|
_prev() { sed -n "s/^$1=//p" "$APP_DIR/.env" 2>/dev/null | tail -1; }
|
||||||
|
GITEA_URL="${GITEA_URL:-$(_prev GITEA_URL)}"
|
||||||
|
GITEA_USER="${GITEA_USER:-$(_prev GITEA_USER)}"
|
||||||
|
GITEA_PASS="${GITEA_PASS:-$(_prev GITEA_PASS)}"
|
||||||
|
if [ -n "${GITEA_PASS:-}" ]; then
|
||||||
|
say "已从 $APP_DIR/.env 复用 GITEA_URL / GITEA_USER / GITEA_PASS"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
GITEA_URL="${GITEA_URL:-http://23.254.236.47:3001/zqlit/blog.git}"
|
||||||
|
GITEA_USER="${GITEA_USER:-zqlit}"
|
||||||
|
|
||||||
|
# 到这里还是没有 Gitea 凭据 → 只推主仓。
|
||||||
|
# 宁可少推一个辅仓,也不要让每次发布都对着一个认证不过的远端空转一遍。
|
||||||
|
# (默认列表就是 origin,gitea,没有凭据等于只剩 origin;
|
||||||
|
# 要推别的远端请连同 gitea 一起显式写全 PUSH_REMOTES。)
|
||||||
|
if [ -z "${GITEA_PASS:-}" ]; then
|
||||||
|
PUSH_REMOTES="origin"
|
||||||
|
fi
|
||||||
|
|
||||||
say "2/5 拉取/更新博客仓库 → $BLOG_DIR"
|
say "2/5 拉取/更新博客仓库 → $BLOG_DIR"
|
||||||
AUTH_URL="$(printf '%s' "$CNB_URL" | sed -E "s#^https://#https://cnb:${CNB_TOKEN}@#")"
|
AUTH_URL="$(printf '%s' "$CNB_URL" | sed -E "s#^https://#https://cnb:${CNB_TOKEN}@#")"
|
||||||
if [ -d "$BLOG_DIR/.git" ]; then
|
if [ -d "$BLOG_DIR/.git" ]; then
|
||||||
@@ -132,6 +150,13 @@ umask 077
|
|||||||
printf 'GIT_AUTHOR_NAME=%s\n' "$GIT_AUTHOR_NAME"
|
printf 'GIT_AUTHOR_NAME=%s\n' "$GIT_AUTHOR_NAME"
|
||||||
printf 'GIT_AUTHOR_EMAIL=%s\n' "$GIT_AUTHOR_EMAIL"
|
printf 'GIT_AUTHOR_EMAIL=%s\n' "$GIT_AUTHOR_EMAIL"
|
||||||
printf 'BLOG_BASE=%s\n' "$BLOG_BASE"
|
printf 'BLOG_BASE=%s\n' "$BLOG_BASE"
|
||||||
|
# ★ Gitea 凭据也写进 .env —— 这是「重跑本脚本时复用」的唯一落点(见 1/5 那段)。
|
||||||
|
# 容器本身**不读**这几个键(它只认 PUSH_REMOTES),写在这里没有副作用。
|
||||||
|
if [ -n "${GITEA_PASS:-}" ]; then
|
||||||
|
printf 'GITEA_URL=%s\n' "$GITEA_URL"
|
||||||
|
printf 'GITEA_USER=%s\n' "$GITEA_USER"
|
||||||
|
printf 'GITEA_PASS=%s\n' "$GITEA_PASS"
|
||||||
|
fi
|
||||||
} > "$APP_DIR/.env"
|
} > "$APP_DIR/.env"
|
||||||
|
|
||||||
say "4/5 生成 compose 并启动容器"
|
say "4/5 生成 compose 并启动容器"
|
||||||
|
|||||||
@@ -57,6 +57,30 @@
|
|||||||
- 鉴权复用后台登录会话:管理员和编辑都放行(`/editor/*` 只有文章相关接口),
|
- 鉴权复用后台登录会话:管理员和编辑都放行(`/editor/*` 只有文章相关接口),
|
||||||
评论/用户/设置等其它后台模块仍然只认管理员。
|
评论/用户/设置等其它后台模块仍然只认管理员。
|
||||||
|
|
||||||
|
## 落盘:写一次,存三处(2026-10-06 定案)
|
||||||
|
|
||||||
|
| 层 | 载体 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| **主仓** | `origin` = CNB(`cnb.cool/zqlit/blog`) | **唯一的 fetch 源**,推送即触发构建 |
|
||||||
|
| **代码辅仓** | `gitea` = 自建 `23.254.236.47:3001` | 只推不拉,代码留档;失败**不阻断**发布 |
|
||||||
|
| **文件备份** | 中兴 F50 上的 OpenList | 整仓加密 bundle,由**家里那台机器**每天 03:30 跑(国内机够不到家庭局域网,故不在这边) |
|
||||||
|
|
||||||
|
线上容器(国内机 `119.29.215.187`)实测配置:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# /srv/editor-api/.env
|
||||||
|
PUSH_REMOTES=origin,gitea
|
||||||
|
```
|
||||||
|
|
||||||
|
改完必须验:`curl -s http://127.0.0.1:8017/health` 的 `remotes` 字段应为 `["origin","gitea"]`;
|
||||||
|
容器启动日志也会打一行 `[editor-api] 分支 main 推送远端 origin, gitea`。
|
||||||
|
写权限用「推临时分支 → `ls-remote` 确认 → 删分支」实测 ——
|
||||||
|
**别用 `--dry-run`**,远端落后时它会误报 non-fast-forward。
|
||||||
|
|
||||||
|
> ⚠️ **铁律:任何提交都必须先落到 CNB。** pull 源只有 `origin`,
|
||||||
|
> 在别处只推了 `gitea` 的提交后台看不见(`git pull` 不从它拉),
|
||||||
|
> 下一次发布就会因 non-fast-forward 被拒。
|
||||||
|
|
||||||
## 环境变量
|
## 环境变量
|
||||||
|
|
||||||
| 变量 | 默认 | 说明 |
|
| 变量 | 默认 | 说明 |
|
||||||
|
|||||||
@@ -15,11 +15,21 @@
|
|||||||
* 改用 Node 内置 crypto 的 AES-256-GCM:零外部依赖、带认证标签(能检测篡改/截断)、
|
* 改用 Node 内置 crypto 的 AES-256-GCM:零外部依赖、带认证标签(能检测篡改/截断)、
|
||||||
* 可流式处理 600 MB 不爆内存。加密格式见下方注释,解密由本脚本 `--decrypt` 完成。
|
* 可流式处理 600 MB 不爆内存。加密格式见下方注释,解密由本脚本 `--decrypt` 完成。
|
||||||
*
|
*
|
||||||
|
* ★ 打包前先 `git fetch --all`(2026-10-06 补):
|
||||||
|
* `git bundle create --all` 取的是**本地已知**的 refs —— 其中
|
||||||
|
* `refs/remotes/origin/main` 停在上一次 fetch/pull 的位置。
|
||||||
|
* 而写作后台(editor-api)发的文章只落在 CNB 主仓,本机这份工作区
|
||||||
|
* 并不会自动跟着更新;不 fetch 就打 bundle,等于把**过期的快照**当成备份,
|
||||||
|
* 后台最近发的文章一份都不在里面 —— 而且失败是静默的(备份照样"成功")。
|
||||||
|
* 所以这里先 fetch,再打包。fetch 失败**不致命**(离线也得出得来备份),
|
||||||
|
* 只降级为「用本地已有 ref 打包」并显著告警。
|
||||||
|
*
|
||||||
* 用法:
|
* 用法:
|
||||||
* node scripts/backup-bundle.mjs # 加密 + 上传 + 比对字节数
|
* node scripts/backup-bundle.mjs # fetch + 加密 + 上传 + 比对字节数
|
||||||
* node scripts/backup-bundle.mjs --verify # 额外下载回来比对 sha256(慢,但端到端最可靠)
|
* node scripts/backup-bundle.mjs --verify # 额外下载回来比对 sha256(慢,但端到端最可靠)
|
||||||
* node scripts/backup-bundle.mjs --dry # 只打包加密,不上传
|
* node scripts/backup-bundle.mjs --dry # 只打包加密,不上传
|
||||||
* node scripts/backup-bundle.mjs --keep 7 # 远端保留最近 7 份(默认)
|
* node scripts/backup-bundle.mjs --keep 7 # 远端保留最近 7 份(默认)
|
||||||
|
* node scripts/backup-bundle.mjs --no-fetch # 跳过 fetch(离线/调试用)
|
||||||
* node scripts/backup-bundle.mjs --no-encrypt # 不加密(仅当历史里的敏感文件已洗净)
|
* node scripts/backup-bundle.mjs --no-encrypt # 不加密(仅当历史里的敏感文件已洗净)
|
||||||
* node scripts/backup-bundle.mjs --decrypt <文件> [--out x.bundle] # 解密(恢复用)
|
* node scripts/backup-bundle.mjs --decrypt <文件> [--out x.bundle] # 解密(恢复用)
|
||||||
* node scripts/backup-bundle.mjs --list # 列出远端现有备份
|
* node scripts/backup-bundle.mjs --list # 列出远端现有备份
|
||||||
@@ -245,16 +255,44 @@ const rawBundle = path.join(tmpDir, `blog-${stamp}.bundle`);
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
log('仓库:', REPO);
|
log('仓库:', REPO);
|
||||||
log('1/6 打包 bundle(git bundle create --all)…');
|
|
||||||
|
// 0) 先同步远端引用 —— 见文件头「打包前先 git fetch」那段。
|
||||||
|
// 不 fetch 就会把过期快照当备份,而且失败是静默的(备份照样报"成功")。
|
||||||
|
const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: '0' };
|
||||||
|
if (has('no-fetch')) {
|
||||||
|
log('0/7 跳过 fetch(--no-fetch)—— 本次打包的是**本地当前 ref** 的快照');
|
||||||
|
} else {
|
||||||
|
log('0/7 同步远端引用(git fetch --all --tags --prune)…');
|
||||||
|
try {
|
||||||
|
const out = execFileSync('git', ['-C', REPO, 'fetch', '--all', '--tags', '--prune'], {
|
||||||
|
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], env: gitEnv, timeout: 180000,
|
||||||
|
});
|
||||||
|
const tail = (out || '').trim().split('\n').filter(Boolean).slice(-3).join(' | ');
|
||||||
|
log(' 完成' + (tail ? ':' + tail : '(无新内容)'));
|
||||||
|
} catch (e) {
|
||||||
|
// 离线不该让备份做不出来 —— 降级为用本地已有 ref 打包,但必须显式告警
|
||||||
|
const why = String(e.stderr || e.stdout || e.message || '').trim().split('\n').filter(Boolean).slice(-2).join(' ');
|
||||||
|
log(' ★ fetch 失败,降级为「用本地已有 ref 打包」—— 快照可能不含最新提交');
|
||||||
|
log(' 原因:', why || '(无输出)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log('1/7 打包 bundle(git bundle create --all)…');
|
||||||
let t = Date.now();
|
let t = Date.now();
|
||||||
execFileSync('git', ['-C', REPO, 'bundle', 'create', rawBundle, '--all'], { stdio: ['ignore', 'inherit', 'inherit'] });
|
execFileSync('git', ['-C', REPO, 'bundle', 'create', rawBundle, '--all'], { stdio: ['ignore', 'inherit', 'inherit'] });
|
||||||
log(` 完成 ${mb(fs.statSync(rawBundle).size)} 用时 ${((Date.now() - t) / 1000).toFixed(1)}s`);
|
log(` 完成 ${mb(fs.statSync(rawBundle).size)} 用时 ${((Date.now() - t) / 1000).toFixed(1)}s`);
|
||||||
|
// 把「这份快照停在哪」写进日志 —— 恢复时第一件要确认的就是这一行
|
||||||
|
try {
|
||||||
|
const head = execFileSync('git', ['-C', REPO, 'log', '-1', '--format=%h %ci %s', 'origin/main'],
|
||||||
|
{ encoding: 'utf8', env: gitEnv }).trim();
|
||||||
|
log(' 快照 origin/main =', head);
|
||||||
|
} catch { /* 没有 origin/main 就不打这行,不影响备份 */ }
|
||||||
|
|
||||||
let upload = rawBundle;
|
let upload = rawBundle;
|
||||||
let finalName = path.basename(rawBundle);
|
let finalName = path.basename(rawBundle);
|
||||||
|
|
||||||
if (ENCRYPT) {
|
if (ENCRYPT) {
|
||||||
log('2/6 AES-256-GCM 加密(scrypt 派生密钥)…');
|
log('2/7 AES-256-GCM 加密(scrypt 派生密钥)…');
|
||||||
const enc = rawBundle + '.enc';
|
const enc = rawBundle + '.enc';
|
||||||
t = Date.now();
|
t = Date.now();
|
||||||
await encryptFile(rawBundle, enc, cfg.BACKUP_PASSPHRASE);
|
await encryptFile(rawBundle, enc, cfg.BACKUP_PASSPHRASE);
|
||||||
@@ -263,7 +301,7 @@ try {
|
|||||||
finalName = path.basename(enc);
|
finalName = path.basename(enc);
|
||||||
log(` 完成 ${mb(fs.statSync(upload).size)} 用时 ${((Date.now() - t) / 1000).toFixed(1)}s`);
|
log(` 完成 ${mb(fs.statSync(upload).size)} 用时 ${((Date.now() - t) / 1000).toFixed(1)}s`);
|
||||||
} else {
|
} else {
|
||||||
log('2/6 跳过加密(--no-encrypt)—— 请确认历史里已无敏感文件');
|
log('2/7 跳过加密(--no-encrypt)—— 请确认历史里已无敏感文件');
|
||||||
}
|
}
|
||||||
|
|
||||||
const size = fs.statSync(upload).size;
|
const size = fs.statSync(upload).size;
|
||||||
@@ -275,10 +313,10 @@ try {
|
|||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
log('3/6 准备远端目录', DAVDIR);
|
log('3/7 准备远端目录', DAVDIR);
|
||||||
await ensureDir(DAV_PREFIX, relDirEncoded);
|
await ensureDir(DAV_PREFIX, relDirEncoded);
|
||||||
|
|
||||||
log('4/6 上传中…');
|
log('4/7 上传中…');
|
||||||
t = Date.now();
|
t = Date.now();
|
||||||
const put = await davReq('PUT', davDirPath + '/' + encodeURIComponent(finalName), {
|
const put = await davReq('PUT', davDirPath + '/' + encodeURIComponent(finalName), {
|
||||||
file: upload, extraHeaders: { 'Content-Type': 'application/octet-stream' },
|
file: upload, extraHeaders: { 'Content-Type': 'application/octet-stream' },
|
||||||
@@ -287,7 +325,7 @@ try {
|
|||||||
if (![200, 201, 204].includes(put.status)) throw new Error('上传失败 HTTP ' + put.status);
|
if (![200, 201, 204].includes(put.status)) throw new Error('上传失败 HTTP ' + put.status);
|
||||||
log(` 完成 ${mb(size)} / ${secs.toFixed(1)}s → ${(size / 1048576 / secs).toFixed(1)} MB/s`);
|
log(` 完成 ${mb(size)} / ${secs.toFixed(1)}s → ${(size / 1048576 / secs).toFixed(1)} MB/s`);
|
||||||
|
|
||||||
log('5/6 校验远端');
|
log('5/7 校验远端');
|
||||||
const stat = await davReq('HEAD', davDirPath + '/' + encodeURIComponent(finalName));
|
const stat = await davReq('HEAD', davDirPath + '/' + encodeURIComponent(finalName));
|
||||||
const remoteLen = Number(stat.headers['content-length'] || 0);
|
const remoteLen = Number(stat.headers['content-length'] || 0);
|
||||||
if (remoteLen !== size) log(` ★ 远端字节数不一致(本地 ${size} / 远端 ${remoteLen})`);
|
if (remoteLen !== size) log(` ★ 远端字节数不一致(本地 ${size} / 远端 ${remoteLen})`);
|
||||||
@@ -313,7 +351,7 @@ try {
|
|||||||
fs.unlinkSync(back);
|
fs.unlinkSync(back);
|
||||||
}
|
}
|
||||||
|
|
||||||
log(`6/6 清理旧份(保留最近 ${KEEP} 份)`);
|
log(`6/7 清理旧份(保留最近 ${KEEP} 份)`);
|
||||||
const ls = await davReq('PROPFIND', davDirPath, { extraHeaders: { Depth: '1' } });
|
const ls = await davReq('PROPFIND', davDirPath, { extraHeaders: { Depth: '1' } });
|
||||||
const files = linkNames(ls.body).filter((n) => RE_BACKUP.test(n)).sort().reverse();
|
const files = linkNames(ls.body).filter((n) => RE_BACKUP.test(n)).sort().reverse();
|
||||||
log(' 远端现有:', files.join(', ') || '(无)');
|
log(' 远端现有:', files.join(', ') || '(无)');
|
||||||
|
|||||||
+14
-5
@@ -2,21 +2,27 @@
|
|||||||
/**
|
/**
|
||||||
* 备份包装器 —— 给「离线那一层」加一道失败告警
|
* 备份包装器 —— 给「离线那一层」加一道失败告警
|
||||||
* =====================================================================
|
* =====================================================================
|
||||||
|
* 本项目的三层留存(失效模式各不相同,不能互相替代):
|
||||||
|
* ① 主仓 `origin` = CNB —— 源码 + 触发构建,依赖 CNB 平台
|
||||||
|
* ② 代码辅仓 `gitea` = 自建 23.254.236.47:3001 —— 不受第三方平台规则约束,可 clone/追溯
|
||||||
|
* ③ 离线备份 = 中兴 F50 上的 OpenList —— 单一加密文件,平台全挂也能恢复
|
||||||
|
* 本脚本负责第 ③ 层。
|
||||||
|
*
|
||||||
* 为什么需要它:
|
* 为什么需要它:
|
||||||
* OpenList(中兴 F50)上这份加密 bundle 是**唯一不依赖任何 git 服务**的备份 ——
|
* 第 ③ 层是**唯一不依赖任何 git 服务**的一份 —— 平台全被清、服务器被回收,
|
||||||
* 在线那一路虽有自建 Gitea 代码辅仓(contributor 机器上,推一次就在),
|
* 只有它能恢复。而这类「每天自动跑」的任务最典型的失败模式恰恰是**静默**的 ——
|
||||||
* 但平台全挂、服务器被回收这类事故只有它能挡。
|
|
||||||
* 而这类「每天自动跑」的任务最典型的失败模式恰恰是**静默**的 ——
|
|
||||||
* F50 被带出门、换了网段、OpenList 容器重启后没起来、WebDAV 口令改过……
|
* F50 被带出门、换了网段、OpenList 容器重启后没起来、WebDAV 口令改过……
|
||||||
* 这些都不会有人主动发现,直到某天真的要用备份时才发现最近三个月一份都没成功。
|
* 这些都不会有人主动发现,直到某天真的要用备份时才发现最近三个月一份都没成功。
|
||||||
*
|
*
|
||||||
* 在线辅仓和主仓都还在,所以这不会立刻致命;但「离线备份悄悄失效」这件事本身
|
* ①②两层都还在,所以这不会立刻致命;但「离线备份悄悄失效」这件事本身
|
||||||
* 必须被告知。于是加这一层:跑备份 → 失败就发邮件。
|
* 必须被告知。于是加这一层:跑备份 → 失败就发邮件。
|
||||||
*
|
*
|
||||||
* 它做什么:
|
* 它做什么:
|
||||||
* 1. 读 .workbuddy-backup/openlist-backup.env(KEY=VALUE,# 开头为注释)
|
* 1. 读 .workbuddy-backup/openlist-backup.env(KEY=VALUE,# 开头为注释)
|
||||||
* —— 只填充**尚未设置**的环境变量,环境变量优先(与 backup-bundle.mjs 一致)
|
* —— 只填充**尚未设置**的环境变量,环境变量优先(与 backup-bundle.mjs 一致)
|
||||||
* 2. 跑 scripts/backup-bundle.mjs,参数原样转发,输出实时透传
|
* 2. 跑 scripts/backup-bundle.mjs,参数原样转发,输出实时透传
|
||||||
|
* (backup-bundle.mjs 会先 `git fetch --all` 再打包 —— 否则备的是过期快照,
|
||||||
|
* 后台新发的文章一份都不在里面;fetch 失败只降级告警,不影响出备份)
|
||||||
* 3. 退出码非 0 → 用 scripts/send_mail.js 发告警邮件,正文附日志尾部
|
* 3. 退出码非 0 → 用 scripts/send_mail.js 发告警邮件,正文附日志尾部
|
||||||
* 成功时默认不发(避免每天一封的邮件疲劳);加 --notify-success 才发
|
* 成功时默认不发(避免每天一封的邮件疲劳);加 --notify-success 才发
|
||||||
* 4. **原样返回 backup-bundle.mjs 的退出码**(发信失败不改变它 ——
|
* 4. **原样返回 backup-bundle.mjs 的退出码**(发信失败不改变它 ——
|
||||||
@@ -152,6 +158,9 @@ if (code === 0 && !notifySuccess) {
|
|||||||
' · WebDAV 账号或口令改过',
|
' · WebDAV 账号或口令改过',
|
||||||
' · 仓库体积增长导致超时',
|
' · 仓库体积增长导致超时',
|
||||||
'',
|
'',
|
||||||
|
'另:若日志里出现「fetch 失败,降级为用本地已有 ref 打包」,',
|
||||||
|
'说明备份本身出得来,但快照可能不含最新提交 —— 修复对应远端的凭据/网络后重跑即可。',
|
||||||
|
'',
|
||||||
'--------- 输出尾部 ---------',
|
'--------- 输出尾部 ---------',
|
||||||
tail.trim() || '(无输出)',
|
tail.trim() || '(无输出)',
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **代码主仓** | `cnb.cool/zqlit/blog` | CNB,**私有**;构建由它触发;唯一 fetch 源 |
|
| **代码主仓** | `cnb.cool/zqlit/blog` | CNB,**私有**;构建由它触发;唯一 fetch 源 |
|
||||||
| **代码辅仓** | 自建 Gitea `23.254.236.47:3001/zqlit/blog` | 只推不拉,纯代码留档;**不受第三方平台规则约束**(见 §5.2) |
|
| **代码辅仓** | 自建 Gitea `23.254.236.47:3001/zqlit/blog` | 只推不拉,纯代码留档;**不受第三方平台规则约束**(见 §5.2) |
|
||||||
|
| **写作后台** | 国内机 `119.29.215.187` → `127.0.0.1:8017`(容器 `editor-api`) | 轻量 Node 后端(零 npm 依赖),经 CF Worker 转发;发布时**推 `origin`(CNB) + `gitea`**(见 §5.2) |
|
||||||
| **异地备份** | 中兴 F50 上的 OpenList(`/本地/备份/blog-bundle/`) | **备选**异地备份:整仓加密 bundle,计划任务每天 03:30 自动跑,**不依赖任何 git 服务**(见 §5.6) |
|
| **异地备份** | 中兴 F50 上的 OpenList(`/本地/备份/blog-bundle/`) | **备选**异地备份:整仓加密 bundle,计划任务每天 03:30 自动跑,**不依赖任何 git 服务**(见 §5.6) |
|
||||||
| **构建 + 发布** | CNB 流水线(腾讯云国内节点,4 核 8G) | 托管,0 元(免费额度内) |
|
| **构建 + 发布** | CNB 流水线(腾讯云国内节点,4 核 8G) | 托管,0 元(免费额度内) |
|
||||||
| **密钥仓库** | `cnb.cool/zqlit/blog-secrets` | CNB「密钥仓库」类型,10 项变量经 `imports` 注入 |
|
| **密钥仓库** | `cnb.cool/zqlit/blog-secrets` | CNB「密钥仓库」类型,10 项变量经 `imports` 注入 |
|
||||||
@@ -67,6 +68,13 @@
|
|||||||
|
|
||||||
**对照**:迁移前是 **7 步,中间有 3 个环节是你自己运维的服务器**。
|
**对照**:迁移前是 **7 步,中间有 3 个环节是你自己运维的服务器**。
|
||||||
|
|
||||||
|
> **两条推送入口,同一份语义**:上面第 ② 步是**本机**的 `git pushall`;
|
||||||
|
> **线上写作后台**(国内机 `editor-api` 容器)走的是同一套 ——
|
||||||
|
> `PUSH_REMOTES=origin,gitea`,在 `/srv/blog` 工作区里逐个远端推,
|
||||||
|
> **主仓(CNB)成功即算发布成功**,辅仓(Gitea)失败只警告不阻断。
|
||||||
|
> 两边都要遵守同一条铁律:**任何提交都必须先落到 CNB** ——
|
||||||
|
> pull 源只有 `origin`,只推 gitea 的提交后台看不见(见 §5.2)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. 流水线细节(`.cnb.yml`,284 行)
|
## 3. 流水线细节(`.cnb.yml`,284 行)
|
||||||
@@ -156,6 +164,25 @@ git push gitea main # 只推 Gitea 辅仓
|
|||||||
- 接入时的同步基线:Gitea 停在 `ed38f938`(2026-10-04),落后 57 个提交,
|
- 接入时的同步基线:Gitea 停在 `ed38f938`(2026-10-04),落后 57 个提交,
|
||||||
且是本地 HEAD 的**祖先** → 一次 fast-forward 就追平,**无需强推、不丢历史**。
|
且是本地 HEAD 的**祖先** → 一次 fast-forward 就追平,**无需强推、不丢历史**。
|
||||||
|
|
||||||
|
**线上 editor-api 容器的推送目标**(2026-10-06 起,国内机 `119.29.215.187`)
|
||||||
|
—— 「CNB 主仓 + Gitea 备份仓 + OpenList 文件备份」这条定案在容器侧的落点:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# /srv/editor-api/.env
|
||||||
|
PUSH_REMOTES=origin,gitea
|
||||||
|
```
|
||||||
|
|
||||||
|
- 容器挂载 `/srv`,仓库工作区 `/srv/blog`,同一份 `.git/config` 里挂着 `origin`(CNB) 与 `gitea`
|
||||||
|
- 发布语义(`editor-api/src/git.mjs`):**逐个远端推,主仓成功即算发布成功**,
|
||||||
|
辅仓失败只在返回里带一条 warning、不阻断 —— 所以 Gitea 挂了不会让文章发不出去
|
||||||
|
- **pull 源永远只有 `origin`**:所以在别处**只推了 gitea** 的提交,后台是看不见的
|
||||||
|
(`git pull` 不会从它拉),下次发布会因 non-fast-forward 被拒。
|
||||||
|
→ **铁律:任何提交都必须先落到 CNB。**
|
||||||
|
- 国内机实测可直连自建 Gitea(`200` / 0.38s),**不需要中转机**
|
||||||
|
- 验证方式(改完必做):`curl -s http://127.0.0.1:8017/health` 看 `remotes` 字段,
|
||||||
|
容器日志里也有一行 `[editor-api] 分支 main 推送远端 origin, gitea`;
|
||||||
|
写权限用「推临时分支 → `ls-remote` 确认 → 删分支」实测,别用 `--dry-run`(会因落后误报)
|
||||||
|
|
||||||
已移除的远端(改动前的完整配置快照留在 `.workbuddy-backup/git-remotes.*.txt`):
|
已移除的远端(改动前的完整配置快照留在 `.workbuddy-backup/git-remotes.*.txt`):
|
||||||
|
|
||||||
| remote | 地址 | 移除于 | 原因 |
|
| remote | 地址 | 移除于 | 原因 |
|
||||||
@@ -296,6 +323,19 @@ OpenList 的登录只保护**访问通道**,不保护**存储介质** —— F
|
|||||||
node spawn 直接 `EBUSY`)。改用 Node 内置 `crypto` 的 **AES-256-GCM**:
|
node spawn 直接 `EBUSY`)。改用 Node 内置 `crypto` 的 **AES-256-GCM**:
|
||||||
零外部依赖、带认证标签(能检测篡改/截断)、可流式处理 600 MB 不爆内存。
|
零外部依赖、带认证标签(能检测篡改/截断)、可流式处理 600 MB 不爆内存。
|
||||||
|
|
||||||
|
**★ 打包前必须先 `git fetch --all`**(2026-10-06 补,此前是个静默漏洞):
|
||||||
|
|
||||||
|
`git bundle create --all` 取的是**本地已知**的 ref —— 其中 `refs/remotes/origin/main`
|
||||||
|
停在上一次 fetch/pull 的位置。而这份备份跑在**家里那台机器**上,它的工作区
|
||||||
|
并不会随写作后台(editor-api)的发布自动更新。不 fetch 就打 bundle,等于
|
||||||
|
**把过期的快照当备份**:后台最近发的文章一篇都不在里面,而且**备份照样报"成功"**。
|
||||||
|
→ 所以现在先 fetch 再打包;**fetch 失败不致命**(离线也得出得来备份),
|
||||||
|
只降级为「用本地已有 ref 打包」并在日志里显著告警、失败邮件里也点名这一条。
|
||||||
|
跳过用 `--no-fetch`。
|
||||||
|
|
||||||
|
日志里那句 `快照 origin/main = <sha> <日期> <标题>` 是这份备份的"封面" ——
|
||||||
|
恢复时第一件要确认的就是它,别只看"备份成功"。
|
||||||
|
|
||||||
**加密文件布局**:`magic(8) | salt(16) | iv(12) | 密文(...) | GCM tag(16)`,
|
**加密文件布局**:`magic(8) | salt(16) | iv(12) | 密文(...) | GCM tag(16)`,
|
||||||
密钥由 `scrypt(N=32768, r=8, p=1)` 从口令派生。
|
密钥由 `scrypt(N=32768, r=8, p=1)` 从口令派生。
|
||||||
|
|
||||||
@@ -308,9 +348,10 @@ node scripts/backup-run.mjs --verify --keep 7 # 计划任务的默认组
|
|||||||
node scripts/backup-run.mjs --dry --notify-success # 试跑并强制发一封成功邮件
|
node scripts/backup-run.mjs --dry --notify-success # 试跑并强制发一封成功邮件
|
||||||
|
|
||||||
# 底层脚本(backup-run.mjs 就是转发到它)
|
# 底层脚本(backup-run.mjs 就是转发到它)
|
||||||
node scripts/backup-bundle.mjs # 加密 + 上传 + 比对字节数
|
node scripts/backup-bundle.mjs # fetch + 加密 + 上传 + 比对字节数
|
||||||
node scripts/backup-bundle.mjs --verify # 额外下载回来比对 sha256(端到端最可靠)
|
node scripts/backup-bundle.mjs --verify # 额外下载回来比对 sha256(端到端最可靠)
|
||||||
node scripts/backup-bundle.mjs --dry # 只打包加密,不上传
|
node scripts/backup-bundle.mjs --dry # 只打包加密,不上传
|
||||||
|
node scripts/backup-bundle.mjs --no-fetch # 跳过 fetch(离线/调试)
|
||||||
node scripts/backup-bundle.mjs --keep 7 # 远端保留最近 7 份(默认值)
|
node scripts/backup-bundle.mjs --keep 7 # 远端保留最近 7 份(默认值)
|
||||||
node scripts/backup-bundle.mjs --list # 列出远端现有备份
|
node scripts/backup-bundle.mjs --list # 列出远端现有备份
|
||||||
node scripts/backup-bundle.mjs --decrypt <文件> [--out x.bundle] # 恢复用
|
node scripts/backup-bundle.mjs --decrypt <文件> [--out x.bundle] # 恢复用
|
||||||
@@ -373,6 +414,21 @@ git clone blog.bundle blog # 或 git fetch blog.bundle 'refs/*:refs/*'
|
|||||||
端到端退出码 0;恢复链路已演练(解密 → `git bundle verify` → 1008 提交完整一致);
|
端到端退出码 0;恢复链路已演练(解密 → `git bundle verify` → 1008 提交完整一致);
|
||||||
失败告警的邮件链路也已实测(发出一封「备份成功」验证信到 `imql@qq.com`)。
|
失败告警的邮件链路也已实测(发出一封「备份成功」验证信到 `imql@qq.com`)。
|
||||||
|
|
||||||
|
**覆盖范围(够到哪儿、够不到哪儿)—— 别把「备份成功」当成"什么都备了"**
|
||||||
|
|
||||||
|
| 内容 | 在不在这一层里 |
|
||||||
|
|---|---|
|
||||||
|
| 博客正文与附件 `content/**` | ✅ 在 —— 打包的是**仓库全量对象** |
|
||||||
|
| 通过写作后台(editor-api)发布的文章 | ✅ 在 —— 前提是**打包前 fetch 成功**(见上,这正是加 fetch 的原因) |
|
||||||
|
| `.env` / TLS 私钥等历史里的敏感文件 | ✅ 在(**这才需要加密**) |
|
||||||
|
| 国内机 `/srv/editor-api/{.env,docker-compose.yml}` | ❌ **不在** —— 仓库外,且国内机够不到家里的 OpenList |
|
||||||
|
|
||||||
|
最后一行是**已知缺口**,但影响可控:那几个文件是**可重建的**,不是不可替代的数据 ——
|
||||||
|
`docker-compose.yml` 由 `deploy/editor-api/bootstrap.sh` 生成(在仓库里),
|
||||||
|
`EDITOR_TOKEN` 丢了就重新生成并同步到 Worker,`ADMIN_PASS` / Gitea 口令都是可轮换的。
|
||||||
|
真正不可再生的只有 **git 历史(含 content 全量)**,而它已经在里面了。
|
||||||
|
(若哪天想连这几个文件也一起备,走 1Panel API 抓回来塞进同一份上传即可 —— 家庭机抓得到国内机。)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. 遗留待办
|
## 6. 遗留待办
|
||||||
@@ -382,14 +438,15 @@ git clone blog.bundle blog # 或 git fetch blog.bundle 'refs/*:refs/*'
|
|||||||
| 1 | `gh` remote(GitHub) | ✅ **已移除**(2026-10-06)。GitHub 账号被标记、仓库被 AUP 清空 —— **GitHub 已彻底退出本项目**:远端、推送别名、发布链路 4 处硬编码、CI 定义全部清干净 |
|
| 1 | `gh` remote(GitHub) | ✅ **已移除**(2026-10-06)。GitHub 账号被标记、仓库被 AUP 清空 —— **GitHub 已彻底退出本项目**:远端、推送别名、发布链路 4 处硬编码、CI 定义全部清干净 |
|
||||||
| 2 | **远端与备份定案** | ✅ **已定案**(2026-10-06):**CNB 主仓 + 自建 Gitea 代码辅仓 + OpenList 离线 bundle**(§5.2/§5.3/§5.6)。自建 Gitea 不受第三方规则约束,是唯一「平台出事也带不走」的在线副本;离线 bundle 保完整历史。选型依据(Gitee 两道硬门槛、Codeup 对照)保留在 §5.3 备查 |
|
| 2 | **远端与备份定案** | ✅ **已定案**(2026-10-06):**CNB 主仓 + 自建 Gitea 代码辅仓 + OpenList 离线 bundle**(§5.2/§5.3/§5.6)。自建 Gitea 不受第三方规则约束,是唯一「平台出事也带不走」的在线副本;离线 bundle 保完整历史。选型依据(Gitee 两道硬门槛、Codeup 对照)保留在 §5.3 备查 |
|
||||||
| 3 | `bin/linux/hugo` 出库 | ✅ **不必做**。那个 83.1MB 二进制只卡 Gitee 的单文件上限;既然不走 Gitee,`deploy/Dockerfile` 的取 hugo 方式与 `.cnb.yml` 的 `by:` 字段一行都不用改 |
|
| 3 | `bin/linux/hugo` 出库 | ✅ **不必做**。那个 83.1MB 二进制只卡 Gitee 的单文件上限;既然不走 Gitee,`deploy/Dockerfile` 的取 hugo 方式与 `.cnb.yml` 的 `by:` 字段一行都不用改 |
|
||||||
| 4 | `gitea` remote | ✅ **已恢复**(2026-10-06):作为代码同步辅仓重新挂上,`pushall` 恢复双推(`origin` + `gitea`)。接入时 Gitea 停在 `ed38f938`,一次 fast-forward 追平(+57 提交),**未强推、未丢历史**。发布链路 5 处默认值同步改回 `origin,gitea`(`bootstrap.sh` / `docker-compose.editor.yml` / `server.mjs` / `README.md` / `setup-cnb-remotes.sh`) |
|
| 4 | `gitea` remote | ✅ **已恢复**(2026-10-06):作为代码同步辅仓重新挂上,`pushall` 恢复双推(`origin` + `gitea`)。接入时 Gitea 停在 `ed38f938`,一次 fast-forward 追平(+57 提交),**未强推、未丢历史**。发布链路 5 处默认值同步改回 `origin,gitea`(`bootstrap.sh` / `docker-compose.editor.yml` / `server.mjs` / `README.md` / `setup-cnb-remotes.sh`)。**线上 editor-api 容器也已切双推**(见 #12) |
|
||||||
| 5 | GitHub 侧旧 workflow | ✅ **已删除**(2026-10-06)。整个 `.github/` 目录移除(`deploy.yml` 1169 行 + `aliyun-backup.yml` + `cleanup.yml`),共 −1284 行 —— **GitHub 已不再是任何环节的依赖**。旧实现可在 git 历史中查 |
|
| 5 | GitHub 侧旧 workflow | ✅ **已删除**(2026-10-06)。整个 `.github/` 目录移除(`deploy.yml` 1169 行 + `aliyun-backup.yml` + `cleanup.yml`),共 −1284 行 —— **GitHub 已不再是任何环节的依赖**。旧实现可在 git 历史中查 |
|
||||||
| 6 | 令牌 scope | 缺 `repo-cnb-history:r`(读构建日志);补上后 agent 可自行排错 |
|
| 6 | 令牌 scope | 缺 `repo-cnb-history:r`(读构建日志);补上后 agent 可自行排错 |
|
||||||
| 7 | Gitea 主机 / 广州中转机 | **Gitea 主机已回归**(2026-10-06):作为代码辅仓,本机直连 `23.254.236.47:3001` 即可推。广州中转机仍不参与 git 链路 |
|
| 7 | Gitea 主机 / 广州中转机 | **Gitea 主机已回归**(2026-10-06):作为代码辅仓,本机直连 `23.254.236.47:3001` 即可推。广州中转机仍不参与 git 链路 |
|
||||||
| 8 | `README.md` | ✅ 已更新(去 GitHub 化 + 补上备份脚本说明) |
|
| 8 | `README.md` | ✅ 已更新(去 GitHub 化 + 补上备份脚本说明) |
|
||||||
| 9 | **敏感文件仍在跟踪中** | `.env`、`GITEA_SECRETS.md`、`write-server/nginx/ssl/privkey.pem`、`content/posts/2024/.../setup-secrets.png` —— 若还要推任何新平台,先 `git rm --cached` 并轮换凭据。⚠️ 当初「不改写历史」的唯一顾虑是备份会分叉;**GitHub 那份已消失,这个顾虑没有了** → `git filter-repo` 现在是可行窗口(代价:全部提交 SHA 改变)。离线 bundle 已加密(§5.6),不受此影响 |
|
| 9 | **敏感文件仍在跟踪中** | `.env`、`GITEA_SECRETS.md`、`write-server/nginx/ssl/privkey.pem`、`content/posts/2024/.../setup-secrets.png` —— 若还要推任何新平台,先 `git rm --cached` 并轮换凭据。⚠️ 当初「不改写历史」的唯一顾虑是备份会分叉;**GitHub 那份已消失,这个顾虑没有了** → `git filter-repo` 现在是可行窗口(代价:全部提交 SHA 改变)。离线 bundle 已加密(§5.6),不受此影响 |
|
||||||
| 10 | **整仓离线备份** | ✅ **已上线**(2026-10-06),现为**备选**异地备份(在线那一路是自建 Gitea 辅仓),见 §5.6。计划任务 `Blog-BundleBackup` 每天 03:30 跑,端到端已验证;失败会发告警邮件(已实测)。待补三件安全项:把 `BACKUP_PASSPHRASE` 抄进密码管理器;给 OpenList **改掉 admin 密码 + 开两步验证**(现 `otp: false`,且旧密码已出现在对话里);确认 5244 端口**未暴露到公网** |
|
| 10 | **整仓离线备份** | ✅ **已上线**(2026-10-06),现为**备选**异地备份(在线那一路是自建 Gitea 辅仓),见 §5.6。计划任务 `Blog-BundleBackup` 每天 03:30 跑,端到端已验证;失败会发告警邮件(已实测)。**2026-10-06 补**:打包前加 `git fetch --all` —— 原来备的是「本机已知 ref」的快照,后台新发的文章可能一份都不在里面,且备份照样报成功(§5.6)。待补三件安全项:把 `BACKUP_PASSPHRASE` 抄进密码管理器;给 OpenList **改掉 admin 密码 + 开两步验证**(现 `otp: false`,且旧密码已出现在对话里);确认 5244 端口**未暴露到公网** |
|
||||||
| 11 | **F50 存储目录的使用约定** | `/本地/` 下的 `刷机` / `系统` / `资料` / `软件` / `驱动` / `备份` 都是用户自己在管的类别。**本项目的 bundle 只写 `备份/blog-bundle/` 子目录**,不与用户手工备份(`github-zqlit-*`、`local-repos-*`)混放 |
|
| 11 | **F50 存储目录的使用约定** | `/本地/` 下的 `刷机` / `系统` / `资料` / `软件` / `驱动` / `备份` 都是用户自己在管的类别。**本项目的 bundle 只写 `备份/blog-bundle/` 子目录**,不与用户手工备份(`github-zqlit-*`、`local-repos-*`)混放 |
|
||||||
|
| 12 | **editor-api 容器的三层留存** | ✅ **已落地**(2026-10-06,国内机 `119.29.215.187`):`PUSH_REMOTES=origin,gitea`(CNB 主仓 + 自建 Gitea 备份仓),文件层走 §5.6 的加密 bundle。实测:容器内推 `origin` ✅ / 推 `gitea` ✅(临时分支推完即删),`/health` 返回 `remotes:["origin","gitea"]`,三处 `main` 对齐 `448b898e`。接入时 `/srv/blog` 落后 10 个提交,一次 fast-forward 追平 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user