Compare commits

..

3 Commits

Author SHA1 Message Date
xfy
f7e4d2f5ea chore: release v0.5.0
Some checks failed
CI / check (push) Has been cancelled
CI / build (push) Has been cancelled
数学公式(KaTeX)、mermaid 流程图、bun 代码运行器、文章脚注、Vue SFC 高亮等新功能;SSR 缓存失效根治、Docker 构建/部署修复、性能优化系列(mimalloc + 热点路径零拷贝/单遍扫描)。
2026-07-22 17:41:29 +08:00
xfy
44c76b48c1 chore(deploy): 新增 xun 服务器全量部署脚本
scripts/xun.fish(fish + docker buildx)一键完成:
runner base / 主应用(透传 git 信息)/ 5 个 runner 子镜像的 amd64 构建、
架构校验、导出 scp 到 xun、导入去 localhost/ 前缀、滚动重启 app 容器,
并附容器/迁移/runner/healthz/外网 HTTPS 全链路校验。
2026-07-22 17:33:09 +08:00
xfy
856eeb758c fix(build): Docker 构建透传 git 信息,修复 x-yggdrasil-git 恒为 unknown
.dockerignore 排除 .git/,容器内 build.rs 跑不了 git 命令,导致
x-yggdrasil-git / git_hash / commit_date 响应头和启动日志全是 "unknown"
(version 来自 CARGO_PKG_VERSION 不受影响)。

三级取值链(build.rs env_or_git):
  YGG_BUILD_* 环境变量 → 本地 git 命令 → "unknown"
第一级 std::env::var 优先读 docker --build-arg 注入的值,第二级保留
本地 cargo build 的既有行为,兜底不变。

- Dockerfile: 主应用 server build 前加 ARG+ENV 块(ARG 仅 RUN 可见,
  build.rs 由 cargo 调用非 RUN,故需 export 成 ENV 让 cargo build 继承)
- Makefile: GIT_DESCRIBE/HASH/DATE 在宿主采集,GIT_BUILD_ARGS 复用块
  注入 docker/docker-amd64/docker-multiarch 三个 target
- deploy-to-linux skill: 两处 docker buildx build 命令(首装 + 更新)
  加同名 --build-arg,附说明
2026-07-22 16:05:49 +08:00
8 changed files with 215 additions and 24 deletions

View File

