技术博客一键多发实战:基于 GitHub Actions 与双引擎的自动化发布流水线

作为一名技术作者,写一篇技术博客,然后手动复制到掘金、CSDN、知乎、微信公众号......这种重复劳动正在吞噬你的创作时间。本文将手把手带你搭建一套基于 GitHub Actions + 文颜 + multi-publisher 的双引擎自动化发布系统,实现
git push即全平台上线。本文所有操作基于 GitHub Actions,配置简单、生态完善、日志清晰,是技术作者的绝佳选择。

一、为什么需要双引擎?
技术作者的发布矩阵通常包含两类平台:
- 微信公众号:受众精准,但格式封闭,图片必须上传到自身素材库。
- 技术社区:掘金、CSDN、知乎 等,开放性较好,但 Cookie 维护和反爬策略各不相同。
单靠一个工具很难同时兼顾这两类平台的稳定性与覆盖面。文颜 在微信公众号的 API 集成上做得非常深入,合规且稳定;而 multi-publisher 则能覆盖 20+ 技术社区,实现广度分发。将二者组合为"双引擎",就能在一个 Git 仓库内,让每篇文章自动适配不同平台的格式、图片和发布方式,真正实现 Write once, publish everywhere。
二、项目结构与文章规范
在动手之前,先约定好 Git 仓库的目录结构。清晰的模块划分是自动化流程能被准确触发和追溯的基础。
css
my-blog-workspace/
├── .github/ # [核心] GitHub Actions 配置目录
│ └── workflows/
│ └── publish.yml # [核心] 流水线配置文件
├── posts/ # [核心] 存放所有待发布的 Markdown 文件
│ ├── my-new-article.md
│ └── another-article.md
├── package.json # [重要] 声明 wenyan 和 multi-publisher 依赖,供 CI 安装
├── .gitignore
└── README.md
为了让工具自动识别标题、标签、封面以及发布状态,每篇 Markdown 文章都必须以 YAML Front Matter 开头。它是整条流水线的"身份证"。
yaml
---
title: "构建个人博客的 CI/CD 自动发布流水线"
date: 2026-08-04
tags: [DevOps, CI/CD, 自动化]
cover: "https://your-image-bed.com/cover-image.jpg"
status: # [可选] 记录各平台发布状态
zhihu: ""
juejin: ""
csdn: ""
weixin: ""
---
- title / date / tags:会被自动提取并填写到对应平台的编辑框。
- cover:统一使用外链图片,建议搭建图床(如阿里云 OSS、腾讯云 COS)。文章中的图片同样使用外链,这样在支持外链的平台可以直接显示,公众号等封闭平台则由工具自动下载并转存。
- status:每次发布成功后,将平台返回的文章 ID 或链接写回此处,避免重复发布。
三、本地环境与核心工具安装
3.1 准备 Node.js 环境
本地计算机需安装 Node.js v18.20+ 。在终端中验证:
css
node --version
npm --version
3.2 全局安装双引擎(本地测试用)
3.2.1 全局安装双引擎指令
bash
# 负责微信公众号稳定发布
npm install -g @wenyan-md/cli
wenyan --version
# 负责其他技术社区广度分发
npm install -g multi-publisher
mpub --version
3.2.2 关于 package.json 的重要说明:
本地测试时,我们使用 npm install -g 全局安装两个工具,所以可以直接在终端运行命令。
但在 GitHub Actions 流水线中,脚本里执行的是 npm ci,这是本地安装 且严格锁定版本 ------它读取 package-lock.json,确保每次 CI 安装的依赖完全一致。这也是为什么必须把 package-lock.json 一起提交到仓库。
因此,请务必在项目根目录创建 package.json ,并将两个工具写入 devDependencies:
perl
{
"name": "my-blog-workspace",
"version": "1.0.0",
"description": "自动化发布博客流水线",
"devDependencies": {
"@wenyan-md/cli": "latest",
"multi-publisher": "latest"
},
"scripts": {
"publish": "wenyan publish && mpub publish"
}
}
这样,流水线中的 npm install 就会正确安装依赖,虽然 package.json 中的 scripts 脚本可以直接调用,但在 GitHub Actions 的 run 步骤中直接执行时,建议加上 npx 前缀(如 npx wenyan publish),以确保 CI 环境能准确找到 node_modules/.bin 下的可执行文件。
3.2.3 关于scripts.publish 的说明:
scripts.publish 是本地调试用的快捷命令。当你在本地写完几篇文章,想测试一下工具能不能正常工作,但不想走 git push 触发 CI 时,可以直接在项目根目录敲 npm run publish,它会把你 posts/ 目录下所有文章都发一遍(前提是你的工具支持默认路径)。GitHub Actions 流水线中直接调用带参数的原始命令(-f 指定具体文件),不依赖此脚本。
四、获取并配置各平台身份凭据
这是整条流水线最关键的一步。不同平台使用不同的认证方式,需要逐个配置。
首选需要明确这里的身份凭据的含义:
a. 在微信公众号中的身份凭据是其 AppID 和 AppSecret,由文颜负责;
b. 其他技术社区的身份凭据形式是Cookie,由multi-publisher 负责;
4.1 配置微信公众号身份凭据(AppID+AppSecret形式,由文颜负责)
文颜使用官方 API 发布到草稿箱,因此需要 AppID 和 AppSecret。
- 登录 微信公众号后台,进入「设置与开发」→「账号设置」→「注册信息」
- 复制 AppID
- 前往 微信开发者平台,进入「首页」→「公众号」→「开发密钥」→「AppSecret」→「开启」→ 「管理员扫码重置」
- 复制 AppSecret (建议永久保存,否则下一次仍需重置)
- 在「设置与开发」→「安全中心」→「IP 白名单」页面,将 GitHub Actions 运行环境的公网出口 IP 加入白名单,否则 API 调用会被拒绝
如何获取 GitHub Actions 的 IP? GitHub Actions 的 IP 段是动态的,官方会定期公布 IP 列表(见 meta API)。建议先不加白名单运行一次流水线,在微信后台的调用日志中会看到被拒绝的 IP,再将对应 IP 段加入白名单。
4.2 配置其他技术社区身份凭据(Cookie形式,multi-publisher 负责)
4.2.1 配置Cookie的指令
multi-publisher 通过保存登录后的 Cookie 来模拟用户操作。在本地终端(cmd/PowerShell)中依次执行:
css
mpub login -p zhihu
mpub login -p juejin
mpub login -p csdn
# ... 按需登录其他平台
每执行一条命令,工具会打开浏览器窗口让你完成登录授权,如下图,登录成功后,Cookie 会被安全地加密保存在本地 ~/.mpub/ 目录下。

