用 simple-git 编写自动化发版通知机器人

封面信息图

在开源项目维护和企业级敏捷迭代中,“软件发版”是一个重要的里程碑。每次我们在 Git 仓库打上新的 Tag 并推送到远端(如 git tag v1.5.0 && git push --tags),除了触发 CI 构建与 npm/Docker 发布之外,还有一项关键任务:将本次版本的更新要点(Changelog)及时同步给开源社区群、用户社群或内部开发团队

如果依赖人工去梳理 Git 提交记录、手写通知文案并在各个群聊中逐个粘贴,不仅效率低下,还极易遗漏关键改动或在深夜发版时延误。

本文介绍如何利用轻量级 Node.js 库 simple-git,编写一个仅需百行代码的自动化发版机器人。它能自动计算两个 Tag 之间的增量提交、按语义化分类归纳,并一键推送到钉钉、企业微信、飞书或 Discord 群组。

1. 自动化流水线流程设计

机器人的核心工作流极其精炼:

[ Git 仓库触发发版 Tag: v1.5.0 ]
               |
               v (simple-git 获取最近两个语义化 Tag)
[ 计算差异区间: v1.4.0...v1.5.0 ]
               |
               v (解析区间内的 Commit 记录与作者信息)
[ 语义化分类器: feat / fix / perf / docs ]
               |
               v (组装精美的 Markdown 发布卡片)
[ Webhook 广播至飞书 / 企微 / 钉钉 / Discord 社区群 ]

2. 核心代码实现

整个脚本无需复杂的框架,只需安装 simple-git 作为核心依赖:

pnpm add simple-git
pnpm add -D typescript @types/node tsx
2.1 提取与分类提交记录
// scripts/release-notifier.ts
import { simpleGit, SimpleGit } from 'simple-git';

const git: SimpleGit = simpleGit();

interface CategorizedChanges {
  features: string[];
  fixes: string[];
  performance: string[];
  others: string[];
}

export async function generateReleaseNotes(): Promise<{ currentTag: string; previousTag: string; changes: CategorizedChanges }> {
  // 1. 获取所有 Tag 并按版本倒序排序
  const tagsResult = await git.tags({ '--sort': '-v:refname' });
  const allTags = tagsResult.all;

  if (allTags.length === 0) {
    throw new Error('未找到任何 Git Tag,请先打 Tag 后运行!');
  }

  const currentTag = allTags[0];
  const previousTag = allTags.length > 1 ? allTags[1] : '';

  console.log(`📌 检测到当前最新版本: ${currentTag}, 上一版本: ${previousTag || '初始提交'}`);

  // 2. 获取两个 Tag 之间的 commit 列表
  const logRange = previousTag ? `${previousTag}..${currentTag}` : currentTag;
  const logSummary = await git.log({ from: previousTag || undefined, to: currentTag });

  const changes: CategorizedChanges = {
    features: [],
    fixes: [],
    performance: [],
    others: []
  };

  // 3. 按照 Conventional Commits 规范进行正则归类
  for (const commit of logSummary.all) {
    const msg = commit.message.trim();
    const author = commit.author_name;
    const item = `${msg} (@${author})`;

    if (/^feat(\(.*\))?:/i.test(msg)) {
      changes.features.push(item);
    } else if (/^fix(\(.*\))?:/i.test(msg)) {
      changes.fixes.push(item);
    } else if (/^perf(\(.*\))?:/i.test(msg)) {
      changes.performance.push(item);
    } else if (!/^chore\(release\)/i.test(msg)) {
      // 过滤掉自动化发版提交
      changes.others.push(item);
    }
  }

  return { currentTag, previousTag, changes };
}
2.2 组装 Markdown 并通过 Webhook 发送
// scripts/release-notifier.ts (续)
function buildMarkdownPayload(projectName: string, currentTag: string, changes: CategorizedChanges): string {
  let content = `🎉 **${projectName} ${currentTag} 正式发布!**\n\n`;

  if (changes.features.length > 0) {
    content += `### 🚀 新特性 (Features)\n`;
    changes.features.forEach(f => { content += `- ${f}\n`; });
    content += `\n`;
  }

  if (changes.fixes.length > 0) {
    content += `### 🐛 Bug 修复 (Bug Fixes)\n`;
    changes.fixes.forEach(f => { content += `- ${f}\n`; });
    content += `\n`;
  }

  if (changes.performance.length > 0) {
    content += `### ⚡ 性能优化 (Performance)\n`;
    changes.performance.forEach(p => { content += `- ${p}\n`; });
    content += `\n`;
  }

  content += `🔗 **源码与下载**: [GitHub Release 查看详情](https://github.com/example-org/${projectName}/releases/tag/${currentTag})\n`;
  return content;
}

// 统一的 Webhook 发送器 (以标准飞书/企微机器人格式为例)
export async function sendWebhookNotification(webhookUrl: string, markdownText: string) {
  const payload = {
    msg_type: 'interactive',
    card: {
      header: {
        title: { tag: 'plain_text', content: '📦 生产环境发版通知' },
        template: 'turquoise'
      },
      elements: [
        {
          tag: 'markdown',
          content: markdownText
        }
      ]
    }
  };

  const res = await fetch(webhookUrl, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload)
  });

  if (!res.ok) {
    throw new Error(`Webhook 推送失败: ${res.status} ${res.statusText}`);
  }

  console.log('✅ 发版通知已成功推送到社区群聊!');
}

// 执行入口
async function main() {
  const webhookUrl = process.env.RELEASE_WEBHOOK_URL;
  if (!webhookUrl) {
    console.warn('⚠️ 未配置 RELEASE_WEBHOOK_URL 环境变量,跳过群消息推送。');
    return;
  }

  try {
    const { currentTag, changes } = await generateReleaseNotes();
    const markdown = buildMarkdownPayload('my-awesome-tool', currentTag, changes);
    await sendWebhookNotification(webhookUrl, markdown);
  } catch (error) {
    console.error('❌ 发版通知执行失败:', error);
    process.exit(1);
  }
}

main();

3. 集成到 GitHub Actions CI 流水线

通过在 .github/workflows/release.yml 中添加一步,即可实现推送 Tag 时全自动广播:

name: Publish & Notify

on:
  push:
    tags:
      - 'v*'

jobs:
  notify:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0 # 必须拉取完整历史以获取所有 Tag

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Install dependencies
        run: pnpm install

      - name: Run Release Notifier
        env:
          RELEASE_WEBHOOK_URL: ${{ secrets.COMMUNITY_WEBHOOK_URL }}
        run: npx tsx scripts/release-notifier.ts

4. 落地收益与极简哲学

  1. 零外部服务账单:无需采购专门的 Release SaaS 工具,纯靠几行脚本与免费的 Webhook 即可实现秒级响应。
  2. 倒逼提交规范化:团队成员看到自己的 feat:fix: 提交信息能直接作为特性高光展示在官方社群公告中,会自发、自觉地规范 Git 提交文案。
  3. 闭环体验:从本地打 Tag 推送,到 npm 包发版、社区公告展示,全程无需人工干预,极大地释放了维护者的心智带宽。

工程效率的提升往往不在于引入多么庞大的平台,而在于用最朴素的工具把重复发生的例行小事彻底自动化。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