臭名昭著aTrust隔离计划1.臭名昭著aTrust隔离计划2.VPN原理与aTrust隔离网络实践3.docker-easyconnect到底做了什么4.TUN(tunnel-隧道-虚拟网卡)模式
个人项目
personal(个人展示)GitHub Actions 自动部署个人展示项目
基于Quartz搭建的个人博客1.Quartz个人博客使用教程2.使用 rsync 增量部署 Quartz 博客3.使用 GitHub Actions 自动部署 Quartz 博客5.域名绑定6.CDN加速7.Google 搜索收录排查:为什么关键词搜得到别人,却搜不到我的网站?Obsidian、双向链接与知识图谱
架构设计
分布式分布式事务RPC原理
旅行
行程攻略青岛三日游行程攻略
前端
NginxNginx配置与反向代理入门
Nodenpm与npx的区别
数据库
mysql01.MySQL基础语法与约束02.MySQL数据库类型03.MySQL多表查询04.MySQL 索引05.MySQL 事务06.MySQL 隔离级别07.MySQL MVCC08.MySQL 锁09.EXPLAIN10.慢查询与 SQL 优化
mysql面试题01.索引问题02.事务与隔离级别03.锁机制04.存储引擎与性能优化05.用户权限、字符集与备份恢复
mysql实战01.EXPLAIN实战02.测试环境菜单误删排查与恢复03.MySQL IN 详解
网络
网络安全1.无线监听网卡苹果M系列虚拟机安装Kali Linux教程3.kali怎么使用RTL8812AU4.kali基础命令
网络基础进程之间怎样通过网络传递数据
网址常用网址
中间键
esElasticsearch 入门
mq
RabbitMQRabbitMQ高级RabbitMQ基础
1. MQ 基础2. MQ 核心角色与代码对应3. MQ Consumer Group 与消费模式4. SOFAMQ 账户变更代码实践5. MQ 可靠性问题6. 消息幂等
redis缓存击穿、穿透与雪崩Redis 分布式锁Redis 缓存问题Redis 数据结构
AI
使用说明
第三方插件&技能简介Archify使用与安装指南geekgeekRun使用OfferFlow使用指南Ponytail使用指南Skills Manager使用指南
CodexCodex CLI与IDE区别及使用指南Codex Hook单独配置与提交通知Codex MCP安装与使用指南Codex第三方插件安装与使用指南
Agent开发0-学习路线1.LLM、Token、上下文窗口与模型参数2.大模型 API、请求参数与响应结构3.Spring AI ChatClient4.Prompt、System Prompt、Prompt 模板6.结构化输出、JSON Schema7.SSE 流式响应8.会话 ID、聊天记录、Redis9.超时、重试、限流、降级10.*完成可运行聊天接口11.Function Calling、Tool Calling 原理12.用 Java 方法定义 Tool13.工具参数设计和校验14.实现订单查询工具15.实现物流查询工具16.实现库存查询工具17.多个工具自动选择18.工具异常、超时、重试
diy主机
主机零件学习1.CPU2.主板3.内存4.显卡5.硬盘6.散热7.风扇8.机箱热门型号价格表
捡垃圾diy-13600kf+5060diy-14600kf+5070
English高频词汇英文基础语法简单句子音标
GitGit常用命令Sublime Merge 使用指南
Java
工具库Caffeine本地缓存Reactor教程
工作实践如何使用@Async线程池使用与排查线上服务 CPU 使用率过高排查压测下RPC接口大量失败排查记录
基础1.HashMap2.ConcurrentHashMap3.HashSet4.final5.static6.volatile7.AtomicInteger8.String动态代理反射锁与线程同步线程注解
面试题1.HashMap 存储在方法内是线程安全的吗2.ArrayList参数传递与引用重新赋值3.== 和 equals 的区别、hashcode4.重载和重写的区别5.String、StringBuffer、StringBuilder区别及使用场景6.接口和抽象类7.ArrayList 与 LinkedList 的区别和扩容机制8.HashMap 和 Hashtable 的区别及底层实现9.Object 和 Objects 的区别10.ThreadLocal的原理和使用场景11.字节码是什么及作用12.Java类加载器有哪些13.双亲委派模型14.Java异常体系15.GC是什么16.GC如何判断对象可以被回收
面试总结0.HR面试模板1.面试一2.面试二3.面试三笔试
网络与 IO(黑马程序员-纯理论)01-网络编程基础02-IO模型与零拷贝03-BIO04-NIO基础与缓冲区05-NIO通道与选择器06-AIO
juc00-目录01-进程与线程02-线程同步与通信03-并发设计模式04-Java内存模型05-CAS与原子类06-ThreadLocal07-阻塞队列08-线程池使用与原理09-任务调度10-ForkJoin与享元模式11-AQS12-ReentrantLock13-读写锁14-并发协作工具15-ConcurrentHashMap16-CopyOnWrite与同步容器17-SkipListMap18-非阻塞队列
juc(工作常见)Java并发编程三个重点拆解
jvm1. JVM运行时数据区
Spring1.Spring IoC、DI与构造方法注入CGLIB、Byte Buddy 与 Spring AOPSpring 常用注解详解Spring Bean 生命周期
Spring MVC异常处理机制
linux常用命令
mac快捷键终端常用操作
python
基础01.Python是什么,为什么要安装Python02.Python项目中.venv是什么03.Python基础语法
基础实战sql替换
HTTPXHRMS员工详情接口调用(Python HTTPX)
windowswindows激活