接下来我们需要拿到本地生成的 Cookie 字符串,然后设置到 GitHub Secret 中,以便 GitHub Actions 流水线能顺利获取这些身份凭据(详见后面的第五步),因此我们接下来的操作的目的是为了第五步做准备的。这一点需要你首先明确。
4.2.2 找到本地 Cookie 的存储位置
multi-publisher 登录成功后,会将 Cookie 加密保存在配置文件中(实际为明文 JSON,但建议不要手动编辑):
- Linux/macOS :
~/.config/multi-publisher/config.json - Windows :
C:\Users[你的用户名].config\multi-publisher\config.json
你可以通过以下命令查看确切的配置文件路径:
css
mpub credential --location

打开配置文件你会发现,cookies 字段是一个 JSON 对象 (例如 {"csrf_session_id":"abc", "s_v_web_id":"xyz"})。然而,在 CI/CD 环境变量中,multi-publisher 需要接收的是 标准的 HTTP Cookie 字符串 ,即 key1=value1; key2=value2 的形式。直接将整个 JSON 对象粘贴到 GitHub Secrets 中是无效的,因此我们必须先将本地的 Cookie 转换成这种字符串格式。下面提供两种简单的方法来完成这一转换。
4.2.3 将 Cookie 的「 JSON 对象」导出成「标准 HTTP Cookie 字符串」
multi-publisher 没有内置的导出命令 (mpub credential export 并不支持 -p 参数),但你可以通过读取配置文件来生成符合 HTTP Cookie 规范的字符串。
方法1:使用 jq 命令格式化 Cookie JSON 对象
jq 是一款轻量级的命令行 JSON 处理器,安装后可以用一条命令直接提取并拼接 Cookie。 如果你不想安装 jq,也可以使用文末的 Node.js 脚本(跨平台通用,无需额外安装)。
安装 jq 提示:
- macOS :
brew install jq- Linux (Debian/Ubuntu) :
sudo apt install jq- Windows :可用
winget install jqlang.jq或下载可执行文件加入 PATH(具体可自行搜索)。
🐧 macOS / Linux(bash/zsh)
scss
# 知乎
jq -r '.zhihu.cookies | to_entries | map(.key + "=" + .value) | join("; ")' ~/.config/multi-publisher/config.json
# 掘金
jq -r '.juejin.cookies | to_entries | map(.key + "=" + .value) | join("; ")' ~/.config/multi-publisher/config.json
# CSDN
jq -r '.csdn.cookies | to_entries | map(.key + "=" + .value) | join("; ")' ~/.config/multi-publisher/config.json
🪟 Windows CMD (Win 用户推荐 使用 Command Prompt,而不是 PowerShell)
arduino
REM 知乎
jq -r ".zhihu.cookies | to_entries | map(.key + "=" + .value) | join("; ")" %USERPROFILE%.config\multi-publisher\config.json
REM 掘金
jq -r ".juejin.cookies | to_entries | map(.key + "=" + .value) | join("; ")" %USERPROFILE%.config\multi-publisher\config.json
REM CSDN
jq -r ".csdn.cookies | to_entries | map(.key + "=" + .value) | join("; ")" %USERPROFILE%.config\multi-publisher\config.json
执行后,终端会输出一行完整的 Cookie 字符串(例如:csrf_session_id=xxx; s_v_web_id=yyy; ...),直接复制即可。

