变更日志生成器Skill changelog-generator

变更日志生成器技能用于自动从git提交历史生成结构化变更日志,遵循常规提交规范和语义版本控制,支持多种格式和工具集成,提升软件版本管理和发布自动化效率。关键词:git提交、变更日志、常规提交、语义版本、自动化、DevOps、CI/CD。

DevOps 0 次安装 0 次浏览 更新于 3/11/2026

name: changelog-generator description: 从git提交自动生成变更日志,遵循常规提交、语义版本控制和最佳实践。

变更日志生成器技能

自动从git提交生成变更日志,遵循常规提交、语义版本控制和最佳实践。

说明

您是一位变更日志生成专家。当调用时:

  1. 分析提交历史

    • 解析git提交消息
    • 识别常规提交类型
    • 分组相关更改
    • 确定版本提升(主版本、次版本、补丁)
  2. 生成变更日志条目

    • 遵循Keep a Changelog格式
    • 按更改类型分类
    • 突出显示重大更改
    • 添加相关元数据(日期、版本、作者)
  3. 格式化输出

    • 使用markdown格式
    • 创建清晰的章节标题
    • 添加提交和PR的链接
    • 包含重大更改的迁移指南
  4. 版本管理

    • 建议语义版本号
    • 识别重大更改
    • 跟踪废弃功能
    • 处理预发布版本

常规提交类型

  • feat: 新功能(次版本提升)
  • fix: 错误修复(补丁版本提升)
  • docs: 文档更改
  • style: 代码风格更改(格式化等)
  • refactor: 代码重构
  • perf: 性能改进
  • test: 测试添加或更改
  • build: 构建系统更改
  • ci: CI/CD更改
  • chore: 维护任务
  • revert: 恢复先前更改

重大更改: 任何在正文中包含BREAKING CHANGE:或类型后带有!的提交(主版本提升)

使用示例

@changelog-generator
@changelog-generator --since v1.2.0
@changelog-generator --unreleased
@changelog-generator --version 2.0.0
@changelog-generator --format keep-a-changelog
@changelog-generator --include-authors

变更日志格式

Keep a Changelog格式

# 变更日志

本项目所有显著更改都将记录在此文件中。

格式基于[Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
并且本项目遵循[语义版本控制](https://semver.org/spec/v2.0.0.html)。

## [未发布]

### 新增
- 新功能X以改善用户体验
- 支持配置选项Y

### 更改
- 更新依赖Z至版本2.0
- 改进数据处理性能

### 废弃
- 函数`oldMethod()` - 使用`newMethod()`代替

### 移除
- 移除废弃的API端点`/api/v1/old`

### 修复
- 修复缓存实现中的内存泄漏
- 修正日期格式化程序中的时区处理

### 安全
- 修复用户输入处理中的XSS漏洞
- 更新加密库以解决CVE-2024-1234

## [1.5.0] - 2024-01-15

### 新增
- 用户认证与OAuth2
- 报告导出功能
- 深色模式主题支持

### 更改
- 重新设计仪表板UI
- 优化数据库查询

### 修复
- 修复分页逻辑中的错误
- 解决API的CORS问题

## [1.4.2] - 2024-01-10

### 修复
- 修复支付处理中的关键错误
- WebSocket连接中的内存泄漏

### 安全
- 修复认证绕过漏洞

## [1.4.1] - 2024-01-05

### 修复
- 部署脚本的热修复
- 修复错误消息中的拼写错误

## [1.4.0] - 2024-01-01

### 新增
- 实时通知
- 拖放文件上传
- 高级搜索过滤器

### 更改
- 从REST迁移到GraphQL
- 更新UI组件库

### 废弃
- 旧的REST API端点(将在2.0中移除)

[未发布]: https://github.com/user/repo/compare/v1.5.0...HEAD
[1.5.0]: https://github.com/user/repo/compare/v1.4.2...v1.5.0
[1.4.2]: https://github.com/user/repo/compare/v1.4.1...v1.4.2
[1.4.1]: https://github.com/user/repo/compare/v1.4.0...v1.4.1
[1.4.0]: https://github.com/user/repo/releases/tag/v1.4.0

自动化变更日志生成

使用Git提交

#!/bin/bash
# generate-changelog.sh - 从git提交生成变更日志

VERSION=${1:-"Unreleased"}
PREV_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")

echo "# 变更日志"
echo ""
echo "## [$VERSION] - $(date +%Y-%m-%d)"
echo ""

# 获取上次标签后的提交
if [ -z "$PREV_TAG" ]; then
  COMMITS=$(git log --pretty=format:"%s|||%h|||%an" --reverse)
else
  COMMITS=$(git log ${PREV_TAG}..HEAD --pretty=format:"%s|||%h|||%an" --reverse)
fi

# 不同类别的数组
declare -a features=()
declare -a fixes=()
declare -a breaking=()
declare -a docs=()
declare -a chores=()
declare -a other=()

# 解析提交
while IFS='|||' read -r message hash author; do
  case "$message" in
    feat:*|feat\(*\):*)
      features+=("- ${message#feat*: } ([${hash}](../../commit/${hash}))")
      ;;
    fix:*|fix\(*\):*)
      fixes+=("- ${message#fix*: } ([${hash}](../../commit/${hash}))")
      ;;
    *BREAKING*|*\!:*)
      breaking+=("- ${message} ([${hash}](../../commit/${hash}))")
      ;;
    docs:*)
      docs+=("- ${message#docs: } ([${hash}](../../commit/${hash}))")
      ;;
    chore:*|build:*|ci:*)
      chores+=("- ${message#*: } ([${hash}](../../commit/${hash}))")
      ;;
    *)
      other+=("- ${message} ([${hash}](../../commit/${hash}))")
      ;;
  esac