一、一句话理解

把 Quartz 项目推送到 GitHub 后,由 GitHub Actions 临时启动一台云端 Linux 机器,自动安装依赖、构建 public,最后通过 SSH 和 rsync 把网站同步到宝塔服务器。

配置完成后,日常发布只需执行:

git add .
git commit -m "博客搭建笔记-更新文章内容"
git push origin master

不再需要每次手工运行构建和上传命令。

二、理论:自动部署是什么

2.1 Git 负责保存源码

Git 仓库保存的是 Markdown、Quartz 配置、主题代码和依赖声明,不保存临时生成的 public。

本项目当前使用:

  • GitHub 仓库:yunyunafyke-sketch/quartz-personal-blog
  • 自动部署分支:master
  • Quartz 版本:Quartz 5
  • Node.js:使用 Node.js 22 构建
  • 构建命令:npx quartz build

2.2 GitHub Actions 启动临时 Linux 机器

GitHub Actions 可以理解成 GitHub 提供的“临时云电脑”。每次工作流运行时,GitHub 会启动一台干净的 Linux 机器,供这一次部署使用。

这台机器会:

  1. 从 GitHub 下载当前 master 分支的源码。
  2. 安装 Node.js、Quartz 和其他依赖。
  3. 执行构建,生成 public。
  4. 连接宝塔服务器并上传构建结果。

工作流结束后,临时机器和其中的文件会被回收。它不是你的宝塔服务器,也不会长期运行 Quartz;真正长期保存网站文件的仍是:

/www/AfykeBlog/afyke-blog

这样做的好处是:服务器只负责提供网页,不需要常驻 Node.js、npm 和 Quartz 构建环境。以后更换电脑也不影响发布,只要把笔记推送到 GitHub 即可。

每次 master 分支收到新的提交后,它会按照 .github/workflows/deploy.yml 中写好的步骤执行:

  1. 拉取项目代码。
  2. 安装 Node.js。
  3. 安装 npm 依赖和 Quartz 插件。
  4. 构建 Quartz。
  5. 处理可能造成无意义差异的 Explorer 编号。
  6. 使用 SSH 密钥连接服务器。
  7. 使用 rsync 增量同步 public。

Quartz 官方的 GitHub Actions 示例同样采用“拉取完整 Git 历史、安装 Node.js 和依赖、构建 public”的流程。参考:Quartz Hosting。

2.3 npm ci 安装锁定的依赖

Quartz 不是 Mac 或 GitHub 自带的命令,它依赖很多 Node.js 软件包,例如 Quartz 本身、Markdown 解析器、代码高亮工具、Mermaid 渲染工具和 Playwright 浏览器组件。