⚠️ Win 用户请注意 : PowerShell 环境下执行
jq命令存在引号解析和转义问题,强烈建议使用 CMD(Command Prompt) 执行以上命令 ,或直接使用下面的 Node.js 脚本(跨平台,无需关心引号)。 如果你在 PowerShell 中执行jq遇到syntax error等报错,属于正常现象,请切换到 CMD 或使用 Node.js 方案。
方法2:使用 Node.js 脚本格式化 Cookie JSON 对象
如果 jq 命令执行报错或你不想额外安装工具 ,可以用 Node.js 脚本(因为 multi-publisher 本身依赖 Node,你的电脑一定有 Node 环境)。 创建一个文件 export-cookie.js,内容如下:
ini
const fs = require('fs');
const path = require('path');
const os = require('os');
const configPath = path.join(os.homedir(), '.config', 'multi-publisher', 'config.json');
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
const platform = process.argv[2] || 'juejin'; // 从命令行参数获取平台名
const cookies = config[platform]?.cookies;
if (!cookies) {
console.error(`平台 ${platform} 未找到,请检查配置文件`);
process.exit(1);
}
const cookieString = Object.entries(cookies).map(([k, v]) => k + '=' + v).join('; ');
console.log(cookieString);
然后执行(任何系统终端均可):
arduino
node export-cookie.js juejin
输出的 Cookie 字符串同样可直接复制使用,无需关心操作系统差异。
注意: 无论使用哪种方式,将得到的字符串填入 GitHub Secrets 时,类型都选择为 "Secret" 。
五、在 GitHub 仓库中配置 Secrets
流水线中需要使用的敏感信息(如微信 AppID、各平台 Cookie 等)不应硬编码在配置文件中,而应通过 GitHub 的 Secrets 功能来管理。现在我们将第四步中得到的微信 AppID+AppSecret 、其他各平台导出的 Cookie字符串 填入到 GitHub Secrets 中,方便 GitHub Actions 流水线 使用这些身份凭据。
5.1 进入 Secrets 设置页面
打开你的 GitHub 代码仓库,进入 Settings → Secrets and variables → Actions。
5.2 添加 Secret
点击 "New repository secret" 按钮,按照提示填写:
- Name :输入纯英文标识符,例如
WECHAT_APP_ID。 - Value:粘贴对应的实际凭证内容。
5.3 保存并重复
点击 "Add secret" 。重复以上步骤,依次添加你在第四节中获取到的所有凭证:
| Secret 名称 | 说明 |
|---|---|
WECHAT_APP_ID |
微信公众号 AppID |
WECHAT_APP_SECRET |
微信公众号 AppSecret |
ZHIHU_COOKIE |
知乎平台 Cookie |
JUEJIN_COOKIE |
掘金平台 Cookie |
CSDN_COOKIE |
CSDN 平台 Cookie |

