|
| 1 | +#+TITLE: 用GitHub Actions自动构建EGO博客 |
| 2 | +#+AUTHOR: lujun9972 |
| 3 | +#+CATEGORY: Emacs之怒 |
| 4 | +#+DATE: [2026-04-11 周五] |
| 5 | +#+OPTIONS: ^:{} |
| 6 | + |
| 7 | +* 背景 |
| 8 | + |
| 9 | +我的博客 [[https://lujun9972.github.io/][暗无天日]] 使用 [[https://github.com/lujun9972/EGO][EGO]](Emacs + Git + Org-mode)作为静态站点生成器。源文件是 ~.org~ 格式,存放在 ~source~ 分支;生成的 HTML 存放在 ~master~ 分支,通过 GitHub Pages 托管。 |
| 10 | + |
| 11 | +一直以来,我都是在本机执行 ~auto_publish.el~ 来发布博客。但既然用了 GitHub,为什么不把构建过程也自动化呢?于是我开始折腾用 GitHub Actions 来自动构建博客。 |
| 12 | + |
| 13 | +* 最终效果 |
| 14 | + |
| 15 | +每次 ~push~ 到 ~source~ 分支时,GitHub Actions 会自动: |
| 16 | +1. 用 EGO 将 ~.org~ 文件转换为 HTML |
| 17 | +2. 将生成的 HTML 推送到 ~master~ 分支 |
| 18 | +3. GitHub Pages 自动部署 |
| 19 | + |
| 20 | +顺带还会在 Habitica 上完成"写博客"任务打个卡(游戏化激励)。 |
| 21 | + |
| 22 | +* 实现过程 |
| 23 | + |
| 24 | +** 第一步:改造 auto_publish.el |
| 25 | + |
| 26 | +原来 ~auto_publish.el~ 里的路径都是硬编码的: |
| 27 | + |
| 28 | +#+begin_src emacs-lisp |
| 29 | +(setq load-path (cons "~/EGO/" load-path)) |
| 30 | +(setq load-path (cons "~/csdn-publish/" load-path)) |
| 31 | +(setq load-path (cons "~/toutiao/" load-path)) |
| 32 | +#+end_src |
| 33 | + |
| 34 | +这在本地没问题,但在 CI 环境里路径完全不同。所以我把所有硬编码路径改为从环境变量读取,保留默认值确保本地兼容: |
| 35 | + |
| 36 | +#+begin_src emacs-lisp |
| 37 | +(setq load-path (cons (or (getenv "EGO_DIR") "~/EGO/") load-path)) |
| 38 | +(setq load-path (cons (or (getenv "CSDN_DIR") "~/csdn-publish/") load-path)) |
| 39 | +(setq load-path (cons (or (getenv "TOUTIAO_DIR") "~/toutiao/") load-path)) |
| 40 | +#+end_src |
| 41 | + |
| 42 | +~ego-project-config-alist~ 中的 ~:repository-directory~ 和 ~:store-dir~ 也做了类似处理: |
| 43 | + |
| 44 | +#+begin_src emacs-lisp |
| 45 | +`(("blog" :repository-directory ,(or (getenv "REPO_DIR") "~/source") |
| 46 | + ... |
| 47 | + :store-dir ,(or (getenv "STORE_DIR") "~/web"))) |
| 48 | +#+end_src |
| 49 | + |
| 50 | +同时删除了已废弃的 CSDN 和头条发布代码(对应的包在本机也不存在了),精简了脚本。 |
| 51 | + |
| 52 | +** 第二步:编写 GitHub Actions Workflow |
| 53 | + |
| 54 | +最终的 workflow 文件 ~.github/workflows/github-action.yml~: |
| 55 | + |
| 56 | +#+begin_src yaml |
| 57 | +name: 提交 habitica 任务 + 构建博客 |
| 58 | +on: |
| 59 | + push: |
| 60 | + branches: [source] |
| 61 | + |
| 62 | +permissions: |
| 63 | + contents: write |
| 64 | + |
| 65 | +jobs: |
| 66 | + Finish-Habitica-Tasks: |
| 67 | + runs-on: ubuntu-latest |
| 68 | + steps: |
| 69 | + - name: Check out habash code |
| 70 | + uses: actions/checkout@v4 |
| 71 | + with: |
| 72 | + fetch-depth: 0 |
| 73 | + repository: nasfarley88/habash |
| 74 | + - name: 确保 habash 可执行 |
| 75 | + run: chmod +x ./habash |
| 76 | + - name: 提交 habitica 任务 |
| 77 | + run: | |
| 78 | + set -x |
| 79 | + ./habash up "写博客" |
| 80 | + env: |
| 81 | + HABITICA_TOKEN: ${{ secrets.HABITICA_TOKEN }} |
| 82 | + HABITICA_UUID: ${{ secrets.HABITICA_UUID }} |
| 83 | + |
| 84 | + Build-Blog: |
| 85 | + runs-on: ubuntu-latest |
| 86 | + env: |
| 87 | + EGO_DIR: ${{ github.workspace }}/EGO |
| 88 | + REPO_DIR: ${{ github.workspace }} |
| 89 | + STORE_DIR: ${{ github.workspace }}/web |
| 90 | + REPO: https://github.com/lujun9972/${{ github.event.repository.name }} |
| 91 | + steps: |
| 92 | + - name: Checkout 博客源文件 |
| 93 | + uses: actions/checkout@v4 |
| 94 | + with: |
| 95 | + fetch-depth: 0 |
| 96 | + ref: source |
| 97 | + |
| 98 | + - name: Checkout EGO |
| 99 | + uses: actions/checkout@v4 |
| 100 | + with: |
| 101 | + repository: lujun9972/EGO |
| 102 | + path: EGO |
| 103 | + |
| 104 | + - name: Checkout master 分支到 store-dir |
| 105 | + run: | |
| 106 | + git clone --branch master --single-branch \ |
| 107 | + https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git \ |
| 108 | + "$STORE_DIR" |
| 109 | + cd "$STORE_DIR" |
| 110 | + git config user.name "github-actions[bot]" |
| 111 | + git config user.email "github-actions[bot]@users.noreply.github.com" |
| 112 | + |
| 113 | + - name: 安装 Emacs 和系统依赖 |
| 114 | + run: sudo apt-get update && sudo apt-get install -y emacs-nox |
| 115 | + |
| 116 | + - name: 确保 repo 干净并配置 git 身份 |
| 117 | + run: | |
| 118 | + git config user.name "github-actions[bot]" |
| 119 | + git config user.email "github-actions[bot]@users.noreply.github.com" |
| 120 | + git clean -fd |
| 121 | + git checkout -- . |
| 122 | + |
| 123 | + - name: 执行 auto_publish.el 构建博客 |
| 124 | + run: emacs --batch -l auto_publish.el |
| 125 | + |
| 126 | + - name: 推送 HTML 到 master 分支 |
| 127 | + run: | |
| 128 | + cd "$STORE_DIR" |
| 129 | + git config user.name "github-actions[bot]" |
| 130 | + git config user.email "github-actions[bot]@users.noreply.github.com" |
| 131 | + git add -A |
| 132 | + git diff --staged --quiet || git commit -m "auto publish: $(date -u +%Y-%m-%dT%H:%M:%SZ)" |
| 133 | + git push origin master |
| 134 | +#+end_src |
| 135 | + |
| 136 | +两个 Job 并行运行,互不影响。 |
| 137 | + |
| 138 | +** 第三步:踩过的坑 |
| 139 | + |
| 140 | +整个过程踩了不少坑,记录一下: |
| 141 | + |
| 142 | +*** 坑1:EGO 缺少依赖 |
| 143 | + |
| 144 | +~auto_publish.el~ 在本地运行时,~ht~ 和 ~dash~ 两个包早就安装过了。但 CI 是全新环境,~package-install~ 列表里没有它们,导致 EGO 加载失败: |
| 145 | + |
| 146 | +#+begin_quote |
| 147 | +Cannot open load file: No such file or directory, ht |
| 148 | +#+end_quote |
| 149 | + |
| 150 | +解决:在 ~package-install~ 列表中加上 ~ht~ 和 ~dash~。 |
| 151 | + |
| 152 | +*** 坑2:本地包在 CI 中不存在 |
| 153 | + |
| 154 | +~toutiao~ 和 ~csdn-publish~ 是我本地的自定义包,不在任何包管理器上。CI 里根本没有: |
| 155 | + |
| 156 | +#+begin_quote |
| 157 | +Cannot open load file: No such file or directory, toutiao |
| 158 | +#+end_quote |
| 159 | + |
| 160 | +解决:用 ~(require 'toutiao nil t)~ 的 ~noerror~ 模式,找不到就静默跳过。后来发现本机上这两个包也早已不用了,干脆直接删除了相关代码。 |
| 161 | + |
| 162 | +*** 坑3:括号不匹配 |
| 163 | + |
| 164 | +删代码时一不小心把 ~let*~ 的关闭括号也删了,导致 Emacs 解析到文件末尾时报错: |
| 165 | + |
| 166 | +#+begin_quote |
| 167 | +End of file during parsing: auto_publish.el |
| 168 | +#+end_quote |
| 169 | + |
| 170 | +解决:补回括号,并用 ~emacs --batch --eval '(check-parens)'~ 验证。 |
| 171 | + |
| 172 | +*** 坑4:EGO 内部 git commit 缺少身份 |
| 173 | + |
| 174 | +EGO 构建完后会用 ~vc-git-commit~ 提交 store-dir 的变更,但 store-dir 里没配 git user: |
| 175 | + |
| 176 | +#+begin_quote |
| 177 | +Failed (status 128): git --no-pager commit -m Update published html files... |
| 178 | +#+end_quote |
| 179 | + |
| 180 | +解决:clone master 分支后立即配置 ~git config user.name/email~。 |
| 181 | + |
| 182 | +*** 坑5:EGO 的 stash/pop 在 CI 中失败 |
| 183 | + |
| 184 | +这是最难搞的一个。EGO 检测到 repo 有未提交变更时,会先 ~git stash~,构建完成后再 ~git stash pop~。但在 CI 中 stash pop 总是失败: |
| 185 | + |
| 186 | +#+begin_quote |
| 187 | +Failed (status 1): git --no-pager stash pop -q 0 . |
| 188 | +#+end_quote |
| 189 | + |
| 190 | +试了 ~git clean -fd~、~git checkout -- .~ 确保构建前 repo 干净,都没用——EGO 内部还是会触发 stash。 |
| 191 | + |
| 192 | +最终解决方案:用 ~ego-do-publication~ 的 ~checkin-all~ 参数(第四个参数),让 EGO 用 ~git commit~ 代替 ~git stash~,从根本上避免 stash/pop: |
| 193 | + |
| 194 | +#+begin_src emacs-lisp |
| 195 | +;; 之前:(ego-do-publication "blog" nil nil nil) |
| 196 | +;; 之后:(ego-do-publication "blog" t nil "CI auto publish") |
| 197 | +#+end_src |
| 198 | + |
| 199 | +第二个参数 ~t~ 表示全量构建,第四个参数非 nil 时 EGO 会 commit 而不是 stash。 |
| 200 | + |
| 201 | +* 总结 |
| 202 | + |
| 203 | +整个折腾过程的核心就是让 ~auto_publish.el~ 在 CI 环境和本地环境都能正常运行。关键改动: |
| 204 | + |
| 205 | +1. 路径可配置化(环境变量 + 默认值) |
| 206 | +2. 删除废弃的第三方发布代码 |
| 207 | +3. 补全缺失的依赖包 |
| 208 | +4. 用 ~checkin-all~ 绕过 stash/pop 兼容性问题 |
| 209 | + |
| 210 | +虽然中间踩了不少坑,但最终效果还是很满意的——以后 push 博客源文件就能自动构建发布,再也不用手动跑 ~auto_publish.el~ 了。 |
0 commit comments