npm ci 的作用是:按照项目的 package-lock.json,把这些软件包的固定版本安装到临时 Linux 机器中。

可以把两个文件理解为:

文件作用
package.json声明项目需要哪些类型的软件包,以及允许的版本范围
package-lock.json记录这次项目实际使用的每一个具体版本

例如,package.json 可以表达“需要某个工具的 5.x 版本”,而 package-lock.json 会进一步固定为“这次使用 5.0.0,以及它依赖的其他具体版本”。

因此自动部署使用 npm ci,而不是普通的 npm install:

  • npm ci 严格按锁文件安装,package.json 与锁文件不一致时会直接失败。
  • npm ci 不会擅自更新锁文件。
  • 每次云端构建使用相同依赖版本,结果更稳定,也更容易排查问题。

它安装的是“构建网站所需工具”,不是安装你的 Markdown 内容。Markdown 仍然来自 GitHub 拉取的项目源码。

2.4 SSH 和 rsync 负责发布

两者的分工是:

  • SSH:验证 GitHub Actions 是否有权限登录服务器,并提供加密连接。
  • rsync:比较云端构建结果和服务器已有文件,只同步内容真正变化的文件。

自动化程序无法像人在终端中一样输入服务器密码,因此需要使用专门的 SSH 密钥。

三、理论:完整流程是怎么工作的

本地 Markdown 和 Quartz 配置
              ↓
        git push origin master
              ↓
       GitHub 保存最新源码
              ↓
  GitHub Actions 启动临时 Linux 机器
              ↓
       npm ci 安装锁定的依赖
              ↓
       npx quartz build
              ↓
        生成 public 静态网站
              ↓
       SSH 密钥连接宝塔服务器
              ↓
       rsync 增量同步并删除旧文件
              ↓
      /www/AfykeBlog/afyke-blog

GitHub Actions 每次运行结束后,临时机器会被回收。服务器只保存最终网站文件,GitHub 保存可以重新构建网站的源码。

四、准备专用 SSH 部署密钥

4.1 为什么要单独生成密钥

不要把自己日常使用的 SSH 私钥交给自动化程序。建议为博客部署单独生成一对密钥:

  • 公钥放到服务器,负责声明“允许谁登录”。
  • 私钥保存到 GitHub Secrets,负责证明 GitHub Actions 的身份。

如果以后不再使用自动部署,只需从服务器删除这把公钥并删除 GitHub Secret,不会影响其他 SSH 登录方式。

4.2 在 Mac 上生成密钥

在 Mac 本地终端执行:

ssh-keygen \
  -t ed25519 \
  -C "github-actions-quartz" \
  -f ~/.ssh/afyke_quartz_actions

这四行合起来是一条命令。行尾的 \ 表示“命令还没结束,下一行接着写”,只是为了让内容更容易阅读。

部分含义
ssh-keygenMac 自带的 SSH 密钥生成工具
-t ed25519选择 ed25519 这种现代的密钥类型
-C "github-actions-quartz"写入备注,方便日后识别这把密钥用于 GitHub Actions;不影响登录能力
-f ~/.ssh/afyke_quartz_actions指定密钥名称和保存位置;~ 代表当前 Mac 用户目录 /Users/chenweili

这条命令只会在 Mac 本地生成文件,不会连接服务器,也不会上传任何内容。

生成两个文件:

~/.ssh/afyke_quartz_actions       私钥,必须保密
~/.ssh/afyke_quartz_actions.pub   公钥,可以放到服务器

没有 .pub 后缀的是私钥,之后放入 GitHub 的 SSH_PRIVATE_KEY Secret;有 .pub 后缀的是公钥,之后放入服务器的 authorized_keys。

如果终端提示目标文件已存在,不要直接确认覆盖。说明之前可能已经生成过同名密钥,应先确认它是否就是这把部署密钥。

自动部署专用密钥通常不设置交互式口令,否则 GitHub Actions 无法在无人值守状态下使用。

命令运行过程中会询问是否设置 passphrase。当前工作流没有配置口令解锁步骤,因此本教程的专用部署密钥应直接按两次回车留空。这样做只适用于这把单独创建、只交给 GitHub Secrets 的部署密钥;不要因此去掉日常 SSH 私钥的口令。