@ -110,12 +110,17 @@ grep UseVirtualizationFrameworkRosetta ~/Library/Group\ Containers/group.com.doc
### 主应用镜像
```bash
docker buildx build --platform linux/amd64 --load -t localhost/yggdrasil:latest .
docker buildx build --platform linux/amd64 --load \
--build-arg YGG_BUILD_GIT_DESCRIBE="$(git describe --tags --always --dirty)" \
--build-arg YGG_BUILD_GIT_HASH="$(git rev-parse HEAD)" \
--build-arg YGG_BUILD_GIT_COMMIT_DATE="$(git log -1 --format=%cd --date=iso-strict)" \
-t localhost/yggdrasil:latest .
```
- Dockerfile 用 `dpkg --print-architecture` 检测架构amd64 腿原生构建 `x86_64-unknown-linux-musl` 静态二进制
- 首次约 15-30 分钟Rosetta 下 cargo 全量编译),有 buildkit 缓存后分钟级
- 产物 `localhost/yggdrasil:latest`scratch 运行时层约 16MB
- **`--build-arg` 透传 git 信息**`.dockerignore` 排除 `.git/`,容器内 `build.rs` 跑不了 git必须在宿主采集后注入否则镜像的 `x-yggdrasil-git` 响应头恒为 `unknown`。三个 `$(...)` 命令替换在本机 zsh/bash 下执行(非服务器)。`make docker-amd64` 等价Makefile 已内置同样的采集+透传)
### 6 个 Code Runner 沙箱镜像
@ -418,7 +423,11 @@ rm -f /tmp/yggdrasil-*.tar* /tmp/.env
```bash
# 1. 本地重新构建主应用(有缓存,快)
docker buildx build --platform linux/amd64 --load -t localhost/yggdrasil:latest .
docker buildx build --platform linux/amd64 --load \
--build-arg YGG_BUILD_GIT_DESCRIBE="$(git describe --tags --always --dirty)" \
--build-arg YGG_BUILD_GIT_HASH="$(git rev-parse HEAD)" \
--build-arg YGG_BUILD_GIT_COMMIT_DATE="$(git log -1 --format=%cd --date=iso-strict)" \
-t localhost/yggdrasil:latest .
# 2. 导出传输
docker save localhost/yggdrasil:latest | gzip > /tmp/yggdrasil-app.tar.gz
scp /tmp/yggdrasil-app.tar.gz <host>:/root/docker/yggdrasil/

View File

@ -7,6 +7,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.5.0] - 2026-07-22
### Added
- **文章数学公式 SSR 渲染(KaTeX)**:引入 `katex-rs` 在服务端把 `$...$`/`$$...$$` TeX 渲染成视觉层 HTML span(`OutputFormat::Html`,不含 MathML,XSS 面最小;`throw_on_error=false` 坏公式渲染成红色错误而非中断);自托管 KaTeX CSS + woff2 字体到 `public/katex/`(`make katex-css` 从 npm `katex` dist 拷贝)。
- **评论数学公式渲染**:评论路径同步开启 `ENABLE_MATH`,span 白名单加 `style` 保留 KaTeX 内联定位样式。
- **编辑器数学公式节点**Tiptap 数学公式节点带 KaTeX 预览,根治编辑器序列化破坏 LaTeX 的问题。
- **mermaid 流程图懒加载渲染**:文章页 `language-mermaid` 代码块经 IntersectionObserver 视口可见时动态 `import('/mermaid/mermaid.js')`(独立 IIFE bundle,~3.4MB / gzip ~900KB,非全局注入),`mermaid.render` 产 SVG 注入;主题经 `__initMermaid` 传入,`securityLevel: 'strict'`,幂等守卫防重复渲染;渲染失败保留源码并加 `.mermaid-error` class。
- **流程图配色对齐 Catppuccin**mermaid 配色对齐 Catppuccin 主题,容器美化。
- **流程图主题切换动画**:流程图主题切换跟随 View Transitions 圆形扩散动画,主题切换时重渲染已渲染的流程图。
- **bun 代码运行器**:新增 `yggdrasil-runner-bun` 沙箱镜像(官方 `bun.sh/install` 脚本 + musl 变体 + `libstdc++`/`libgcc` C++ 运行时);admin 代码试运行沙箱加 bun 语言按钮;CodeMirror 加 TypeScript 模式;语言别名归一化(`ts`/`typescript``bun`,在 `parse_fence_info` 统一,`LANGUAGES.get` 只见规范化 key)。
- **文章页脚注完整支持**:语义化 + back-link + 样式。
- **Vue SFC 语法高亮**:文章页代码块支持 Vue SFC 语法高亮。
- **搜索入口改为图标按钮**header 搜索入口从文字改为图标按钮。
- **正文折叠块卡片化**`<details>` 折叠块卡片化,自绘 chevron + hover/focus 态。
- **task-list checkbox 自绘**:文章页 task-list checkbox 改用 `appearance:none` 自绘圆角方框。
- **代码块字号调整**:文章页代码块字号从 13.6px 调整为 16px。
- **响应头暴露版本信息**server 通过 `Server`/`X-Yggdrasil-Version`/`X-Yggdrasil-Git` 响应头主动暴露版本与 git 描述信息(`EXPOSE_VERSION_HEADERS` 可关)。
- **Docker multi-arch 构建目标**`make docker-amd64``make docker-apple` 构建 x86_64 镜像。
- **服务器端口占用优雅退出**:端口被占用时优雅退出而非 panic。
- **压缩算法默认 off**`COMPRESSION_ALGORITHMS` 中间件默认值改为 off。
### Changed
- **mimalloc 全局分配器**:用 mimalloc 替换系统全局分配器(`#[global_allocator]`,双 cfg 门控:server feature + 非 wasm32;musl 静态链接友好)。
- **性能优化系列**`escape_html` 链式 5 次 replace 改单遍扫描;`slugify` 单遍状态机重写(分配 4→1);Markdown 渲染消除双解析 + `format!``write!` 直写;upload 消除 `data.to_vec()` 多次全文件深拷贝;`cache_key` 单次拼接 + `detect_format` 零分配后缀匹配;posts list/search 零 capacity Vec 改 collect 预分配 + helpers retain。
- **重构系列**admin `/admin/posts``/admin/posts/trash` 合并为单路由 + tab 切换;`Pagination` 支持可选 `on_prev`/`on_next` 回调;抽取 `@yggdrasil/shared` 内部源共享包消除跨 IIFE 库的类型/常量重复;抽 `main.rs` 中间件到 `src/middleware.rs`;为 `Response` 类型添加构造器消除 51 处样板;抽 `invalidate_post_metadata()`/`upload_error()` 等消除样板;统一 WASM sleep 到 `utils::time::sleep_ms`;删除死代码(`delayed_loading.rs`/`ui.rs EmptyState`/`CommentActions`/未用 re-export);拆分 `system.rs``system/` 目录(按 tab 分文件);图片处理合并维度读取函数共享 `image_reader_limits`
- **依赖升级**TypeScript 升级至 7.0.2(Go 原生编译器)。
### Fixed
- **SSR 缓存失效根治**:文章写入后物理删除 SSR 磁盘缓存目录,根治「重建后内容不更新」(Dioxus 0.7 增量渲染器只暴露 TTL 失效手段,通过删文件绕过限制);build 前清除 `static/` SSR 缓存目录。
- **Docker 构建/部署**Docker 构建透传 git 信息修复 `x-yggdrasil-git` 恒为 unknown;预装 binaryen 避免 dx 运行时下载 wasm-opt 失败;补齐 Dockerfile 缺失的 katex-css 与 restore-webp 步骤;升级 builder 至 trixie 满足 dx 对 GLIBC_2.39 的需求;用 tmpfs `mode=1777` 替换 uid/gid 选项兼容 Podman;GitHub Releases 下载最终改用直连(移除 gh-proxy)。
- **线上代码高亮缺失**:编译期内嵌自定义语法,修复线上 Docker 镜像代码高亮缺失(原先运行时加载语法文件在打包镜像中找不到)。
- **mermaid 渲染**:改用 script 标签加载 IIFE bundle 修正全局变量取值;修复 tsc 类型错误;主题切换时重渲染已渲染的流程图。
- **文章锚点导航**`scrollToHash` 增加一次性守卫,切主题不再跳回 URL hash;增加 ResizeObserver 布局稳定期,修正 mermaid 异步渲染导致的锚点落点偏移。
- **WASM 双端编译**hooks 模块移出 server gate 双端可见(原先 WASM 编译失败)。
- **评论代码块转义**:统一 `escape_html`,修复代码块单引号未转义。
- **Docker daemon 容错**Docker daemon 不可用/断连时不再 panic(集成测试在无 daemon 环境优雅跳过)。
- **后台布局**:重建结果消息改绝对定位,避免撑高容器顶起按钮。
- **图片 cfg 门控**:为 `ImageFmt` 别名补上 `#[cfg(feature = "server")]` 门控。
- **clippy/lint**:修复 rust-1.97 clippy `useless_borrows_in_formatting` 告警、Biome 告警。
- **安全测试**:补齐安全关键路径的单元测试盲区。
### Internal
- **skills 体系**:新增 `optimizing-rust-performance``rust-advanced-performance` 性能优化技能;清理已卸载的第三方 skills 及 lock 注册;`deploy-to-linux` 添加手动部署模式。
- **部署脚本**:新增 xun 服务器全量部署脚本。
- **文档**AGENTS.md 补充别名归一化与 bun 镜像说明、数学公式与流程图架构说明、xterm-terminal/shared 库说明;补全 `.env.example` 缺失的 5 个环境变量。
## [0.4.0] - 2026-07-13
### Added

