-
Notifications
You must be signed in to change notification settings - Fork 0
ci: add auto-release composite action #26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
044c65a
ci: add auto-release composite action
yuanyuanxin 2f015da
feat(auto-release): add full feature support
yuanyuanxin b77766b
feat(auto-release): add body and tag outputs
yuanyuanxin 525537b
fix(auto-release): add refs/tags validation
yuanyuanxin fc88048
fix(auto-release): validate tag must start with v prefix
yuanyuanxin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| # Auto Release from CHANGELOG | ||
|
|
||
| 解析 CHANGELOG.md 并自动创建 GitHub Release 的 Composite Action。 | ||
|
|
||
| ## 特性 | ||
|
|
||
| - ✅ **分类映射**: Added→New Features, Changed→Improvements, Fixed→Bug Fixes | ||
| - ✅ **CAUTION 警告块**: Removed/Deprecated/Security 显示为警告 | ||
| - ✅ **中英文支持**: 支持中文分类(新增/变更/修复/移除/废弃/安全) | ||
| - ✅ **Full Changelog 链接**: 自动生成版本对比链接 | ||
| - ✅ **Upsert 模式**: 已存在的 Release 会更新而非报错 | ||
| - ✅ **预发布版本支持**: 支持 `1.0.0-rc4` 格式的版本号 | ||
| - ✅ **CRLF 兼容**: 自动处理 Windows 换行符 | ||
|
|
||
| ## 使用方式 | ||
|
|
||
| ```yaml | ||
| name: Auto Release | ||
|
|
||
| on: | ||
| push: | ||
| tags: | ||
| - 'v*' | ||
|
|
||
| permissions: | ||
| contents: write | ||
|
|
||
| jobs: | ||
| release: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Auto Release | ||
| uses: wuji-technology/.github/actions/auto-release@v1 | ||
| with: | ||
| github-token: ${{ secrets.GITHUB_TOKEN }} | ||
| ``` | ||
|
|
||
| ## 输入参数 | ||
|
|
||
| | 参数 | 必填 | 默认值 | 说明 | | ||
| |-----|------|-------|------| | ||
| | `changelog-path` | 否 | `CHANGELOG.md` | CHANGELOG 文件路径 | | ||
| | `github-token` | 是 | - | GitHub Token | | ||
| | `draft` | 否 | `false` | 是否创建草稿 Release | | ||
| | `prerelease` | 否 | `true` | 是否标记为预发布 | | ||
|
|
||
| ## 输出 | ||
|
|
||
| | 输出 | 说明 | | ||
| |-----|------| | ||
| | `version` | 发布的版本号 | | ||
| | `release-url` | Release 页面 URL | | ||
| | `body` | 解析后的 Release Notes 内容 | | ||
| | `tag` | Git tag 名称 | | ||
|
|
||
| ## 高级用法 | ||
|
|
||
| ### 上传 Release Assets | ||
|
|
||
| ```yaml | ||
| - uses: wuji-technology/.github/actions/auto-release@v1 | ||
| with: | ||
| github-token: ${{ secrets.GITHUB_TOKEN }} | ||
|
|
||
| - name: Upload assets | ||
| run: gh release upload ${{ github.ref_name }} dist/*.whl dist/*.tar.gz | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| ``` | ||
|
|
||
| ### 同步到其他仓库 | ||
|
|
||
| ```yaml | ||
| - uses: wuji-technology/.github/actions/auto-release@v1 | ||
| id: release | ||
| with: | ||
| github-token: ${{ secrets.GITHUB_TOKEN }} | ||
|
|
||
| - name: Sync to public repo | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| github-token: ${{ secrets.RELEASE_TOKEN }} | ||
| script: | | ||
| await github.rest.repos.createRelease({ | ||
| owner: context.repo.owner, | ||
| repo: 'public-repo-name', | ||
| tag_name: '${{ steps.release.outputs.tag }}', | ||
| name: '${{ steps.release.outputs.tag }}', | ||
| body: `${{ steps.release.outputs.body }}`, | ||
| prerelease: true | ||
| }); | ||
| ``` | ||
|
|
||
| ## CHANGELOG 格式要求 | ||
|
|
||
| 支持 [Keep a Changelog](https://keepachangelog.com/) 格式: | ||
|
|
||
| ```markdown | ||
| ## [1.0.0] - 2026-01-27 | ||
|
|
||
| ### Added | ||
| - 新功能 A | ||
| - 新功能 B | ||
|
|
||
| ### Fixed | ||
| - 修复问题 A | ||
|
|
||
| ### Removed | ||
| - 移除旧 API | ||
| ``` | ||
|
|
||
| ## Release 输出示例 | ||
|
|
||
| ```markdown | ||
| ## v1.0.0 (2026-01-27) | ||
|
|
||
| ### New Features | ||
| - 新功能 A | ||
| - 新功能 B | ||
|
|
||
| ### Bug Fixes | ||
| - 修复问题 A | ||
|
|
||
| > [!CAUTION] | ||
| > - **Removed**: 移除旧 API | ||
|
|
||
| --- | ||
|
|
||
| **Full Changelog**: [v0.9.0...v1.0.0](https://github.com/owner/repo/compare/v0.9.0...v1.0.0) | ||
| ``` | ||
|
|
||
| ## 分类映射规则 | ||
|
|
||
| | CHANGELOG 分类 | Release 分类 | 警告 | | ||
| |---------------|-------------|------| | ||
| | Added / 新增 | New Features | - | | ||
| | Changed / 变更 | Improvements | - | | ||
| | Fixed / 修复 | Bug Fixes | - | | ||
| | Removed / 移除 | CAUTION | ⚠️ | | ||
| | Deprecated / 废弃 | CAUTION | ⚠️ | | ||
| | Security / 安全 | CAUTION | ⚠️ | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,214 @@ | ||
| name: 'Auto Release from CHANGELOG' | ||
| description: '解析 CHANGELOG.md 并自动创建 GitHub Release' | ||
| author: 'wuji-technology' | ||
|
|
||
| inputs: | ||
| changelog-path: | ||
| description: 'CHANGELOG 文件路径' | ||
| required: false | ||
| default: 'CHANGELOG.md' | ||
| github-token: | ||
| description: 'GitHub Token' | ||
| required: true | ||
| draft: | ||
| description: '是否创建草稿 Release' | ||
| required: false | ||
| default: 'false' | ||
| prerelease: | ||
| description: '是否标记为预发布' | ||
| required: false | ||
| default: 'true' | ||
|
|
||
| outputs: | ||
| version: | ||
| description: '发布的版本号' | ||
| value: ${{ steps.release.outputs.version }} | ||
| release-url: | ||
| description: 'Release 页面 URL' | ||
| value: ${{ steps.release.outputs.html_url }} | ||
| body: | ||
| description: '解析后的 Release Notes 内容' | ||
| value: ${{ steps.release.outputs.body }} | ||
| tag: | ||
| description: 'Git tag 名称' | ||
| value: ${{ steps.release.outputs.tag }} | ||
|
|
||
| runs: | ||
| using: 'composite' | ||
| steps: | ||
| - name: Parse CHANGELOG and Upsert Release | ||
| id: release | ||
| uses: actions/github-script@v7 | ||
| env: | ||
| CHANGELOG_PATH: ${{ inputs.changelog-path }} | ||
| INPUT_DRAFT: ${{ inputs.draft }} | ||
| INPUT_PRERELEASE: ${{ inputs.prerelease }} | ||
| with: | ||
| github-token: ${{ inputs.github-token }} | ||
| script: | | ||
| const fs = require('fs'); | ||
|
|
||
| const changelogPath = process.env.CHANGELOG_PATH || 'CHANGELOG.md'; | ||
|
|
||
| // 检查 CHANGELOG.md 是否存在 | ||
| if (!fs.existsSync(changelogPath)) { | ||
| core.setFailed(`${changelogPath} not found`); | ||
| return; | ||
| } | ||
| const changelog = fs.readFileSync(changelogPath, 'utf8') | ||
| .replace(/\r\n?/g, '\n'); | ||
|
|
||
| // 验证触发事件是否为 tag push | ||
| if (!context.ref.startsWith('refs/tags/')) { | ||
| core.setFailed(`This action must be triggered by a tag push. Current ref: ${context.ref}`); | ||
| return; | ||
| } | ||
|
|
||
| // 从 tag 提取版本号(支持预发布版本如 1.0.0-rc4) | ||
| const tag = context.ref.replace('refs/tags/', ''); | ||
|
|
||
| // 验证 tag 格式必须以 v 开头 | ||
| if (!tag.startsWith('v')) { | ||
| core.setFailed(`Invalid tag format: "${tag}". Tag must start with "v" (e.g., v1.0.0, v1.0.0-rc1). This is required for consistent Full Changelog links.`); | ||
| return; | ||
| } | ||
|
|
||
| const version = tag.replace(/^v/, ''); | ||
| console.log(`Processing tag: ${tag}, version: ${version}`); | ||
|
|
||
| // 分类映射 | ||
| const categoryMap = { | ||
| 'Added': 'New Features', '新增': 'New Features', | ||
| 'Changed': 'Improvements', '变更': 'Improvements', | ||
| 'Fixed': 'Bug Fixes', '修复': 'Bug Fixes' | ||
| }; | ||
| const cautionCategories = { | ||
| 'Removed': 'Removed', '移除': 'Removed', | ||
| 'Deprecated': 'Deprecated', '废弃': 'Deprecated', | ||
| 'Security': 'Security', '安全': 'Security' | ||
| }; | ||
|
|
||
| // 解析版本内容 | ||
| function parseContent(content) { | ||
| const sections = { 'New Features': [], 'Improvements': [], 'Bug Fixes': [] }; | ||
| const cautionItems = []; | ||
| let currentCategory = ''; | ||
| let currentCautionPrefix = ''; | ||
|
|
||
| for (const line of content.trim().split('\n')) { | ||
| const catMatch = line.match(/^### (.+)/); | ||
| if (catMatch) { | ||
| const cat = catMatch[1].trim(); | ||
| if (categoryMap[cat]) { | ||
| currentCategory = categoryMap[cat]; | ||
| currentCautionPrefix = ''; | ||
| } else if (cautionCategories[cat]) { | ||
| currentCategory = 'CAUTION'; | ||
| currentCautionPrefix = cautionCategories[cat]; | ||
| } else { | ||
| currentCategory = ''; | ||
| } | ||
| } else if (line.startsWith('- ')) { | ||
| const item = line.slice(2); | ||
| if (currentCategory === 'CAUTION') { | ||
| cautionItems.push(`**${currentCautionPrefix}**: ${item}`); | ||
| } else if (currentCategory && sections[currentCategory]) { | ||
| sections[currentCategory].push(item); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| let body = ''; | ||
| for (const section of ['New Features', 'Improvements', 'Bug Fixes']) { | ||
| if (sections[section].length > 0) { | ||
| body += `### ${section}\n${sections[section].map(i => `- ${i}`).join('\n')}\n\n`; | ||
| } | ||
| } | ||
| if (cautionItems.length > 0) { | ||
| body += `> [!CAUTION]\n${cautionItems.map(i => `> - ${i}`).join('\n')}\n\n`; | ||
| } | ||
| return body.trim(); | ||
| } | ||
|
|
||
| // 转义版本号中的特殊字符用于正则匹配 | ||
| const escapedVersion = version.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | ||
|
|
||
| // 更健壮的版本匹配正则(支持多种日期格式和可选内容) | ||
| const versionRegex = new RegExp(`## \\[${escapedVersion}\\](?:[^\\n]*-\\s*(\\d{4}-\\d{2}-\\d{2}))?[^\\n]*\\n([\\s\\S]*?)(?=\\n## \\[|$)`); | ||
| const match = changelog.match(versionRegex); | ||
|
|
||
| if (!match) { | ||
| core.setFailed(`Version ${version} not found in CHANGELOG.md`); | ||
| return; | ||
| } | ||
|
|
||
| const date = match[1] || new Date().toISOString().split('T')[0]; | ||
| const content = match[2]; | ||
| let body = parseContent(content); | ||
|
|
||
| if (!body) { | ||
| console.log(`Warning: No content parsed for version ${version}`); | ||
| } | ||
|
|
||
| // 获取所有版本用于 Full Changelog(支持预发布版本) | ||
| const allVersions = [...changelog.matchAll(/## \[(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.]+)?)\]/g)].map(m => m[1]); | ||
| const versionIndex = allVersions.indexOf(version); | ||
| if (versionIndex === -1) { | ||
| console.log(`Warning: version ${version} not found in version list for Full Changelog`); | ||
| } else { | ||
| const prevVersion = allVersions[versionIndex + 1]; | ||
| if (prevVersion) { | ||
| body += `\n\n---\n\n**Full Changelog**: [v${prevVersion}...${tag}](https://github.com/${context.repo.owner}/${context.repo.repo}/compare/v${prevVersion}...${tag})`; | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // 检查 release 是否已存在(upsert 模式) | ||
| let releaseId = null; | ||
| try { | ||
| const { data } = await github.rest.repos.getReleaseByTag({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| tag: tag | ||
| }); | ||
| releaseId = data.id; | ||
| console.log(`Found existing release: ${tag} (id: ${releaseId})`); | ||
| } catch (e) { | ||
| if (e.status !== 404) throw e; | ||
| console.log(`No existing release for tag: ${tag}`); | ||
| } | ||
|
|
||
| const releaseBody = `## ${tag} (${date})\n\n${body}`; | ||
| const isDraft = process.env.INPUT_DRAFT === 'true'; | ||
| const isPrerelease = process.env.INPUT_PRERELEASE === 'true'; | ||
|
|
||
| const releaseData = { | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| tag_name: tag, | ||
| name: tag, | ||
| body: releaseBody, | ||
| draft: isDraft, | ||
| prerelease: isPrerelease, | ||
| make_latest: 'false' | ||
| }; | ||
|
|
||
| let releaseUrl = ''; | ||
| if (releaseId) { | ||
| // 更新现有 release | ||
| const { data } = await github.rest.repos.updateRelease({ | ||
| ...releaseData, | ||
| release_id: releaseId | ||
| }); | ||
| releaseUrl = data.html_url; | ||
| console.log(`Updated release: ${tag}`); | ||
| } else { | ||
| // 创建新 release | ||
| const { data } = await github.rest.repos.createRelease(releaseData); | ||
| releaseUrl = data.html_url; | ||
| console.log(`Created release: ${tag}`); | ||
| } | ||
|
|
||
| core.setOutput('version', version); | ||
| core.setOutput('html_url', releaseUrl); | ||
| core.setOutput('body', releaseBody); | ||
| core.setOutput('tag', tag); | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.