4.3 公钥和私钥是如何匹配的

可以把这对密钥理解成一把锁和一把唯一的钥匙:

Mac 生成同一对密钥
├── 公钥 → 放入服务器 authorized_keys
└── 私钥 → 放入 GitHub Secrets
 
GitHub Actions 使用私钥证明身份
服务器使用公钥验证身份
验证成功 → 允许登录

服务器不是把公钥和私钥的文字直接进行比较。实际验证过程是:

  1. GitHub Actions 连接服务器,并声明要使用这把公钥对应的身份。
  2. 服务器检查 /root/.ssh/authorized_keys,确认其中存在该公钥。
  3. GitHub Actions 使用私钥,对本次连接生成一次数学签名。
  4. 服务器使用保存的公钥验证签名。
  5. 验证成功后,服务器允许 SSH 登录;验证失败则显示 Permission denied (publickey)。

私钥不会通过网络发送给服务器。公钥可以验证签名,但无法反推出私钥,因此服务器只保存公钥也足够完成验证。

4.3.1 “数学签名”到底在做什么

这里的“签名”不是把名字写在文件上,而是一段只能由私钥算出来的验证结果。可以把一次 SSH 登录想成下面这段对话:

服务器:这是我刚刚随机生成的一道临时题目 X,请证明你有对应私钥。
GitHub Actions:我用私钥对 X 算出签名 S,发给你。
服务器:我用已保存的公钥检查 S 是否确实对应 X。

检查通过,服务器只能得出一个结论:这次连接的一方确实持有与该公钥配对的私钥;它看不到、也拿不到私钥本身。

题目 X 每次连接都会重新生成,因此即使有人截获一次旧签名,也不能拿它通过下一次登录验证。

4.3.2 为什么公钥不能反推出私钥

本教程生成的是 ed25519 密钥。它利用一种具有“单向性”的数学关系:

  • 用私钥可以很容易计算出公钥;
  • 已知公钥,却几乎不可能在可接受的时间内倒推出私钥;
  • 私钥能为临时题目生成签名;公钥只能验证签名是否正确,不能自己伪造签名。

这不是因为服务器故意不去看私钥,而是数学上做不到高效反推。就像知道一道非常大的乘法结果,想反推出原来的两个大质数会极其困难;ed25519 使用的具体数学不同,但安全思路相同:正向容易,反向极难。

GitHub Actions 运行时,工作流会把 SSH_PRIVATE_KEY Secret 临时写成:

~/.ssh/deploy_key

随后 rsync 通过这把临时私钥连接服务器。工作流结束后,GitHub 的临时 Linux 机器会被回收。

4.4 把公钥加入服务器

先查看公钥:

cat ~/.ssh/afyke_quartz_actions.pub

复制输出的整行内容,然后登录服务器:

ssh -p 22 root@47.99.178.186

在服务器中执行:

mkdir -p ~/.ssh
chmod 700 ~/.ssh

编辑:

/root/.ssh/authorized_keys

把刚才复制的公钥单独放在一行,保存后执行:

chmod 600 ~/.ssh/authorized_keys

4.4.1 这两个 chmod 权限是什么意思

SSH 会检查密钥目录和公钥白名单是否足够私密;若其他账号可以随意读取或修改,SSH 可能拒绝使用它们。因此需要执行:

chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys

数字按“文件主人、同组用户、其他用户”的顺序写:4 是读取、2 是写入、1 是进入目录或执行,数字可以相加。

命令权限含义在本教程中的实际效果
chmod 700 ~/.ssh主人有读、写、进入目录权限;其他人没有权限只有当前登录的 root 用户能进入 /root/.ssh
chmod 600 ~/.ssh/authorized_keys主人可读、可写;其他人没有权限只有 root 能读取或修改允许登录的公钥清单

这里以 root 登录服务器,所以 ~ 指向 /root。这两条命令不会修改公钥内容,只是收紧访问权限。

4.5 测试密钥登录

退出服务器,回到 Mac 后执行:

