fix(entrypoint): pending 判定不走 Laravel(坏/外来的配置缓存会打死 artisan)

CI 第一次跑这个特性时红了,根因是一个真实存在的坑:
站点自己的 bootstrap/cache/config.php 一旦是坏的或外来的(CI 真升级用例里那个
**故意写坏**的哨兵文件就是),任何 artisan 命令都会报
`Target class [files] does not exist` —— 于是 migrate:status 读不到任何东西,
pending 被误判成 0,自动迁移静默不发生。

改法:SQLite 的 pending 判定改成**纯只读 PDO** 比对「盘上迁移文件」vs「记账表里的
行」,完全不经过 Laravel(连配置缓存都不读),因此对坏缓存免疫;其它数据库(MySQL /
Postgres)仍走 migrate:status(那类站点不会有本地 config 缓存文件这回事)。

顺带在 CI 用例的 4b 段里把那个哨兵配置缓存挪开:真站点的配置缓存要么不存在、
要么是有效的;它在前面那段是用来验证「同步不许覆盖站点自己的配置缓存」的,
两种用途不能混在一个卷上。

本机实测(拿生产库只读副本造的真 v2.1 卷,409 张图):
  · 干净卷 → 自动补 2 个迁移文件 + 检测到 2 条 pending + 备份 + 两张表 DONE,
    数据行指纹与升级前**逐字节一致**,容器退出码 0
  · 坏配置缓存仍在时 → 迁移如实报错、只打警告,容器照常启动(退出码 0)
  · 表在但记账行被删的半迁移状态 → 同样只警告不崩,下次启动自动重试
This commit is contained in:
mole404 committed 2026-10-02 17:20:44 +08:00
1 parent 4d75333320
commit 3b74c8aae8
2 files changed
+43 -8

No files matched your search

+38 -8
View File
@@ -434,14 +434,44 @@ auto_migrate() {
fi
# ③ 真的有 pending 才备份 + 迁移(否则每次重启都白备份一份库,纯属糟蹋磁盘)
status=$(runuser -u www-data -- sh -c 'cd /var/www/html && php artisan migrate:status' 2>&1) || true
case "$status" in
*Pending*|*pending*) pending=1 ;;
*) pending=0 ;;
esac
if [ "$pending" -eq 0 ]; then
echo "[lsky] 自动迁移检查完成:没有待执行的迁移"
return 0
# · SQLite:直接**只读**比对「盘上的迁移文件」vs「记账表里的行」——
# 刻意不走 Laravel:站点自己的 bootstrap/cache/config.php 一旦是坏的/外来的,
# 任何 artisan 命令都会报 "Target class [files] does not exist"(CI 的真升级用例
# 里那个哨兵配置缓存就是这么把 migrate:status 打死的,实测踩到)。
# · 其它库:走 migrate:status(纯只读查询)。
if [ "$conn" = "sqlite" ]; then
pending=$(runuser -u www-data -- php -r '
$dir = "/var/www/html/database/migrations";
$p = $argv[1];
$ran = [];
try {
$db = new PDO("sqlite:file:" . $p . "?mode=ro", null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
foreach ($db->query("SELECT migration FROM migrations") as $r) { $ran[$r[0]] = true; }
} catch (Throwable $e) { echo "-1"; exit; }
$n = 0;
foreach (glob($dir . "/*.php") as $f) {
if (!isset($ran[basename($f, ".php")])) { $n++; }
}
echo (string) $n;
' "$dbpath" 2>/dev/null) || pending=-1
case "$pending" in ''|*[!0-9]*) pending=-1 ;; esac
if [ "$pending" -lt 0 ]; then
echo "[lsky] 警告:读不出 SQLite 的迁移记账表,跳过自动迁移(站点不受影响)"
return 0
fi
[ "$pending" -eq 0 ] && { echo "[lsky] 自动迁移检查完成:没有待执行的迁移"; return 0; }
echo "[lsky] 检测到 $pending 条待执行迁移"
else
status=$(runuser -u www-data -- sh -c 'cd /var/www/html && php artisan migrate:status' 2>&1) || true
case "$status" in
*Pending*|*pending*) pending=1 ;;
*) pending=0 ;;
esac
if [ "$pending" -eq 0 ]; then
echo "[lsky] 自动迁移检查完成:没有待执行的迁移"
return 0
fi
echo "[lsky] 检测到待执行的迁移"
fi
# 迁移前备份(仅 SQLite 三件套;保留最近 3 份。MySQL/Postgres 请自行确保有备份)
+5
View File
@@ -557,6 +557,9 @@ echo " ✓ 数据面快照(含整份 md5 清单)逐字节相同"
# 所以那段证明不了「真老站点会自动补表」。这一段专门构造真站点形状:
# ① 建 migrations 记账表并写入上游那 10 条(= 2022-08 的 v2.1 站点就是这么记账的)
# ② 把卷里那两个 fork 迁移文件删掉(模拟「镜像里的新迁移还没进卷」这个原始坑)
# ③ 把上面用过的哨兵 bootstrap/cache/config.php 挪开:它是**故意写坏**的,任何 artisan
# 命令都会被它打成 "Target class [files] does not exist";而真站点的配置缓存要么
# 不存在、要么是有效的。不挪开就测不出「真站点能自动迁移」这件事。
# 然后重启新镜像,断言:迁移文件被自动补进卷、两张新表被自动建好、**哨兵数据一字未变**。
echo "== 4b) 自动迁移用例:v2.1 形状老站点 → 新镜像(无需人工干预)=="
docker run --rm -i -v "$VOL":/var/www/html --entrypoint sh "$NEW_REF" > "$LOGDIR/phase4b-seed.out" 2>&1 <<'EOSH'
@@ -564,6 +567,8 @@ set -eu
cd /var/www/html
# ② 删掉卷里那两个 fork 迁移文件(模拟「新迁移还没进卷」;镜像里那份干净树仍在)
rm -f database/migrations/2026_10_01_*.php
# ③ 挪开故意写坏的哨兵配置缓存(见上面第 ③ 条说明)
rm -f bootstrap/cache/config.php
# ① 建记账表并写入上游 10 条:v2.1 站点就是这样被 Laravel 记账的
php -r '
$p = "/var/www/html/database/database.sqlite";