💡 如何为
multi-publisher平台准备正确的 Cookie 值? 在 4.2 节中我们已经介绍了从本地配置文件导出标准 Cookie 字符串的方法。请使用 jq 或 Node.js 脚本 生成完整的key=value; ...字符串,然后将其粘贴到对应 Secret 的 "Value" 框中。切勿直接粘贴 JSON 对象或仅粘贴部分键值对,否则multi-publisher将无法正常登录。各平台对应的 Secret 名称如下:
- 知乎 →
ZHIHU_COOKIE- 掘金 →
JUEJIN_COOKIE- CSDN →
CSDN_COOKIE如果后期 Cookie 过期,只需在本地重新登录(
mpub login -p <平台>),再次运行导出脚本,更新 GitHub Secret 即可,流水线配置无需改动。
5.4 在流水线中引用 Secret
变量添加完成后,在 GitHub Actions 的 YAML 中通过 ${{ secrets.变量名 }} 的形式直接引用即可。例如:
bash
env:
WECHAT_APP_ID: ${{ secrets.WECHAT_APP_ID }}
GitHub Actions 在执行流水线时会自动将这些 Secret 注入到运行环境中。
六、搭建 GitHub Actions 流水线:双引擎实战
在编写核心的自动化脚本之前,我们需要先在 GitHub 仓库中配置好流水线的运行权限。由于我们的流水线在发布成功后会执行 git push 将文章状态写回仓库,而 GitHub Actions 默认只有只读权限,因此需要提前开启写入权限,否则流水线会在最后的推送步骤报错退出。
6.1 配置 Workflow 写入权限
- 进入你的 GitHub 仓库页面。
- 点击 Settings(设置)。
- 在左侧导航栏找到 Actions -> General。
- 滚动到页面最下方,找到 Workflow permissions(工作流权限)。
- 将默认的
Read repository contents and packages permissions修改为Read and write permissions(读写权限)。 - 点击 Save 保存。