ssh \
  -i ~/.ssh/afyke_quartz_actions \
  -p 22 \
  root@47.99.178.186

这条命令的意思是:使用指定的私钥,以 root 身份登录这台服务器。

部分含义
ssh远程登录服务器。
\只是把一条长命令分成多行写,实际仍是一条命令。
-i ~/.ssh/afyke_quartz_actions指定使用刚生成的部署私钥。
-p 22连接服务器的 22 端口。
root@47.99.178.186用 root 用户登录 IP 为 47.99.178.186 的服务器。
> ssh \
  -i ~/.ssh/afyke_quartz_actions \
  -p 22 \
  root@47.99.178.186
 
Welcome to Alibaba Cloud Elastic Compute Service !
 
Activate the web console with: systemctl enable --now cockpit.socket
 
Last login: Fri Aug 14 16:10:16 2026 from 185.220.236.142

如果不再要求输入服务器密码,就说明密钥配置成功。

五、准备服务器身份信息

5.1 获取 known_hosts

known_hosts 用来验证 GitHub Actions 连接的确实是自己的服务器,避免被伪造服务器截获部署内容。

这里会用到两套用途不同的密钥,不要混淆:

密钥谁持有私钥用途
部署密钥GitHub ActionsGitHub Actions 证明“我是被允许登录的人”。
服务器身份密钥服务器服务器证明“我确实是这台服务器”。

服务器安装 SSH 服务时,会自动生成自己的身份密钥对:私钥留在服务器内部,公钥可以告诉所有来访者。ssh-keyscan 取得的正是这套密钥中的服务器公钥;它不是前面生成的部署公钥。

GitHub Actions 将服务器公钥保存到 known_hosts 后,之后部署时会核对服务器返回的身份是否一致:一致才连接;不一致便拒绝连接,防止误把博客文件传给冒充的服务器。

5.1.1 SSH 的双向身份验证

SSH 连接中,两边都要验明身份:

① GitHub Actions 验证服务器
   GitHub:你真是我配置的那台服务器吗?
   服务器:用自己的身份私钥证明。
   GitHub:用 known_hosts 中保存的服务器公钥核对。
 
② 服务器验证 GitHub Actions
   服务器:你真是被允许登录的 GitHub Actions 吗?
   GitHub:用部署私钥证明。
   服务器:用 authorized_keys 中保存的部署公钥核对。

两边都验证成功,才会建立加密连接并开始使用 rsync 上传博客文件。因此,known_hosts 是 GitHub Actions 保存的“服务器身份证”,authorized_keys 是服务器保存的“允许 GitHub 使用的钥匙”。

在 Mac 上执行:

ssh-keyscan -p 22 47.99.178.186 > /tmp/afyke-quartz-known-hosts

这条命令不会登录服务器,也不会修改服务器;它只是取得服务器的 SSH 身份公钥,并保存到 Mac 的临时文件中:

部分含义
ssh-keyscan向服务器询问“你的 SSH 身份公钥是什么?”
-p 22询问 22 端口。
47.99.178.186询问这台服务器。
>把查询结果写入文件。
/tmp/afyke-quartz-known-hostsMac 上临时存放结果的位置。

查看准备保存到 GitHub 的内容:

cat /tmp/afyke-quartz-known-hosts

条件允许时,应通过宝塔终端或云服务器控制台核对 SSH 主机密钥指纹,再把内容保存到 GitHub。

5.2 查看部署私钥

在 Mac 上执行:

cat ~/.ssh/afyke_quartz_actions

复制包括下面两行标记在内的完整内容:

-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----

私钥只能放入 GitHub Secrets,不要发送给他人,不要写入 Markdown、工作流文件或 Git 仓库。

六、配置 GitHub Actions Secrets

6.1 打开配置页面

进入 GitHub 仓库:

yunyunafyke-sketch/quartz-personal-blog

依次点击:

Settings
→ Secrets and variables
→ Actions
→ Secrets
→ New repository secret

GitHub 官方说明见:Using secrets in GitHub Actions。

6.2 添加部署配置

添加以下 Repository secrets:

Secret 名称填写内容
SSH_PRIVATE_KEY~/.ssh/afyke_quartz_actions 的完整私钥
SSH_KNOWN_HOSTS/tmp/afyke-quartz-known-hosts 的完整内容
SERVER_HOST47.99.178.186
SERVER_PORT22
SERVER_USERroot
SERVER_PATH/www/AfykeBlog/afyke-blog

GitHub Secrets 保存后不能再次查看明文,只能覆盖更新,这是正常的安全设计。

七、创建自动部署工作流

7.1 创建文件

在 Quartz 项目根目录创建:

.github/workflows/deploy.yml

这里的项目根目录是:

/Users/chenweili/Documents/quartz

不要把工作流放进 content 或 public。

7.2 完整工作流

写入:

name: 自动部署 Quartz 博客
 
on:
  push:
    branches:
      - master
  workflow_dispatch:
 
permissions:
  contents: read
 
concurrency:
  group: afyke-blog-production
  cancel-in-progress: true
 
jobs:
  deploy:
    runs-on: ubuntu-latest
    timeout-minutes: 20
 
    steps:
      - name: 拉取项目源码
        uses: actions/checkout@v6
        with:
          fetch-depth: 0
 
      - name: 安装 Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 22
          cache: npm
 
      - name: 安装项目依赖
        run: npm ci
 
      - name: 安装 Quartz 插件
        run: npx quartz plugin install
 
      - name: 安装 Mermaid 构建所需浏览器
        run: npx playwright install --with-deps chromium
 
      - name: 构建 Quartz
        run: npx quartz build
 
      - name: 检查构建结果
        run: test -f public/index.html
 
      - name: 稳定 Explorer 页面编号
        run: |
          find public -type f -name '*.html' \
            -exec perl -pi -e 's/explorer-\d+/explorer/g' {} +
 
      - name: 配置服务器 SSH 密钥
        env:
          SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
          SSH_KNOWN_HOSTS: ${{ secrets.SSH_KNOWN_HOSTS }}
        run: |
          install -m 700 -d ~/.ssh
          printf '%s\n' "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key
          printf '%s\n' "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
          chmod 600 ~/.ssh/deploy_key ~/.ssh/known_hosts
 
      - name: 发布到宝塔服务器
        env:
          SERVER_HOST: ${{ secrets.SERVER_HOST }}
          SERVER_PORT: ${{ secrets.SERVER_PORT }}
          SERVER_USER: ${{ secrets.SERVER_USER }}
          SERVER_PATH: ${{ secrets.SERVER_PATH }}
        run: |
          rsync -azc --no-times --delete \
            -e "ssh -i ~/.ssh/deploy_key -p ${SERVER_PORT} -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes" \
            public/ \
            "${SERVER_USER}@${SERVER_HOST}:${SERVER_PATH}/"

7.3 关键配置说明

配置作用
branches: master只有推送到当前正式分支时才部署
workflow_dispatch允许在 GitHub 页面手动执行部署
contents: read将 GitHub Token 权限限制为只读
concurrency避免两次部署同时覆盖服务器文件
fetch-depth: 0拉取完整 Git 历史,让文章修改时间计算准确
node-version: 22与当前本地环境保持一致,并满足 Quartz 5 的 Node.js 版本要求
npm ci严格按照 package-lock.json 安装依赖
playwright install为当前项目的静态 Mermaid 渲染准备 Chromium
test -f没有生成首页时立即停止部署
-c按内容校验和判断文件是否变化
--no-times不因构建时间不同而同步文件
--delete删除服务器上、本地 public 已不存在的旧文件

GitHub 官方推荐明确限制工作流权限,并通过 Secrets 向步骤传递敏感信息。参考:Secure use reference。

八、处理 Explorer 编号造成的无意义差异

8.1 问题原因

当前 Explorer 插件使用全局递增编号:

const id = `explorer-${numExplorers++}`

新增一篇文章后,旧页面可能从:

id="explorer"

变成:

id="explorer"

正文虽然没变,但 rsync 会正确识别到 HTML 字节发生了变化。

8.2 自动规范化

工作流在构建后执行:

