用GitHub Actions自动构建EGO博客
Table of Contents
1. 背景
2. 最终效果
每次 push 到 source 分支时,GitHub Actions 会自动:
- 用 EGO 将
.org文件转换为 HTML(增量构建) - 将生成的 HTML 推送到
master分支 - GitHub Pages 自动部署
顺带还会在 Habitica 上完成"写博客"任务打个卡(游戏化激励)。
3. 实现过程
3.1. 第一步:改造 auto_publish.el
原来 auto_publish.el 里的路径都是硬编码的:
(setq load-path (cons "~/EGO/" load-path))
这在本地没问题,但在 CI 环境里路径完全不同。所以我把所有硬编码路径改为从环境变量读取,保留默认值确保本地兼容:
(setq load-path (cons (or (getenv "EGO_DIR") "~/EGO/") load-path))
ego-project-config-alist 中的 :repository-directory 和 :store-dir 也做了类似处理:
`(("blog" :repository-directory ,(or (getenv "REPO_DIR") "~/source") ... :store-dir ,(or (getenv "STORE_DIR") "~/web")))
最后,用 condition-case 包裹 ego-do-publication 调用,确保 EGO 出错时不会静默失败:
(condition-case err (ego-do-publication "blog" nil nil nil) (error (message "EGO ERROR: %s" (error-message-string err)) (message "EGO ERROR backtrace:") (backtrace)))
3.2. 第二步:编写 GitHub Actions Workflow
最终的 workflow 文件 .github/workflows/github-action.yml :
name: 提交 habitica 任务 + 构建博客 on: push: branches: [source] permissions: contents: write jobs: Finish-Habitica-Tasks: runs-on: ubuntu-latest steps: - name: Check out habash code uses: actions/checkout@v4 with: fetch-depth: 0 repository: nasfarley88/habash - name: 确保 habash 可执行 run: chmod +x ./habash - name: 提交 habitica 任务 run: | set -x ./habash up "写博客" env: HABITICA_TOKEN: ${{ secrets.HABITICA_TOKEN }} HABITICA_UUID: ${{ secrets.HABITICA_UUID }} Build-Blog: runs-on: ubuntu-latest env: CI: true EGO_DIR: /tmp/EGO REPO_DIR: ${{ github.workspace }} STORE_DIR: /tmp/web REPO: https://github.com/lujun9972/${{ github.event.repository.name }} steps: - name: Checkout 博客源文件 uses: actions/checkout@v4 with: fetch-depth: 0 ref: source - name: Checkout EGO run: git clone https://github.com/lujun9972/EGO.git "$EGO_DIR" - name: Checkout master 分支到 store-dir run: | git clone --branch master --single-branch \ https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git \ "$STORE_DIR" cd "$STORE_DIR" git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - name: 安装 Emacs 和系统依赖 run: sudo apt-get update && sudo apt-get install -y emacs-nox - name: 执行 auto_publish.el 构建博客 run: | git config core.quotePath false emacs --batch -l auto_publish.el - name: 推送 HTML 到 master 分支 run: | cd "$STORE_DIR" git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add -A git diff --staged --quiet || git commit -m "auto publish: $(date -u +%Y-%m-%dT%H:%M:%SZ)" git push origin master
两个 Job 并行运行,互不影响。关键设计决策:
- **EGO_DIR 和 STORE_DIR 指向 /tmp**:不在
github.workspace内,避免污染 source 仓库的 git 状态(详见坑3) - **~git config core.quotePath false~**:解决 EGO 无法识别中文路径的问题(详见坑4)
- **增量构建**:~ego-do-publication~ 第二个参数为 ~nil~,只重建变更文件
3.3. 第三步:踩过的坑
3.3.1. 坑1:EGO 缺少依赖
auto_publish.el 在本地运行时,~ht~ 和 dash 两个包早就安装过了。但 CI 是全新环境,~package-install~ 列表里没有它们,导致 EGO 加载失败:
Cannot open load file: No such file or directory, ht
解决:在 package-install 列表中加上 ht 和 ~dash~。
3.3.2. 坑2:EGO 内部 git commit 缺少身份
EGO 构建完后会用 vc-git-commit 提交 store-dir 的变更,但 store-dir 里没配 git user:
Failed (status 128): git –no-pager commit -m Update published html files…
解决:clone master 分支后立即配置 ~git config user.name/email~。
3.3.3. 坑3:EGO 的 stash/pop 在 CI 中失败
这是最有意思的一个坑。
EGO 在发布博客前,会通过 ego-git-repo-up2date-p 检查 repo 是否干净:
(defun ego-git-repo-up2date-p (repo-dir) (let* ((default-directory (file-name-as-directory repo-dir)) (state (vc-git-state ""))) (equal state 'up-to-date)))
如果检测到 state 不是 up-to-date~,EGO 会先 ~git stash 保护变更,构建完成后再 git stash pop 恢复。但在 CI 中 stash pop 总是失败:
Failed (status 1): git –no-pager stash pop -q 0 .
我一开始以为是 CI 环境 git 版本的问题,试了 git clean -fd~、~git checkout -- . 确保构建前 repo 干净,都没用。试了全量构建模式 (ego-do-publication "blog" t nil "CI auto publish") 绕过 stash,虽然能工作但丢失了增量构建的优势。
后来仔细看 CI 日志,发现了真正的原因:
repo home/runner/work/lujun9972.github.com/lujun9972.github.com state is edited
repo 是 edited 而不是 ~up-to-date~! 明明是从空白环境 checkout 出来的,怎么会有未提交的变更?
答案是:我最初把 EGO 和 master 分支都 checkout 到了 github.workspace 的子目录下:
EGO_DIR: ${{ github.workspace }}/EGO # ❌ 在 workspace 内 STORE_DIR: ${{ github.workspace }}/web # ❌ 在 workspace 内
这样 github.workspace~(即 source 分支的 git repo)下面多了 ~EGO/ 和 web/ 两个未跟踪目录,~vc-git-state~ 自然返回 ~edited~。
解决:把 EGO 和 master 分支 checkout 到 /tmp 下,与 source repo 完全隔离:
EGO_DIR: /tmp/EGO # ✅ 在 workspace 外 STORE_DIR: /tmp/web # ✅ 在 workspace 外
改完后日志变为:
repo home/runner…/lujun9972.github.com/ state is up-to-date
EGO 不再触发 stash,增量构建也正常工作了。
3.3.4. 坑4:中文路径导致 git ls-tree 无法识别 .org 文件
这个坑藏得最深,折腾了我很久。
EGO 的 ego-git-get-all-files 函数用 git ls-tree -r --name-only HEAD 获取所有 org 文件,然后用 string-suffix-p ".org" 过滤:
(defun ego-git-get-all-files (repo-dir &optional branch) (let ((output (ego--shell-command repo-dir (concat "env LC_ALL=C git ls-tree -r --name-only " (or branch "HEAD")) t))) (delq nil (mapcar #'(lambda (line) (when (string-suffix-p ".org" line t) (expand-file-name line repo-dir))) (split-string output "\n")))))
看起来没问题对吧?但 git ls-tree 默认会对非 ASCII 路径进行八进制转义:
"Emacs\344\271\213\346\200\222/Connection Local Variable.org"
注意路径被双引号包裹,中文变成了 \344\271\213\346\200\222~。这样一来,每行的实际结尾是 ~.org"~(带引号),而不是 ~.org~,导致 ~string-suffix-p ".org" 匹配失败。
结果就是只有纯 ASCII 路径的 about.org 和 README.org 能被匹配到,整个博客 663 个 org 文件中只有这 2 个被 EGO 识别。首页也只显示了 about 一个分类。
解决:在运行 EGO 前设置 ~git config core.quotePath false~,让 git 输出原始 UTF-8 路径:
- name: 执行 auto_publish.el 构建博客 run: | git config core.quotePath false emacs --batch -l auto_publish.el
改完后 EGO 正确识别了所有 662 个 org 文件,首页也恢复了完整的 10 个分类。
3.3.5. 坑5:force-all 模式的静默失败
在调试坑4 的过程中,我尝试用 ~force-all=t~(全量构建)来绕过问题:
(ego-do-publication "blog" t nil nil) ; 第二个参数 t = force-all
但 EGO 静默失败了——没有任何错误输出,构建日志在 "current-branch is already source" 之后就断了。
查看 EGO 源码发现,~force-all=t~ 时 base-git-commit 会被设为 ~nil~:
(base-git-commit (unless force-all ; force-all=t → nil! (or (ego--get-first-commit-before-publish ...) "HEAD~1")))
而 base-git-commit 为 nil 时,EGO 的 changed-files 会变成 ~(:update 全部文件 :delete 全部文件)~,即同时删除并重建所有文件。这个逻辑矛盾导致了静默失败。
解决:使用增量构建模式 ~force-all=nil~,让 EGO 自动根据 master 分支的上次发布时间计算变更:
(ego-do-publication "blog" nil nil nil) ; 增量构建
4. 总结
整个折腾过程有个关键收获:
- **checkout 路径隔离**:GitHub Actions 中多个仓库的 checkout 不要互相嵌套,否则会污染父仓库的 git 状态
- **中文路径注意 ~core.quotePath~**:git 默认转义非 ASCII 路径,在需要解析路径的场景下记得关闭
虽然中间踩了不少坑,但最终效果还是很满意的——以后 push 博客源文件就能自动构建发布,再也不用手动跑 auto_publish.el 了。