done <<< "$COMMITS"

# 输出章节
if [ ${#breaking[@]} -gt 0 ]; then
  echo "### ⚠️ 重大更改"
  echo ""
  printf '%s
' "${breaking[@]}"
  echo ""
fi

if [ ${#features[@]} -gt 0 ]; then
  echo "### 新增"
  echo ""
  printf '%s
' "${features[@]}"
  echo ""
fi

if [ ${#fixes[@]} -gt 0 ]; then
  echo "### 修复"
  echo ""
  printf '%s
' "${fixes[@]}"
  echo ""
fi

if [ ${#docs[@]} -gt 0 ]; then
  echo "### 文档"
  echo ""
  printf '%s
' "${docs[@]}"
  echo ""
fi

if [ ${#chores[@]} -gt 0 ]; then
  echo "### 内部"
  echo ""
  printf '%s
' "${chores[@]}"
  echo ""
fi

if [ ${#other[@]} -gt 0 ]; then
  echo "### 其他更改"
  echo ""
  printf '%s
' "${other[@]}"
  echo ""
fi

使用conventional-changelog

# 安装
npm install -g conventional-changelog-cli

# 生成变更日志
conventional-changelog -p angular -i CHANGELOG.md -s

# 首次发布
conventional-changelog -p angular -i CHANGELOG.md -s -r 0

# 指定版本
conventional-changelog -p angular -i CHANGELOG.md -s --release-count 0 \
  --tag-prefix v --preset angular

package.json配置:

{
  "scripts": {
    "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
    "version": "npm run changelog && git add CHANGELOG.md"
  },
  "devDependencies": {
    "conventional-changelog-cli": "^4.1.0"
  }
}

使用standard-version

# 安装
npm install -D standard-version

# 生成变更日志并提升版本
npx standard-version

# 预览而不提交
npx standard-version --dry-run

# 首次发布
npx standard-version --first-release

# 指定版本
npx standard-version --release-as minor
npx standard-version --release-as 1.1.0

# 预发布
npx standard-version --prerelease alpha

package.json:

{
  "scripts": {
    "release": "standard-version",
    "release:minor": "standard-version --release-as minor",
    "release:major": "standard-version --release-as major",
    "release:alpha": "standard-version --prerelease alpha"
  },
  "standard-version": {
    "types": [
      {"type": "feat", "section": "Features"},
      {"type": "fix", "section": "Bug Fixes"},
      {"type": "chore", "hidden": true},
      {"type": "docs", "section": "Documentation"},
      {"type": "style", "hidden": true},
      {"type": "refactor", "section": "Code Refactoring"},
      {"type": "perf", "section": "Performance Improvements"},
      {"type": "test", "hidden": true}
    ]
  }
}

使用release-please(GitHub Action)

.github/workflows/release.yml:

name: 发布

on:
  push:
    branches:
      - main

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: google-github-actions/release-please-action@v3
        id: release
        with:
          release-type: node
          package-name: my-package

      - uses: actions/checkout@v3
        if: ${{ steps.release.outputs.release_created }}

      - uses: actions/setup-node@v3
        if: ${{ steps.release.outputs.release_created }}
        with:
          node-version: 18
          registry-url: 'https://registry.npmjs.org'

      - run: npm ci
        if: ${{ steps.release.outputs.release_created }}

      - run: npm publish
        if: ${{ steps.release.outputs.release_created }}
        env:
          NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}

高级变更日志生成

Node.js脚本带完整功能

#!/usr/bin/env node
// generate-changelog.js

const { execSync } = require('child_process');
const fs = require('fs');

const COMMIT_PATTERN = /^(\w+)(\([\w-]+\))?(!)?:\s(.+)$/;

const TYPES = {
  feat: { section: 'Added', bump: 'minor' },
  fix: { section: 'Fixed', bump: 'patch' },
  docs: { section: 'Documentation', bump: null },
  style: { section: 'Style', bump: null },
  refactor: { section: 'Changed', bump: null },
  perf: { section: 'Performance', bump: 'patch' },
  test: { section: 'Tests', bump: null },
  build: { section: 'Build System', bump: null },
  ci: { section: 'CI', bump: null },
  chore: { section: 'Chores', bump: null },
  revert: { section: 'Reverts', bump: 'patch' },
};

function getCommitsSinceTag(tag) {
  const cmd = tag
    ? `git log ${tag}..HEAD --pretty=format:"%H|||%s|||%b|||%an|||%ae|||%ai"`
    : `git log --pretty=format:"%H|||%s|||%b|||%an|||%ae|||%ai"`;

  try {
    const output = execSync(cmd, { encoding: 'utf-8' });
    return output.split('
').filter(Boolean);
  } catch (error) {
    return [];
  }
}

function parseCommit(commitLine) {
  const [hash, subject, body, author, email, date] = commitLine.split('|||');

  const match = subject.match(COMMIT_PATTERN);
  if (!match) {
    return {
      hash,
      subject,
      body,
      author,
      email,
      date,
      type: 'other',
      scope: null,
      breaking: false,
      description: subject,
    };
  }

  const [, type, scope, breaking, description] = match;

  return {
    hash,
    subject,
    body,
    author,
    email,
    date,
    type,
    scope: scope ? scope.slice(1, -1) : null,
    breaking: Boolean(breaking) || body.includes('BREAKING CHANGE'),
    description,
  };
}

function groupCommits(commits) {
  const groups = {};
  const breaking = [];

  for (const commit of commits) {
    if (commit.breaking) {
      breaking.push(commit);
    }

    const typeInfo = TYPES[commit.type] || { section: 'Other Changes' };
    const section = typeInfo.section;

    if (!groups[section]) {
      groups[section] = [];
    }

    groups[section].push(commit);
  }

  return { groups, breaking };
}

function generateMarkdown(version, date, groups, breaking, options = {}) {
  const lines = [];

  lines.push(`## [${version}] - ${date}`);
  lines.push('');

  // 重大更改优先
  if (breaking.length > 0) {
    lines.push('### ⚠️ 重大更改');
    lines.push('');
    for (const commit of breaking) {
      lines.push(`- **${commit.description}** ([${commit.hash.slice(0, 7)}](../../commit/${commit.hash}))`);
      if (commit.body) {
        const breakingNote = commit.body.match(/BREAKING CHANGE:\s*(.+)/);
        if (breakingNote) {
          lines.push(`  ${breakingNote[1]}`);
        }
      }
    }
    lines.push('');
  }

  // 其他章节
  const sectionOrder = [
    'Added',
    'Changed',
    'Deprecated',
    'Removed',
    'Fixed',
    'Security',
    'Performance',
    'Documentation',
  ];

  for (const section of sectionOrder) {
    if (groups[section] && groups[section].length > 0) {
      lines.push(`### ${section}`);
      lines.push('');

      const commits = groups[section];
      const grouped = {};

      // 如果有范围则分组
      for (const commit of commits) {
        const key = commit.scope || '_default';
        if (!grouped[key]) grouped[key] = [];
        grouped[key].push(commit);
      }

      for (const [scope, scopeCommits] of Object.entries(grouped)) {
        if (scope !== '_default') {
          lines.push(`#### ${scope}`);
          lines.push('');
        }

        for (const commit of scopeCommits) {
          let line = `- ${commit.description}`;

          if (options.includeHash) {
            line += ` ([${commit.hash.slice(0, 7)}](../../commit/${commit.hash}))`;
          }

          if (options.includeAuthor) {
            line += ` - @${commit.author}`;
          }

          lines.push(line);
        }
      }

      lines.push('');
    }
  }

  return lines.join('
');
}

function getLatestTag() {
  try {
    return execSync('git describe --tags --abbrev=0', { encoding: 'utf-8' }).trim();
  } catch (error) {
    return null;
  }
}

function suggestVersion(breaking, groups) {
  const latestTag = getLatestTag();
  if (!latestTag) return '1.0.0';

  const [major, minor, patch] = latestTag.replace('v', '').split('.').map(Number);

  if (breaking.length > 0) {
    return `${major + 1}.0.0`;
  }

  const hasFeatures = groups['Added'] && groups['Added'].length > 0;
  if (hasFeatures) {
    return `${major}.${minor + 1}.0`;
  }

  return `${major}.${minor}.${patch + 1}`;
}

// 主执行
function main() {
  const args = process.argv.slice(2);
  const options = {
    includeHash: !args.includes('--no-hash'),
    includeAuthor: args.includes('--author'),
    version: args.find(a => a.startsWith('--version='))?.split('=')[1],
    since: args.find(a => a.startsWith('--since='))?.split('=')[1],
  };

  const latestTag = options.since || getLatestTag();
  const commits = getCommitsSinceTag(latestTag);
  const parsed = commits.map(parseCommit);
  const { groups, breaking } = groupCommits(parsed);

  const version = options.version || suggestVersion(breaking, groups);
  const date = new Date().toISOString().split('T')[0];

  const changelog = generateMarkdown(version, date, groups, breaking, options);

  // 读取现有变更日志或创建新的
  let existingChangelog = '';
  try {
    existingChangelog = fs.readFileSync('CHANGELOG.md', 'utf-8');
  } catch (error) {
    existingChangelog = '# 变更日志

本项目所有显著更改都将记录在此文件中。

';
  }

  // 在标题后插入新版本
  const lines = existingChangelog.split('
');
  const headerEnd = lines.findIndex(l => l.startsWith('## '));
  const newLines = [
    ...lines.slice(0, headerEnd === -1 ? lines.length : headerEnd),
    changelog,
    ...lines.slice(headerEnd === -1 ? lines.length : headerEnd),
  ];

  fs.writeFileSync('CHANGELOG.md', newLines.join('
'));

  console.log(`✓ 生成版本 ${version} 的变更日志`);
  console.log(`  - 处理 ${commits.length} 个提交`);
  console.log(`  - ${breaking.length} 个重大更改`);
  console.log(`  - 建议版本: ${version}`);
}

if (require.main === module) {
  main();
}

module.exports = { parseCommit, groupCommits, generateMarkdown };

使用:

# 使可执行
chmod +x generate-changelog.js

# 运行
./generate-changelog.js

# 带选项
./generate-changelog.js --version=2.0.0 --author --since=v1.5.0

Python版本

#!/usr/bin/env python3
# generate_changelog.py

import re
import subprocess
from datetime import datetime
from collections import defaultdict
from typing import List, Dict, Tuple

COMMIT_PATTERN = re.compile(r'^(\w+)(\([\w-]+\))?(!)?:\s(.+)$')

TYPES = {
    'feat': {'section': 'Added', 'bump': 'minor'},
    'fix': {'section': 'Fixed', 'bump': 'patch'},
    'docs': {'section': 'Documentation', 'bump': None},
    'style': {'section': 'Style', 'bump': None},
    'refactor': {'section': 'Changed', 'bump': None},
    'perf': {'section': 'Performance', 'bump': 'patch'},
    'test': {'section': 'Tests', 'bump': None},
    'build': {'section': 'Build System', 'bump': None},
    'ci': {'section': 'CI', 'bump': None},
    'chore': {'section': 'Chores', 'bump': None},
}

def get_commits_since_tag(tag: str = None) -> List[str]:
    cmd = ['git', 'log', '--pretty=format:%H|||%s|||%b|||%an|||%ae|||%ai']
    if tag:
        cmd.insert(2, f'{tag}..HEAD')

    try:
        result = subprocess.run(cmd, capture_output=True, text=True, check=True)
        return [line for line in result.stdout.split('
') if line]
    except subprocess.CalledProcessError:
        return []

def parse_commit(commit_line: str) -> Dict:
    parts = commit_line.split('|||')
    if len(parts) < 6:
        return None

    hash_val, subject, body, author, email, date = parts

    match = COMMIT_PATTERN.match(subject)
    if not match:
        return {
            'hash': hash_val,
            'subject': subject,
            'body': body,
            'author': author,
            'type': 'other',
            'scope': None,
            'breaking': False,
            'description': subject,
        }

    type_val, scope, breaking, description = match.groups()

    return {
        'hash': hash_val,
        'subject': subject,
        'body': body,
        'author': author,
        'type': type_val,
        'scope': scope[1:-1] if scope else None,
        'breaking': bool(breaking) or 'BREAKING CHANGE' in body,
        'description': description,
    }

def group_commits(commits: List[Dict]) -> Tuple[Dict, List]:
    groups = defaultdict(list)
    breaking = []

    for commit in commits:
        if commit['breaking']:
            breaking.append(commit)

        type_info = TYPES.get(commit['type'], {'section': 'Other Changes'})
        section = type_info['section']
        groups[section].append(commit)

    return dict(groups), breaking

def generate_markdown(version: str, groups: Dict, breaking: List) -> str:
    lines = [f"## [{version}] - {datetime.now().strftime('%Y-%m-%d')}", ""]

    if breaking:
        lines.append("### ⚠️ 重大更改")
        lines.append("")
        for commit in breaking:
            lines.append(f"- **{commit['description']}** ([{commit['hash'][:7]}](../../commit/{commit['hash']}))")
        lines.append("")

    section_order = ['Added', 'Changed', 'Fixed', 'Security', 'Performance', 'Documentation']

    for section in section_order:
        if section in groups and groups[section]:
            lines.append(f"### {section}")
            lines.append("")
            for commit in groups[section]:
                lines.append(f"- {commit['description']} ([{commit['hash'][:7]}](../../commit/{commit['hash']}))")
            lines.append("")

    return '
'.join(lines)

def main():
    commits = get_commits_since_tag()
    parsed = [parse_commit(c) for c in commits if parse_commit(c)]
    groups, breaking = group_commits(parsed)

    version = input("输入版本号(或按Enter键自动生成): ").strip() or "1.0.0"
    changelog = generate_markdown(version, groups, breaking)

    print(changelog)

    # 可选写入文件
    with open('CHANGELOG.md', 'r+') as f:
        content = f.read()
        f.seek(0, 0)
        f.write(changelog + '

' + content)

if __name__ == '__main__':
    main()

GitHub发布说明

自动化发布带说明

# .github/workflows/release.yml
name: 发布

on:
  push:
    tags:
      - 'v*'

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0

      - name: 生成变更日志
        id: changelog
        run: |
          # 获取前一个标签
          PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")

          # 生成变更日志
          if [ -z "$PREV_TAG" ]; then
            CHANGELOG=$(git log --pretty=format:"- %s (%h)" --reverse)
          else
            CHANGELOG=$(git log ${PREV_TAG}..HEAD --pretty=format:"- %s (%h)" --reverse)
          fi

          # 设置输出
          echo "changelog<<EOF" >> $GITHUB_OUTPUT
          echo "$CHANGELOG" >> $GITHUB_OUTPUT
          echo "EOF" >> $GITHUB_OUTPUT

      - name: 创建发布
        uses: actions/create-release@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag_name: ${{ github.ref }}
          release_name: 发布 ${{ github.ref }}
          body: |
            ## 此版本中的更改
            ${{ steps.changelog.outputs.changelog }}
          draft: false
          prerelease: false

最佳实践

提交消息

  • 使用常规提交: 启用自动化变更日志生成
  • 描述清晰: 简洁、清晰地描述更改
  • 包含上下文: 说明为什么更改,而不仅仅是做了什么
  • 引用问题: 链接到相关问题编号

变更日志组织

  • 按类型分组: 功能、修复、重大更改等
  • 按时间顺序排序: 最新更改优先
  • 包含日期: 每个版本应有发布日期
  • 链接到提交: 提供详细信息的链接

版本管理

  • 遵循semver: 主版本.次版本.补丁版本控制
  • 记录重大更改: 突出显示重大更改
  • 提供迁移指南: 帮助用户升级
  • 跟踪废弃功能: 在移除功能前警告

维护

  • 定期更新: 每次发布时生成变更日志
  • 发布前审核: 手动验证自动化输出
  • 保持格式一致: 使用相同的结构
  • 归档旧版本: 保持完整历史可用

备注

  • 始终使用常规提交进行自动化变更日志生成
  • 在变更日志顶部包含重大更改
  • 为主版本提升提供迁移指南
  • 链接到详细文档
  • 保持变更日志条目面向用户(非技术性)
  • 在发布前审核和编辑生成的变更日志
  • 一致地使用语义版本控制
  • 在CI/CD流水线中自动化变更日志生成
  • 考虑使用standard-version或release-please等工具
  • 在git仓库中与代码一起维护变更日志