2
Cargo.lock generated
View File

@ -5591,7 +5591,7 @@ dependencies = [
[[package]]
name = "yggdrasil"
version = "0.4.0"
version = "0.5.0"
dependencies = [
"argon2",
"axum",

View File

@ -1,6 +1,6 @@
[package]
name = "yggdrasil"
version = "0.4.0"
version = "0.5.0"
edition = "2021"
# 构建时工具二进制:用 syntectserver-only 依赖)生成 highlight.css。

View File

@ -148,6 +148,24 @@ COPY libs/lightbox/package.json libs/lightbox/
COPY libs/yggdrasil-core/package.json libs/yggdrasil-core/
RUN cd libs && pnpm install --frozen-lockfile
# Build-time git info, injected by the caller via --build-arg. `.dockerignore`
# excludes `.git/`, so build.rs can't run `git` inside the container — these
# ARGs are the only channel for git metadata to reach build.rs (it reads them
# via std::env::var, its first-precedence source). Defaults are empty so a
# bare `docker build` without args degrades to "unknown" gracefully (build.rs
# falls back to the git command, then "unknown").
#
# Each ARG → ENV pair is needed: ARG is only visible in Dockerfile RUN
# commands (and build.rs is invoked by cargo, not a RUN), so we export it as
# ENV for the cargo build step to inherit. Both default to empty; Makefile's
# docker/docker-amd64/docker-multiarch targets override them on the host.
ARG YGG_BUILD_GIT_DESCRIBE=""
ARG YGG_BUILD_GIT_HASH=""
ARG YGG_BUILD_GIT_COMMIT_DATE=""
ENV YGG_BUILD_GIT_DESCRIBE=${YGG_BUILD_GIT_DESCRIBE}
ENV YGG_BUILD_GIT_HASH=${YGG_BUILD_GIT_HASH}
ENV YGG_BUILD_GIT_COMMIT_DATE=${YGG_BUILD_GIT_COMMIT_DATE}
# Copy the rest of the source tree and build everything.
COPY . .

View File

@ -186,19 +186,32 @@ doc-open:
# Push examples:
# make docker-multiarch IMAGE=ghcr.io/owner/yggdrasil:latest
# make docker-multiarch IMAGE=user/yggdrasil:v1 PLATFORMS=linux/amd64
#
# git 信息透传:.dockerignore 排除 .git/,容器内 build.rs 跑不了 git 命令,
# 所以在宿主(本 Makefile 里)采集后用 --build-arg 注入。Dockerfile 把 ARG
# 再 export 成 ENV,build.rs 的 std::env::var 优先读它。三个值在 Make 变量
# 里采集一次,所有 docker target 复用;git 不可用时退化为空串,Dockerfile
# 默认值也是空,build.rs 最终降级为 "unknown",不阻断构建。
IMAGE ?= yggdrasil
PLATFORMS ?= linux/amd64,linux/arm64
GIT_DESCRIBE := $(shell git describe --tags --always --dirty 2>/dev/null)
GIT_HASH := $(shell git rev-parse HEAD 2>/dev/null)
GIT_DATE := $(shell git log -1 --format=%cd --date=iso-strict 2>/dev/null)
# build-arg 复用块:每个 docker target 展开一次。空值也传(让 Dockerfile 默认接管)。
GIT_BUILD_ARGS = --build-arg YGG_BUILD_GIT_DESCRIBE="$(GIT_DESCRIBE)" \
--build-arg YGG_BUILD_GIT_HASH="$(GIT_HASH)" \
--build-arg YGG_BUILD_GIT_COMMIT_DATE="$(GIT_DATE)"
docker:
@docker buildx build --load -t yggdrasil .
@docker buildx build --load $(GIT_BUILD_ARGS) -t yggdrasil .
# Cross-build x86_64 into the local daemon. buildx 仿真非原生架构,无需修改 Dockerfile
# (它本就按 dpkg --print-architecture 自适应选 musl target)。Apple Silicon 上走 QEMU,
# 比 native 慢;产物可直接 docker run / docker save 导出。
docker-amd64:
@docker buildx build --platform linux/amd64 --load -t yggdrasil:amd64 .
@docker buildx build --platform linux/amd64 --load $(GIT_BUILD_ARGS) -t yggdrasil:amd64 .
docker-multiarch:
@docker buildx build --platform $(PLATFORMS) -t $(IMAGE) --push .
@docker buildx build --platform $(PLATFORMS) $(GIT_BUILD_ARGS) -t $(IMAGE) --push .
clean:
@cargo clean

View File

@ -7,6 +7,9 @@
//! - git 不可用(非仓库 / tarball 构建)时降级为 "unknown",不 fail the build。
//! - 声明 rerun-if-changed=.git/HEAD,否则 cargo 默认仅在 build.rs 自身变化时
//! 重跑,会导致打印的还是旧 hash。
//! - 三级取值:编译期环境变量(`YGG_BUILD_*`,CI / docker --build-arg 注入)
//! → 本地 `git` 命令 → `"unknown"`。Docker 构建时 `.git/` 不进构建上下文
//! (`.dockerignore` 排除),靠 Makefile 在宿主采集后用 `--build-arg` 透传。
use std::process::Command;
@ -16,9 +19,9 @@ fn main() {
// 工作区脏状态变化时 describe 的 --dirty 后缀也会变,重跑一次。
println!("cargo:rerun-if-changed=.git/index");
set_env("YGG_BUILD_GIT_DESCRIBE", git_describe());
set_env("YGG_BUILD_GIT_HASH", git_hash());
set_env("YGG_BUILD_GIT_COMMIT_DATE", git_commit_date());
set_env("YGG_BUILD_GIT_DESCRIBE", env_or_git("YGG_BUILD_GIT_DESCRIBE", &["describe", "--tags", "--always", "--dirty"]));
set_env("YGG_BUILD_GIT_HASH", env_or_git("YGG_BUILD_GIT_HASH", &["rev-parse", "HEAD"]));
set_env("YGG_BUILD_GIT_COMMIT_DATE", env_or_git("YGG_BUILD_GIT_COMMIT_DATE", &["log", "-1", "--format=%cd", "--date=iso-strict"]));
set_env("YGG_BUILD_RUSTC_VERSION", rustc_version());
// 编译时刻(Unix 秒)。std 无 ISO 8601 格式化,存秒数由运行时 chrono 解析。
set_env("YGG_BUILD_TIME", build_time_unix());
@ -29,20 +32,21 @@ fn set_env(key: &str, value: String) {
println!("cargo:rustc-env={key}={value}");
}
/// `git describe --tags --always --dirty`:最紧凑的"版本 + 提交数 + hash + 脏标记"。
fn git_describe() -> String {
git_output(&["describe", "--tags", "--always", "--dirty"]).unwrap_or_else(|| "unknown".into())
}
/// 完整 40 位 commit hash。
fn git_hash() -> String {
git_output(&["rev-parse", "HEAD"]).unwrap_or_else(|| "unknown".into())
}
/// 提交时间(ISO 8601 strict,带时区偏移,可直接被 chrono 解析)。
fn git_commit_date() -> String {
git_output(&["log", "-1", "--format=%cd", "--date=iso-strict"])
.unwrap_or_else(|| "unknown".into())
/// 三级取值:`YGG_BUILD_<KEY>` 环境变量 → 本地 git 命令 → `"unknown"`。
///
/// - 第一级优先 `std::env::var`(Docker 构建时由 `--build-arg` → `ARG` → `ENV`
/// 注入到 build.rs 进程环境),非空即采用——这是 Docker 构建唯一能拿到 git
/// 信息的通道(`.git/` 被 `.dockerignore` 排除,容器内没有仓库可查)。
/// - 第二级落回本地 `git` 命令(本地 `cargo build` / 非 Docker CI 走这条)。
/// - 兜底 `"unknown"`(tarball 构建、git 未安装等)。
fn env_or_git(env_key: &str, git_args: &[&str]) -> String {
if let Ok(v) = std::env::var(env_key) {
let v = v.trim();
if !v.is_empty() {
return v.to_string();
}
}
git_output(git_args).unwrap_or_else(|| "unknown".into())
}
/// 执行一条 git 命令,返回 trim 后的 stdout。失败返回 None(降级路径)。

97
scripts/xun.fish Executable file
View File

@ -0,0 +1,97 @@
#!/usr/bin/env fish
# Yggdrasil 全量部署到 xun(Podman + fish + nginx-proxy 反代)
# 主应用 + 5 个 Code Runner 沙箱镜像全量重建、传输、导入、滚动重启
# 用法: fish scripts/xun.fish
# 5 个 runner 子镜像(base 由脚本自己先建,FROM 它)
set -l RUNNERS python node go rust bun
echo "==> [1/7] 构建 runner base 镜像"
docker buildx build --platform linux/amd64 --load \
-t localhost/yggdrasil-runner-base:latest docker/runner-base; or exit 1
docker tag localhost/yggdrasil-runner-base:latest yggdrasil-runner-base:latest; or exit 1
echo "==> [2/7] 构建主应用镜像(透传 git 信息)"
docker buildx build --platform linux/amd64 --load \
--build-arg YGG_BUILD_GIT_DESCRIBE=(git describe --tags --always --dirty) \
--build-arg YGG_BUILD_GIT_HASH=(git rev-parse HEAD) \
--build-arg YGG_BUILD_GIT_COMMIT_DATE=(git log -1 --format=%cd --date=iso-strict) \
-t localhost/yggdrasil:latest .; or exit 1
echo "==> [3/7] 构建 5 个 runner 子镜像"
for img in $RUNNERS
echo " -- $img"
docker buildx build --platform linux/amd64 --load \
-t localhost/yggdrasil-runner-$img:latest docker/runner-$img; or exit 1
docker tag localhost/yggdrasil-runner-$img:latest yggdrasil-runner-$img:latest; or exit 1
end
echo "==> [4/7] 构建验证(期望全 amd64)"
set -l ALL_IMAGES yggdrasil yggdrasil-runner-base
for img in $RUNNERS
set -a ALL_IMAGES yggdrasil-runner-$img
end
for img in $ALL_IMAGES
set -l arch (docker image inspect localhost/$img:latest --format "{{.Architecture}}")
echo " $img: $arch"
if test "$arch" != "amd64"
echo " 架构错误!期望 amd64" >&2
exit 1
end
end
echo "==> [5/7] 导出 + 传输到 xun"
# 主应用单独一个 tar(滚动重启时只 reload 它)
docker save localhost/yggdrasil:latest -o /tmp/yggdrasil-app.tar; or exit 1
# 6 个 runner 镜像打包成一个 tar(base + 5 子镜像)
docker save \
localhost/yggdrasil-runner-base:latest \
localhost/yggdrasil-runner-python:latest \
localhost/yggdrasil-runner-node:latest \
localhost/yggdrasil-runner-go:latest \
localhost/yggdrasil-runner-rust:latest \
localhost/yggdrasil-runner-bun:latest \
-o /tmp/yggdrasil-runners.tar; or exit 1
gzip -f /tmp/yggdrasil-app.tar /tmp/yggdrasil-runners.tar; or exit 1
scp /tmp/yggdrasil-app.tar.gz /tmp/yggdrasil-runners.tar.gz xun:/root/docker/yggdrasil/; or exit 1
rm -f /tmp/yggdrasil-app.tar.gz /tmp/yggdrasil-runners.tar.gz
echo "==> [6/7] 服务器导入 + runner 去前缀 + 滚动重启 app"
ssh xun 'cd /root/docker/yggdrasil && gunzip -kf yggdrasil-app.tar.gz && gunzip -kf yggdrasil-runners.tar.gz'; or exit 1
ssh xun 'docker load -i /root/docker/yggdrasil/yggdrasil-app.tar'; or exit 1
ssh xun 'docker load -i /root/docker/yggdrasil/yggdrasil-runners.tar'; or exit 1
# runner 去 localhost/ 前缀:LANGUAGES 注册表硬编码 yggdrasil-runner-*:latest(无前缀、无 env 覆盖)
# fish 不认 bash for 循环,逐条 ssh 执行
ssh xun 'docker tag localhost/yggdrasil-runner-base:latest yggdrasil-runner-base:latest'
ssh xun 'docker tag localhost/yggdrasil-runner-python:latest yggdrasil-runner-python:latest'
ssh xun 'docker tag localhost/yggdrasil-runner-node:latest yggdrasil-runner-node:latest'
ssh xun 'docker tag localhost/yggdrasil-runner-go:latest yggdrasil-runner-go:latest'
ssh xun 'docker tag localhost/yggdrasil-runner-rust:latest yggdrasil-runner-rust:latest'
ssh xun 'docker tag localhost/yggdrasil-runner-bun:latest yggdrasil-runner-bun:latest'
# 滚动重启:只重建 app 容器(postgres 和数据卷不动)
ssh xun 'cd /root/docker/yggdrasil && docker compose --env-file .env up -d app'; or exit 1
ssh xun 'rm -f /root/docker/yggdrasil/yggdrasil-app.tar* /root/docker/yggdrasil/yggdrasil-runners.tar*'
echo "==> [7/7] 验证"
echo "--- 容器状态(期望 postgres healthy + app up)---"
ssh xun 'docker ps --filter name=yggdrasil --format "{{.Names}} {{.Status}}"'
echo "--- 迁移日志(期望 applied,无 error/panic)---"
ssh xun 'docker logs yggdrasil-app 2>&1 | grep -iE "migrat|error|panic" | tail'
echo "--- runner 镜像(期望全 found amd64)---"
ssh xun 'docker inspect yggdrasil-runner-base:latest --format "base: found {{.Architecture}}"'
ssh xun 'docker inspect yggdrasil-runner-python:latest --format "python: found {{.Architecture}}"'
ssh xun 'docker inspect yggdrasil-runner-node:latest --format "node: found {{.Architecture}}"'
ssh xun 'docker inspect yggdrasil-runner-go:latest --format "go: found {{.Architecture}}"'
ssh xun 'docker inspect yggdrasil-runner-rust:latest --format "rust: found {{.Architecture}}"'
ssh xun 'docker inspect yggdrasil-runner-bun:latest --format "bun: found {{.Architecture}}"'
echo "--- 健康检查 ---"
ssh xun 'docker exec nginx-proxy curl -s http://yggdrasil-app:3000/healthz'; echo
ssh xun 'docker exec nginx-proxy curl -s http://yggdrasil-app:3000/readyz'; echo
echo "--- git 版本头(确认不再是 unknown)---"
ssh xun 'docker exec nginx-proxy curl -sI http://yggdrasil-app:3000/ | grep -i x-yggdrasil-git'
echo "--- 外部 HTTPS ---"
curl -s https://rua.plus/healthz; echo
curl -sI https://rua.plus/ | grep -iE "HTTP/|strict-transport"
echo "==> 部署完成"