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

技术博客一键多发实战:基于 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。

  1. 登录 微信公众号后台,进入「设置与开发」→「账号设置」→「注册信息」
  2. 复制 AppID
  3. 前往 微信开发者平台,进入「首页」→「公众号」→「开发密钥」→「AppSecret」→「开启」→ 「管理员扫码重置」
  4. 复制 AppSecret (建议永久保存,否则下一次仍需重置)
  5. 在「设置与开发」→「安全中心」→「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 流水线能顺利获取这些身份凭据(详见后面的第五步),因此我们接下来的操作的目的是为了第五步做准备的。这一点需要你首先明确。

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 转换成这种字符串格式。下面提供两种简单的方法来完成这一转换。

multi-publisher 没有内置的导出命令 (mpub credential export 并不支持 -p 参数),但你可以通过读取配置文件来生成符合 HTTP Cookie 规范的字符串。

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 方案。


如果 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 写入权限

  1. 进入你的 GitHub 仓库页面。
  2. 点击 Settings(设置)。
  3. 在左侧导航栏找到 Actions -> General。
  4. 滚动到页面最下方,找到 Workflow permissions(工作流权限)。
  5. 将默认的 Read repository contents and packages permissions 修改为 Read and write permissions(读写权限)。
  6. 点击 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 验证流水线

  1. 在 GitHub 仓库页面,点击 Actions 标签页。

  2. 你会看到名为 "自动发布技术博客" 的 Workflow。

  3. 点击 "Run workflow" 按钮,在下拉菜单中可以选择输入 files 参数(留空则发布全部),然后点击 "Run workflow" 手动触发一次。

  4. 观察构建日志,确保所有步骤都成功。

6.5 日常使用流程

现在,你的日常创作流程就简化为了:

  1. 在本地用 Typora、VS Code 或 Obsidian 写文章,图片自动上传至图床。

  2. 填写 Front Matter,保存到 posts/ 目录。

  3. 执行:

    sql 复制代码
    git add posts/新文章.md
    git commit -m "新文章: xxx"
    git push
  4. 几秒钟后,GitHub Actions 会自动检测到 main 分支的推送,并启动流水线。你可以在 Actions 页面查看实时构建日志。

  5. 构建成功后:

    • 掘金、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:遇到发布失败如何调试?

  1. 优先查看 GitHub Actions 的详细日志输出(点击 Actions 中的具体任务即可展开)。
  2. 在本地重现命令,例如 wenyan publish -f your-article.md,通过错误信息定位问题。
  3. 访问对应工具的 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 然后全平台上线带来的畅快体验吧。

相关推荐
恒拓高科WorkPlus2 小时前
企业怎样选择IM即时通讯平台?私有化部署还是SaaS更适合?
网络·github
Maynor在掘金3 小时前
只用 1 张随手拍,在 Claude Code 里做出 15 秒抖音带货视频(配音 + 字幕花字 + BGM,本地出片)
github
littlex6 小时前
复盘分享:「高性价比人生指南」衍生网站,一周 8k+ 用户
github
u1301306 小时前
GitHub 热榜项目:周榜(2026-10-04)
人工智能·github
挖掘狂人7 小时前
Git 从 0 到 1:用一个小项目走完 add / commit / reset / merge / rebase / push
git·后端·github
miofly7 小时前
macOS 本地 AI 搜索工具 SCM 发布 0.2.4 版本,支持多模型照片视频检索
开源·github
codigger8 小时前
Git 三区域模型:把 add、commit、reset、merge 一次讲透
git·github·编程·编程语言
高频因子挖掘机8 小时前
行情监控脚本重启后,怎样快速恢复而不重复拉取数据?
后端·github·api
idanzk8 小时前
Git 推送 GitHub 报 SSL_READ /src refspec main 不匹配 完整踩坑记录
git·github·ssl