find public -type f -name '*.html' \
  -exec perl -pi -e 's/explorer-\d+/explorer/g' {} +

当前每个页面只有一个 Explorer,因此不同 HTML 页面都使用固定的 explorer ID 不会冲突。

第一次启用规范化时,服务器旧页面仍然包含数字编号,可能会集中同步一次。完成这次部署后,新增文章不会再仅因 Explorer 编号变化而重复上传无关页面。

8.3 仍然应该变化的文件

新增文章后,下列文件发生变化是正常的:

  • 新文章 HTML。
  • 所在文件夹的目录页面。
  • 首页或栏目入口。
  • index.xml。
  • sitemap.xml。
  • static/contentIndex.json。
  • 新文章的 OG 图片。

这些文件包含全站目录、搜索、RSS 或站点地图数据,不属于无意义变化。

九、第一次启动自动部署

9.1 提交工作流

在 Mac 的 Quartz 项目根目录执行:

git add .github/workflows/deploy.yml
 
git commit -m "部署配置-新增 Quartz 自动发布流程"
 
git push origin master

提交说明使用中文,明确说明本次增加了什么。

9.2 查看运行过程

打开 GitHub 仓库的:

Actions
→ 自动部署 Quartz 博客
→ 最新一次运行

可以看到每个步骤的状态:

  • 绿色对勾:执行成功。
  • 黄色圆点:正在执行。
  • 红色叉号:执行失败,点击步骤查看错误日志。

9.3 手动触发

工作流配置了:

workflow_dispatch:

因此可以进入 Actions 页面,选择工作流后点击 Run workflow 手动执行。

GitHub 支持使用 push、手动触发和并发控制组织部署流程。参考:Deploying with GitHub Actions。

十、以后如何发布文章

10.1 日常发布流程

以后修改或新增笔记后,只需:

cd /Users/chenweili/Documents/quartz
 
git add content
 
git commit -m "博客搭建笔记-更新自动部署说明"
 
git push origin master

只要推送成功,GitHub Actions 就会自动开始构建和部署。

10.2 不需要再手工执行的命令

自动化生效后,日常发布不再需要在 Mac 上手工执行:

npx quartz build

也不再需要手工执行:

rsync ...

本地仍然可以执行 npx quartz build --serve 预览文章,但是否预览与服务器自动部署互不影响。

十一、常见问题

11.1 Permission denied (publickey)

表示服务器不接受 GitHub Actions 使用的私钥。依次检查:

  1. SSH_PRIVATE_KEY 是否包含完整私钥。
  2. 服务器 authorized_keys 中是否存在对应公钥。
  3. SERVER_USER 是否正确。
  4. .ssh 是否为 700 权限。
  5. authorized_keys 是否为 600 权限。

11.2 Host key verification failed

表示 SSH_KNOWN_HOSTS 缺失、内容错误,或者服务器重装后主机密钥发生变化。

重新获取并核对:

ssh-keyscan -p 22 47.99.178.186

确认安全后,覆盖更新 GitHub 中的 SSH_KNOWN_HOSTS。

不要为了跳过错误而设置:

StrictHostKeyChecking=no

这会失去服务器身份验证能力。

11.3 npm ci 失败

常见原因包括:

  • package.json 和 package-lock.json 不一致。
  • 依赖没有完整提交。
  • npm 官方源暂时不可访问。
  • Quartz 插件版本或锁文件不完整。

先在本地项目根目录执行相同的:

npm ci
npx quartz plugin install
npx quartz build

如果本地也失败,应先修复依赖问题,再重新推送。

11.4 Playwright 或 Chromium 失败

当前项目会在构建时使用浏览器把 Mermaid 转换为静态 SVG,因此工作流增加了:

npx playwright install --with-deps chromium

如果仍然失败,重点查看 安装 Mermaid 构建所需浏览器 和 构建 Quartz 两个步骤的日志。

11.5 rsync code 23

通常表示部分文件无法写入。检查:

  • SERVER_PATH 是否正确。
  • 部署用户是否有目标目录写入权限。
  • 磁盘空间是否充足。
  • 服务器目录中是否存在权限异常的旧文件。

