diff --git a/.gitignore b/.gitignore index 28faf98..5dd0a44 100644 --- a/.gitignore +++ b/.gitignore @@ -35,9 +35,19 @@ Desktop.ini # local runtime artifacts *.pid *.seed +.omc/ +.omx/ +scripts/.omx/ +.codex/skills/ui-ux-pro-max/scripts/__pycache__/ public/images/gallery # generated sync data src/data/generated/*.json !src/data/generated/.gitkeep + +# generated logs under content/logs, keep tracked templates +src/content/logs/*.md +!src/content/logs/homepage-design-system.md +!src/content/logs/homepage-landing-build.md +!src/content/logs/homepage-requirements.md diff --git a/README.md b/README.md index beea174..4523e51 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,7 @@ npm run rebuild - `docs/rebuild-trigger-spec.md`:AstrBot / cron 重建触发与错误码约定 - `docs/content-sync-guide.md`:环境变量、映射文件、同步脚本职责、故障排查 - `docs/docker-deploy.md`:Docker / Compose 部署说明 +- `docs/linux-cron-deploy.md`:宿主机定时同步 log、重建 homepage、刷新 Docker 的脚本说明 - `docs/ui-design-spec.md`:UI 设计规范 ## 内容入口 diff --git a/docs/linux-cron-deploy.md b/docs/linux-cron-deploy.md new file mode 100644 index 0000000..9fbfdbe --- /dev/null +++ b/docs/linux-cron-deploy.md @@ -0,0 +1,125 @@ +# Linux 定时部署脚本说明 + +适用于当前这套流程: + +- log 由另一个服务生成 +- log 落到宿主机目录 +- 宿主机定时任务复制 log 到 homepage 仓库 +- 宿主机执行 `npm run rebuild` +- 宿主机执行 `docker compose up -d --build homepage` + +统一脚本: + +```bash +scripts/deploy-homepage.sh +``` + +--- + +## 1. 当前默认值 + +脚本里的默认值已经按当前主页项目直接写死: + +| 变量 | 默认值 | 说明 | +|---|---|---| +| `LOG_SOURCE_DIR` | `/home/basil/bot/data/git-summary` | 日志生成服务写入的源目录 | +| `SITE_DIR` | `/home/basil/source/personal-homepage` | 个人主页仓库根目录 | +| `LOG_TARGET_DIR` | `$SITE_DIR/src/content/logs` | 复制后的日志目标目录 | +| `DOCKER_COMPOSE_FILE` | `$SITE_DIR/docker-compose.yml` | Compose 文件 | +| `DOCKER_SERVICE_NAME` | `homepage` | 要重建/重启的服务名 | +| `RSYNC_DELETE` | `false` | 同步时默认不删除旧日志 | +| `RUN_LOCAL_REBUILD` | `true` | 默认先在宿主机执行 `npm run rebuild` | +| `LOCK_FILE` | `/tmp/personal-homepage-deploy.lock` | 防止重复执行的锁文件 | + +如果你的机器目录不同,可以通过环境变量覆盖。 + +--- + +## 2. 脚本实际执行内容 + +脚本默认顺序如下: + +1. 检查目录与命令是否存在 +2. 获取锁文件,避免重复执行 +3. 把 `LOG_SOURCE_DIR` 同步到 `LOG_TARGET_DIR` +4. 进入 `SITE_DIR` 执行 `npm run rebuild` +5. 进入 `SITE_DIR` 执行: + +```bash +docker compose -f "$DOCKER_COMPOSE_FILE" up -d --build "$DOCKER_SERVICE_NAME" +``` + +--- + +## 3. 直接执行 + +使用默认值: + +```bash +./scripts/deploy-homepage.sh +``` + +覆盖部分变量: + +```bash +LOG_SOURCE_DIR=/data/logs \ +SITE_DIR=/srv/personal-homepage \ +RSYNC_DELETE=true \ +./scripts/deploy-homepage.sh +``` + +如果你只想做 log 同步 + Docker 重建,不想先在宿主机跑一次 `npm run rebuild`: + +```bash +RUN_LOCAL_REBUILD=false ./scripts/deploy-homepage.sh +``` + +--- + +## 4. 推荐的 cron 配置 + +例如每天 00:00 执行一次: + +```cron +0 0 * * * /home/basil/source/personal-homepage/scripts/deploy-homepage.sh >> /var/log/personal-homepage-deploy.log 2>&1 +``` + +例如每 30 分钟执行一次: + +```cron +*/30 * * * * /home/basil/source/personal-homepage/scripts/deploy-homepage.sh >> /var/log/personal-homepage-deploy.log 2>&1 +``` + +因为脚本内部已经带了 `LOCK_FILE` 锁,所以 cron 不一定还要额外再包一层 `flock`。 + +--- + +## 5. 推荐前提 + +宿主机应具备: + +- `docker` +- `docker compose` +- `npm` +- `rsync`(推荐,有则优先使用) +- `flock`(推荐,用于避免重入) + +如果没有 `rsync`,脚本会自动退回到 `cp -a`。 + +如果没有 `flock`,脚本也能跑,只是没有并发保护。 + +--- + +## 6. 建议 + +当前建议: + +- `LOG_SOURCE_DIR` 作为日志服务的宿主机挂载目录 +- `SITE_DIR` 直接指向 homepage 仓库根目录 +- 优先先手动跑通一次: + +```bash +./scripts/deploy-homepage.sh +``` + +确认没问题后再挂 cron。 diff --git a/scripts/deploy-homepage.sh b/scripts/deploy-homepage.sh new file mode 100755 index 0000000..221d8bd --- /dev/null +++ b/scripts/deploy-homepage.sh @@ -0,0 +1,151 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +# 日志源目录:由日志生产服务写入 +# 默认值:/home/basil/bot/data/git-summary +LOG_SOURCE_DIR="${LOG_SOURCE_DIR:-/home/basil/bot/data/git-summary}" + +# 个人主页仓库目录 +# 默认值:/home/basil/source/personal-homepage +SITE_DIR="${SITE_DIR:-/home/basil/source/personal-homepage}" + +# 日志目标目录:复制到 homepage 仓库内 +# 默认值:$SITE_DIR/src/content/logs +LOG_TARGET_DIR="${LOG_TARGET_DIR:-${SITE_DIR}/src/content/logs}" + +# Docker Compose 配置与服务名 +# 默认 compose 文件:$SITE_DIR/docker-compose.yml +# 默认服务名:homepage +DOCKER_COMPOSE_FILE="${DOCKER_COMPOSE_FILE:-${SITE_DIR}/docker-compose.yml}" +DOCKER_SERVICE_NAME="${DOCKER_SERVICE_NAME:-homepage}" + +# 是否在同步时删除目标目录中源目录已不存在的文件 +# 默认值:false +RSYNC_DELETE="${RSYNC_DELETE:-false}" + +# 是否先在宿主机执行一次 npm run rebuild +# 默认值:true +RUN_LOCAL_REBUILD="${RUN_LOCAL_REBUILD:-true}" + +# 锁文件路径,避免定时任务重入 +# 默认值:/tmp/personal-homepage-deploy.lock +LOCK_FILE="${LOCK_FILE:-/tmp/personal-homepage-deploy.lock}" + +timestamp() { + date '+%Y-%m-%d %H:%M:%S' +} + +log() { + printf '[%s] %s\n' "$(timestamp)" "$*" +} + +fail() { + log "ERROR: $*" + exit 1 +} + +require_command() { + command -v "$1" >/dev/null 2>&1 || fail "缺少命令: $1" +} + +is_true() { + case "${1,,}" in + 1|true|yes|on) return 0 ;; + *) return 1 ;; + esac +} + +sync_logs() { + mkdir -p "$LOG_TARGET_DIR" + + if command -v rsync >/dev/null 2>&1; then + local -a rsync_args=(-av) + if is_true "$RSYNC_DELETE"; then + rsync_args+=(--delete) + fi + + log "同步日志: $LOG_SOURCE_DIR -> $LOG_TARGET_DIR" + rsync "${rsync_args[@]}" "${LOG_SOURCE_DIR}/" "${LOG_TARGET_DIR}/" + return 0 + fi + + log "未找到 rsync,改用 cp -a 复制日志" + + if is_true "$RSYNC_DELETE"; then + find "$LOG_TARGET_DIR" -mindepth 1 -maxdepth 1 -exec rm -rf {} + + fi + + cp -a "${LOG_SOURCE_DIR}/." "$LOG_TARGET_DIR/" +} + +run_local_rebuild() { + log "开始宿主机构建校验: npm run rebuild" + ( + cd "$SITE_DIR" + npm run rebuild + ) +} + +refresh_docker_service() { + log "刷新 Docker 服务: $DOCKER_SERVICE_NAME" + ( + cd "$SITE_DIR" + docker compose -f "$DOCKER_COMPOSE_FILE" up -d --build "$DOCKER_SERVICE_NAME" + ) +} + +main_impl() { + require_command docker + if is_true "$RUN_LOCAL_REBUILD"; then + require_command npm + fi + + docker compose version >/dev/null 2>&1 || fail "当前环境不可用 docker compose" + + [[ -d "$LOG_SOURCE_DIR" ]] || fail "日志源目录不存在: $LOG_SOURCE_DIR" + [[ -d "$SITE_DIR" ]] || fail "站点目录不存在: $SITE_DIR" + [[ -f "$DOCKER_COMPOSE_FILE" ]] || fail "docker compose 文件不存在: $DOCKER_COMPOSE_FILE" + + log "部署开始" + log "LOG_SOURCE_DIR=$LOG_SOURCE_DIR" + log "LOG_TARGET_DIR=$LOG_TARGET_DIR" + log "SITE_DIR=$SITE_DIR" + log "DOCKER_COMPOSE_FILE=$DOCKER_COMPOSE_FILE" + log "DOCKER_SERVICE_NAME=$DOCKER_SERVICE_NAME" + log "RSYNC_DELETE=$RSYNC_DELETE" + log "RUN_LOCAL_REBUILD=$RUN_LOCAL_REBUILD" + log "LOCK_FILE=$LOCK_FILE" + + sync_logs + + if is_true "$RUN_LOCAL_REBUILD"; then + run_local_rebuild + else + log "跳过宿主机 npm run rebuild(RUN_LOCAL_REBUILD=false)" + fi + + refresh_docker_service + + log "部署完成" +} + +main() { + if ! command -v flock >/dev/null 2>&1; then + log "未找到 flock,跳过锁保护" + main_impl "$@" + return 0 + fi + + mkdir -p "$(dirname "$LOCK_FILE")" + exec 9>"$LOCK_FILE" + + if ! flock -n 9; then + fail "已有部署任务在执行中,锁文件: $LOCK_FILE" + fi + + log "已获取部署锁: $LOCK_FILE" + main_impl "$@" +} + +main "$@" diff --git a/src/content/projects/index.json b/src/content/projects/index.json index 396c979..2ef489c 100644 --- a/src/content/projects/index.json +++ b/src/content/projects/index.json @@ -2,7 +2,7 @@ { "name": "personal-homepage", "description": "一个由 Markdown 与 JSON 驱动的公开个人主页,用于展示项目、开发日志与外部分享。", - "gitea_repo": "sepcomet/personal-homepage", + "gitea_repo": "basil/personal-homepage", "cover_image": "/images/projects/personal-homepage.svg", "demo_video": "", "download_link": "", @@ -12,7 +12,7 @@ { "name": "devlog-pipeline", "description": "规划中的开发日志生成与写入链路,用于把日常开发过程沉淀为结构化 Markdown 内容。", - "gitea_repo": "sepcomet/devlog-pipeline", + "gitea_repo": "basil/devlog-pipeline", "cover_image": "/images/projects/devlog-pipeline.svg", "demo_video": "", "download_link": "", @@ -22,7 +22,7 @@ { "name": "gitea-activity-panel", "description": "面向个人主页的 Gitea 活动概览模块,计划提供贡献热力图与近期活动列表。", - "gitea_repo": "sepcomet/gitea-activity-panel", + "gitea_repo": "basil/gitea-activity-panel", "cover_image": "/images/projects/gitea-activity-panel.svg", "demo_video": "", "download_link": "",