配置完成后,流水线就拥有了向仓库提交状态更新的能力。接下来我们开始编写具体的自动化脚本。
6.2 创建流水线配置文件
GitHub Actions 的配置文件需要放在仓库根目录下的 .github/workflows/ 目录中 ,文件名为 publish.yml。
在仓库根目录创建 .github/workflows/publish.yml:
yaml
name: 自动发布技术博客
on:
push:
branches: [main]
paths:
- 'posts/*.md'
workflow_dispatch:
inputs:
files:
description: '要发布的文件(空格分隔),留空则全部发布'
required: false
default: ''
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: 检出代码
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: 安装 Node.js
uses: actions/setup-node@v5
with:
node-version: '22'
- name: 安装依赖
run: npm ci
- name: 安装 Playwright 浏览器
run: npx playwright install chromium --with-deps
- name: 获取要发布的文件
id: get-files
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
if [ -n "${{ inputs.files }}" ]; then
FILES="${{ inputs.files }}"
else
FILES=$(find posts -name "*.md" | tr '\n' ' ')
fi
else
CHANGED_FILES=$(git diff --name-only HEAD^ HEAD | grep 'posts/.*.md$' | tr '\n' ' ')
FILES="$CHANGED_FILES"
fi
echo "files=$FILES" >> $GITHUB_OUTPUT
- name: 写入 multi-publisher Cookie 配置
env:
JUEJIN_COOKIE: ${{ secrets.JUEJIN_COOKIE }}
CSDN_COOKIE: ${{ secrets.CSDN_COOKIE }}
ZHIHU_COOKIE: ${{ secrets.ZHIHU_COOKIE }}
run: |
node <<'EOF'
const fs = require('fs');
const path = require('path');
const os = require('os');
const configDir = path.join(os.homedir(), '.config', 'multi-publisher');
const configPath = path.join(configDir, 'config.json');
fs.mkdirSync(configDir, { recursive: true });
// 解析标准 HTTP Cookie 字符串为 key-value 对象
function parseCookieString(str) {
const cookies = {};
str.split(';').forEach(part => {
const t = part.trim();
if (!t) return;
const i = t.indexOf('=');
if (i === -1) return;
let k = t.slice(0, i).trim().replace(/^"|"$/g, '');
let v = t.slice(i + 1).trim().replace(/^"|"$/g, '');
cookies[k] = v;
});
return cookies;
}
const platforms = ['csdn', 'juejin', 'zhihu'];
const config = { version: 1 };
for (const platform of platforms) {
const envName = platform.toUpperCase() + '_COOKIE';
const val = process.env[envName];
if (!val) {
console.log(`⏭ 跳过 ${platform}(环境变量 ${envName} 为空)`);
continue;
}
const cookies = parseCookieString(val);
const count = Object.keys(cookies).length;
config[platform] = { cookies };
console.log(`✅ ${platform} Cookie 已写入(${count} 条)`);
}
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
console.log(`📁 配置文件已保存到:${configPath}`);
EOF
- name: 发布文章
if: steps.get-files.outputs.files != ''
env:
WECHAT_APP_ID: ${{ secrets.WECHAT_APP_ID }}
WECHAT_APP_SECRET: ${{ secrets.WECHAT_APP_SECRET }}
run: |
FAILED_PLATFORMS=""
for file in ${{ steps.get-files.outputs.files }}; do
echo "📝 开始发布:$file"
# 剥离 Front Matter + 删 TOC + 压缩空行(CSDN 用)
sed '1{/^---$/!q}; 1,/^---$/d' "$file" | sed '/^[TOC]$/d' | cat -s > /tmp/publish-body.md
echo "🚀 文颜引擎 → 微信公众号"
if ! npx wenyan publish -f "$file"; then
FAILED_PLATFORMS="$FAILED_PLATFORMS WeChat"
echo "❌ 文颜引擎发布失败" >&2
fi
echo "🚀 multi-publisher → 掘金、知乎、CSDN"
for platform in juejin zhihu csdn; do
echo "🚀 multi-publisher → $platform"
PUBLISH_FILE="$file" # 默认用原文件(掘金 / 知乎)
THEME_ARG=""
case "$platform" in
zhihu)
# 知乎:mpub publish 不支持自定义 CSS,用内置 modern 主题
THEME_ARG="-t modern"
;;
csdn)
# CSDN:编辑器不识别 Front Matter,用剥离后的纯正文
PUBLISH_FILE="/tmp/publish-body.md"
;;
juejin)
# 掘金:原生样式已足够好,不加主题
;;
esac
if ! npx mpub publish -f "$PUBLISH_FILE" -p "$platform" $THEME_ARG; then
FAILED_PLATFORMS="$FAILED_PLATFORMS $platform"
echo "❌ $platform 发布失败" >&2
fi
done
echo "✅ 完成发布:$file"
done
if [ -n "$FAILED_PLATFORMS" ]; then
echo "⚠️ 警告:以下平台发布可能失败:$FAILED_PLATFORMS"
fi
- name: 提交状态更新
if: steps.get-files.outputs.files != ''
run: |
git config --global user.name "GitHub Actions Bot"
git config --global user.email "actions@github.com"
git add posts/
git commit -m "ci: 更新文章发布状态 [skip ci]" || echo "没有状态变更"
git push
配置解读:
① 触发条件(on)
push.branches: [main]:只有推送到main分支才触发,其他分支的改动不会误发。push.paths: 'posts/*.md':只有posts/目录下的 Markdown 变更才触发。这意味着你改 README、改 workflow 文件本身都不会触发发布,避免无效运行。workflow_dispatch:支持在 GitHub 网页端手动触发,可输入files参数指定要发布的文件(空格分隔),留空则全量发布。适合"重发某一篇"或"补发历史文章"的场景。
② 运行环境
runs-on: ubuntu-latest:使用 Ubuntu 最新版运行器。actions/checkout@v5+fetch-depth: 0:检出完整 Git 历史。fetch-depth: 0是必须的,因为后续步骤要用git diff HEAD^ HEAD找出本次推送改动了哪些文件;默认的浅克隆会导致这个命令失败。actions/setup-node@v5+node-version: '22':安装 Node 22。multi-publisher 要求 Node ≥ 18,同时选用 LTS 版本以保证长期稳定性。
③ 依赖安装
npm ci:严格按package-lock.json安装依赖,比npm install更快、更可复现。只要 lock 文件被提交且与package.json一致,就能保证每次 CI 安装的依赖完全相同。npx playwright install chromium --with-deps:安装 Playwright 的 Chromium 浏览器与系统依赖。multi-publisher 通过浏览器自动化完成掘金、CSDN、知乎等平台的发布,这一步是运行时硬依赖,不能省。
④ 增量发布逻辑(get-files)
- push 事件 :通过
git diff --name-only HEAD^ HEAD只提取本次提交中改动或新增的posts/*.md,实现"只发新文章/改过的文章",避免全量重发。 - 手动触发 :如果填写了
files参数就用它,否则用find posts -name "*.md"全量发布。 - 结果通过
echo "files=..." >> $GITHUB_OUTPUT写入,供后续步骤用${{ steps.get-files.outputs.files }}读取。
⑤ Cookie 配置(写入 multi-publisher 配置文件)
- 从 GitHub Secrets 读取
JUEJIN_COOKIE、CSDN_COOKIE、ZHIHU_COOKIE三个环境变量。 - 用一段内联的 Node.js 脚本把标准 HTTP Cookie 字符串(
key1=value1; key2=value2)解析成 key-value 对象,写入~/.config/multi-publisher/config.json。 - 这一步是 multi-publisher 在 CI 环境下的唯一凭据来源 。Cookie 过期后需要在本地
mpub login重新登录并导出,再更新对应的 GitHub Secret。
⑥ 双引擎发布(核心逻辑)
流水线同时调用两个工具,各自负责不同的平台:
| 引擎 | 负责平台 | 认证方式 | 特点 |
|---|---|---|---|
| 文颜(wenyan) | 微信公众号 | AppID + AppSecret(官方 API) | 合规稳定,自动下载图片上传到微信素材库 |
| multi-publisher(mpub) | 掘金、CSDN、知乎 | Cookie(浏览器自动化) | 覆盖 20+ 社区,支持增量发布 |
⑦ 各平台的差异化处理
同一个 Markdown 文件,在发布到不同平台时走了不同的预处理路径:
- 掘金 :直接使用原始文件
$file(含 Front Matter)。掘金原生识别 Front Matter,样式已足够好,不需要额外处理。 - CSDN :用
sed剥离 Front Matter +sed删除[TOC]+cat -s压缩连续空行,得到纯正文后发布。CSDN 编辑器不识别 Front Matter,会把元数据当正文渲染。 - 知乎 :直接使用原始文件 +
-t modern。mpub 会自行解析 Front Matter 提取标题,并用内置 modern 主题渲染。注意:mpub 1.1.4 的publish子命令不支持自定义 CSS ,因此themes/zhihu.css当前未被引用(保留作未来备胎)。 - 微信公众号:直接使用原始文件,由文颜内部处理 Front Matter、图片转存等。
⑧ 失败容错
- 每个平台独立 try-catch:某一平台失败不会中断其他平台的发布。
- 失败时把平台名追加到
FAILED_PLATFORMS变量,最后统一打印警告。 - 注意 :这种设计下,即使所有平台都失败,workflow 也会显示"绿色成功"。如果希望在失败时让 workflow 报红,可以最后加
[ -z "$FAILED_PLATFORMS" ] || exit 1。
⑨ 状态回写
git add posts/ && git commit -m "ci: 更新文章发布状态 [skip ci]"用来把发布状态写回仓库(例如把文章 ID 填到 Front Matter 的status字段)。- 当前实现里这部分是预留位 ------workflow 还没有真的修改
posts/下的文件,所以每次都会走|| echo "没有状态变更"分支。 - 未来如果要做"防重复发布",可以让 workflow 解析每个平台的返回链接,写回 Front Matter,再提交。
[skip ci]标记防止这次回写 commit 再次触发 workflow。
6.3 提交并推送配置
bash
# 创建 workflows 目录
mkdir -p .github/workflows
# 将上面的 YAML 内容保存到 .github/workflows/publish.yml
# 然后提交并推送
git add .github/workflows/publish.yml package.json posts/
git commit -m "feat: 添加 GitHub Actions 自动发布流水线"
git push origin main
6.4 验证流水线
-
在 GitHub 仓库页面,点击 Actions 标签页。
-
你会看到名为 "自动发布技术博客" 的 Workflow。

-
点击 "Run workflow" 按钮,在下拉菜单中可以选择输入
files参数(留空则发布全部),然后点击 "Run workflow" 手动触发一次。 -
观察构建日志,确保所有步骤都成功。


6.5 日常使用流程
现在,你的日常创作流程就简化为了:
-
在本地用 Typora、VS Code 或 Obsidian 写文章,图片自动上传至图床。
-
填写 Front Matter,保存到
posts/目录。 -
执行:
sqlgit add posts/新文章.md git commit -m "新文章: xxx" git push -
几秒钟后,GitHub Actions 会自动检测到
main分支的推送,并启动流水线。你可以在 Actions 页面查看实时构建日志。 -
构建成功后:
- 掘金、CSDN:图文完整、标签正确
- 知乎:标题、正文、代码块正常;图片与二级标题格式暂时受 mpub 上游 bug 影响,需手动补一次
- 微信公众号:需要在微信后台配置 IP 白名单后才能成功
手动触发指定文件 :若你只想发布某几篇文章,可以在 Actions 页面点击 "Run workflow" ,在弹出的对话框中填写 files 参数,例如 posts/my-new-article.md posts/another.md,流水线将只发布这些文件(留空则发布全部)。
七、进阶功能与长期维护
7.1 分平台管理文章状态,防止重复发布
发布成功后,建议将返回的文章 ID 写回 Front Matter 的 status 字段,并提交回仓库。例如:
lua
status:
juejin: "https://juejin.cn/post/1234567890"
zhihu: "https://zhuanlan.zhihu.com/p/1234567890"
weixin: "https://mp.weixin.qq.com/s/xxxxxx"
在下次流水线触发时,脚本可以增加判断逻辑:若对应平台已有 ID,则跳过或执行"更新"而非"重新发布",避免产生重复文章。
7.2 定时巡检与 Token 有效期维护
Cookie 过期是多平台分发的最大敌人。可以通过创建一个定时触发的流水线,配合 multi-publisher 的 cookie 相关命令来监控状态(具体子命令请通过 mpub cookie --help 确认)。如果检测到凭证失效,流水线通过企业微信、钉钉或飞书机器人发送通知,提醒你重新登录并更新 Secrets。
7.3 发布结果通知
在流水线 YAML 的 run 脚本最后,添加一个通知脚本,调用你习惯的 IM 工具的 Webhook 接口,将发布结果(成功或失败)及时推送给自己,让你能第一时间处理异常。
八、常见问题 Q&A
Q1:国内访问 GitHub Actions 速度慢怎么办?
GitHub Actions 的运行器位于海外,但执行 npm ci 时可以使用国内镜像源加速。在 package.json 同级目录创建 .npmrc 文件:
ini
registry=https://registry.npmmirror.com
或在流水线的 npm install 前添加:
arduino
npm config set registry https://registry.npmmirror.com
你仓库中的 package-lock.json 的 resolved 字段已经指向 registry.npmmirror.com,说明本地装依赖时已经用了淘宝源,CI 会沿用。
Q2:multi-publisher 支持 20+ 平台,如何只发布到指定的几个?
使用 -p 参数即可精确指定目标平台:
css
mpub publish -f my-article.md -p juejin -p csdn
Q3:如何只发布新增或修改的文章,而不是全量发布?
流水线配置中的「获取要发布的文件」步骤就是为此而设计。它通过 git diff 只识别出本次推送涉及到的 md 文件,实现 增量发布。
Q4:文章中使用了图床外链,微信公众号无法显示怎么办?
这正是引入文颜的意义之一。文颜在执行发布时,会自动将 Markdown 中的外部图片链接下载并上传到微信公众号素材库,然后用返回的 mmbiz.qpic.cn 链接替换原文,全程无需手动操作。
Q5:遇到发布失败如何调试?
- 优先查看 GitHub Actions 的详细日志输出(点击 Actions 中的具体任务即可展开)。
- 在本地重现命令,例如
wenyan publish -f your-article.md,通过错误信息定位问题。 - 访问对应工具的 GitHub 仓库,查看已有 Issues 或提交新问题。
Q6:GitHub Actions 的免费额度够用吗?
GitHub Actions 对公开仓库 完全免费,对私有仓库每月提供 2000 分钟免费额度。对于个人博客发布场景,通常绰绰有余。如果额度用尽,可以考虑将仓库设为公开,或购买额外额度。
Q7:微信公众平台需要配置 IP 白名单,GitHub Actions 的 IP 怎么获取?
GitHub Actions 的 IP 段是动态的,官方通过 api.github.com/meta 发布 IP 列表。建议先不加白名单运行一次流水线,在微信后台的调用日志中会看到被拒绝的 IP,再去「设置与开发」→「安全中心」→「IP 白名单」中将对应 IP 段加入。由于 IP 会变化,可能需要定期更新。
Q8:流水线报错 Process completed with exit code 128 怎么解决?
这个错误大概率发生在流水线最后一步 git push 时。说明你跳过了 6.1 节的配置,GitHub Actions 默认给工作流的 GITHUB_TOKEN 只有读权限,无法向仓库推送新的 commit。
解决办法 : 请回到文章 6.1 节 ,前往仓库的 Settings -> Actions -> General,在页面最底部的 Workflow permissions 中,勾选 Read and write permissions,保存后重新运行(Re-run jobs)即可。
Q9:流水线报错 40164: invalid ip ... not in whitelist 怎么办?
说明你把文章 4.1 节的 IP 白名单没配好,或者 GitHub Actions 出口 IP 变了。建议将
https://api.github.com/meta里actions的完整 IP 段批量加入微信后台白名单,或者写一个自动更新脚本。
Q10:multi-publisher 报错"未配置 XX Cookie"怎么办?
这是提示你 Secrets 没配好。请回到文章 4.2 节,使用
mpub login和导出脚本重新生成 Cookie 字符串,并确保在 GitHub 仓库 Settings -> Secrets 中正确添加了对应平台的 Secret(如CSDN_COOKIE、JUEJIN_COOKIE等),且值粘贴正确。
Q11:知乎和 CSDN 草稿开头多出了一段 Front Matter 怎么办?
Front Matter 是给流水线提取元数据用的,但 CSDN 的编辑器不识别它,会把它当正文渲染。解决办法是在 workflow 中按平台分别处理:
- CSDN :剥离 Front Matter + 删除
[TOC]+ 压缩连续空行,生成纯正文后发布 - 知乎 :保留原始 Markdown,直接交给 mpub,由 mpub 自己解析 Front Matter,并通过
-t modern主题渲染 - 掘金:保留原文件(掘金原生识别 Front Matter)
说明:知乎和掘金都保留 Front Matter,因为 mpub 会从中提取标题;CSDN 走的是另一条路径,由 workflow 主动剥离。
九、结语
从"写完一篇技术博客,花半小时搬运到各个平台",到"git push 之后静待花开",这条自动化之路在今天已经非常成熟。双引擎(文颜 + multi-publisher)的组合,既保证了对微信公众号这一封闭生态的稳定输出,又兼顾了众多技术社区的广泛触达。
GitHub Actions 作为 CI/CD 平台,配置简单、生态完善、日志清晰,是技术博客自动化发布的最佳选择。一旦搭建完成,你就能把时间真正还给思考和创作,而不再被格式、图片和登录这些琐事打断。现在就动手,享受一次 git push 然后全平台上线带来的畅快体验吧。