11.6 部署成功但页面没有更新

依次检查:

  1. GitHub Actions 是否真的运行了最新提交。
  2. SERVER_PATH 是否为网站实际根目录。
  3. Nginx 或 Caddy 是否指向同一个目录。
  4. 浏览器或 CDN 是否缓存了旧页面。
  5. Quartz 的 baseUrl 是否正确。

11.7 Actions 页面没有 Run workflow

确认工作流已经进入默认分支,并包含:

workflow_dispatch:

新工作流第一次必须先提交并推送,GitHub 页面才会识别。

十二、安全边界与常见误区

12.1 不要提交私钥

以下内容不能出现在 Git 仓库:

  • SSH 私钥。
  • 服务器密码。
  • 宝塔账号密码。
  • 数据库密码。
  • GitHub Token。

一旦误提交,即使后来删除文件,也应立即撤销并更换对应密钥,因为敏感信息可能仍然存在于 Git 历史中。

12.2 尽量不要长期使用 root

本教程为了沿用当前部署方式,暂时使用:

root@47.99.178.186

更安全的正式方案是创建专门的部署用户,只授予:

/www/AfykeBlog/afyke-blog

目录的读写权限。即使部署私钥泄漏,攻击范围也会更小。

12.3 不要把 -n 放进正式工作流

-n 表示只预演:

rsync -n ...

如果自动部署命令包含 -n,Actions 会显示执行成功,但服务器不会收到任何文件。

12.4 谨慎使用 —delete

--delete 会删除服务器目标目录中、本地 public 不存在的内容。

只有确认:

/www/AfykeBlog/afyke-blog

专门用于存放 Quartz 构建结果时,才应使用该参数。不要在该目录中同时保存手工文件、备份或其他网站内容。

12.5 不要提交 public

public 是可重复生成的产物,已经被 .gitignore 忽略。正确方式是提交源码,让 GitHub Actions 重新构建,而不是把成百上千个 HTML 提交到 Git。

12.6 不要在不受信任的代码上开放部署密钥

自动部署只监听正式的 master 分支,不要直接让外部 Pull Request 获得服务器 Secrets。

GitHub 官方建议工作流遵守最小权限原则,并谨慎处理能够读取 Secrets 的代码修改权限。参考:GitHub Actions 安全使用说明。

十三、如何回滚

13.1 回滚原理

服务器只保存当前构建结果,真正的版本历史保存在 Git 中。

如果新文章或配置部署后有问题,可以撤销对应 Git 提交,再次推送。GitHub Actions 会重新构建旧版本内容,并通过 rsync 恢复服务器。

13.2 使用 revert 回滚

先查看提交记录:

git log --oneline

找到需要撤销的提交后执行:

git revert 提交编号
git push origin master

git revert 会新增一条“撤销”提交,不会破坏已有 Git 历史,适合已经推送到远程的内容。

十四、GitHub 和 Gitee 怎么选

14.1 当前项目优先使用 GitHub

当前 Quartz 项目已经配置 GitHub 远程仓库,直接增加 GitHub Actions 所需改动最少:

origin = https://github.com/yunyunafyke-sketch/quartz-personal-blog.git

因此本教程默认使用 GitHub。

14.2 Gitee 的原理相同

如果以后改用 Gitee,整体流程不变:

推送源码
→ CI 平台安装 Node.js
→ 构建 Quartz
→ 使用 SSH 密钥连接服务器
→ rsync 同步 public

区别主要在流水线配置文件、Secret 配置入口、可用构建环境和平台权限。没有迁移需求时,不必同时维护 GitHub 和 Gitee 两套自动部署。

十五、总结

自动部署的核心不是让服务器自己长期运行 Quartz,而是让 GitHub Actions 在每次推送后临时完成构建,再把静态网站同步到服务器。

最终日常流程可以压缩成:

写笔记
→ 中文 Git 提交
→ git push origin master
→ GitHub Actions 自动构建
→ rsync 自动发布

一句话记忆:Git 保存源码,GitHub Actions 负责构建,SSH 负责连接,rsync 负责发布。