GitHub Release automatically generates workflow practice processes
Introduction
Recently, when I was sorting out the project release, I found that each time I had to manually write the release description, which was not only cumbersome, but also easy to miss some important changes. So I spent some time studying GitHub Actions and configuring a Release to automatically generate workflows.

After using it for a while, I feel that the effect is not bad, and every time I post it, it becomes much easier. Today, I will share with you my practice process, hoping to help friends who have similar needs.
📋 Preview
After pushing the tag, GitHub Actions will automatically generate a Release in the following format:
## 📋 版本说明
📈 [查看变更对比](https://github.com/xxx/xxx/compare/v0.1.0...v1.0.0)
✨ 新功能
- **api** 添加百度翻译 API 支持
- **nginx** 支持自动生成 301 跳转规则
🐛 Bug 修复
- **csv** 修复中文 slug 解码问题
📝 文档更新
- **readme** 更新目录结构说明
---
🚀 发布于 2026/06/15

🔧 Implementation steps
Step 1: Create a workflow file
First, I created in the project root directory .github/workflows/release.yml file:
name: Release
on:
push:
tags:
- "v*"
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Generate Release Notes
run: |
echo "## 📋 版本说明" > /tmp/release_notes.md
echo "" >> /tmp/release_notes.md
# 获取前一个标签
PREV_TAG=$(git tag --list 'v*' --sort=-v:refname | grep -A1 "${{ github.ref_name }}" | tail -n1)
if [ -n "$PREV_TAG" ]; then
COMPARE_URL="${{ github.event.repository.html_url }}/compare/${PREV_TAG}...${{ github.ref_name }}"
echo "📈 [查看变更对比]($COMPARE_URL)" >> /tmp/release_notes.md
echo "" >> /tmp/release_notes.md
fi
# 初始化分类文件
> /tmp/feat.txt
> /tmp/fix.txt
> /tmp/docs.txt
> /tmp/refactor.txt
> /tmp/perf.txt
> /tmp/test.txt
> /tmp/chore.txt
> /tmp/style.txt
# 解析提交并分类
if [ -n "$PREV_TAG" ]; then
COMMITS=$(git log --oneline "$PREV_TAG"..HEAD)
else
COMMITS=$(git log --oneline)
fi
echo "$COMMITS" | while read -r COMMIT_LINE; do
# 提取类型、范围和描述
TYPE=$(echo "$COMMIT_LINE" | awk '{print $2}' | sed 's/^\([a-z]*\).*/\1/')
REST=$(echo "$COMMIT_LINE" | cut -d' ' -f3-)
# 提取范围(如果有)
SCOPE=$(echo "$REST" | sed -n 's/^\(([^)]*)\):.*/\1/p')
DESCRIPTION=$(echo "$REST" | sed 's/^([^)]*): //')
if [ -z "$DESCRIPTION" ]; then
DESCRIPTION="$REST"
fi
case "$TYPE" in
feat) echo "- ${SCOPE:+**$SCOPE** }$DESCRIPTION" >> /tmp/feat.txt ;;
fix) echo "- ${SCOPE:+**$SCOPE** }$DESCRIPTION" >> /tmp/fix.txt ;;
docs) echo "- ${SCOPE:+**$SCOPE** }$DESCRIPTION" >> /tmp/docs.txt ;;
refactor) echo "- ${SCOPE:+**$SCOPE** }$DESCRIPTION" >> /tmp/refactor.txt ;;
perf) echo "- ${SCOPE:+**$SCOPE** }$DESCRIPTION" >> /tmp/perf.txt ;;
test) echo "- ${SCOPE:+**$SCOPE** }$DESCRIPTION" >> /tmp/test.txt ;;
chore) echo "- ${SCOPE:+**$SCOPE** }$DESCRIPTION" >> /tmp/chore.txt ;;
style) echo "- ${SCOPE:+**$SCOPE** }$DESCRIPTION" >> /tmp/style.txt ;;
esac
done
# 输出分类结果
if [ -s /tmp/feat.txt ]; then
echo "✨ 新功能" >> /tmp/release_notes.md
echo "" >> /tmp/release_notes.md
cat /tmp/feat.txt >> /tmp/release_notes.md
echo "" >> /tmp/release_notes.md
fi
if [ -s /tmp/fix.txt ]; then
echo "🐛 Bug 修复" >> /tmp/release_notes.md
echo "" >> /tmp/release_notes.md
cat /tmp/fix.txt >> /tmp/release_notes.md
echo "" >> /tmp/release_notes.md
fi
if [ -s /tmp/docs.txt ]; then
echo "📝 文档更新" >> /tmp/release_notes.md
echo "" >> /tmp/release_notes.md
cat /tmp/docs.txt >> /tmp/release_notes.md
echo "" >> /tmp/release_notes.md
fi
if [ -s /tmp/refactor.txt ]; then
echo "🔧 代码重构" >> /tmp/release_notes.md
echo "" >> /tmp/release_notes.md
cat /tmp/refactor.txt >> /tmp/release_notes.md
echo "" >> /tmp/release_notes.md
fi
if [ -s /tmp/perf.txt ]; then
echo "⚡ 性能优化" >> /tmp/release_notes.md
echo "" >> /tmp/release_notes.md
cat /tmp/perf.txt >> /tmp/release_notes.md
echo "" >> /tmp/release_notes.md
fi
if [ -s /tmp/test.txt ]; then
echo "🧪 测试" >> /tmp/release_notes.md
echo "" >> /tmp/release_notes.md
cat /tmp/test.txt >> /tmp/release_notes.md
echo "" >> /tmp/release_notes.md
fi
if [ -s /tmp/chore.txt ]; then
echo "🔨 构建/工具" >> /tmp/release_notes.md
echo "" >> /tmp/release_notes.md
cat /tmp/chore.txt >> /tmp/release_notes.md
echo "" >> /tmp/release_notes.md
fi
if [ -s /tmp/style.txt ]; then
echo "🎨 代码风格" >> /tmp/release_notes.md
echo "" >> /tmp/release_notes.md
cat /tmp/style.txt >> /tmp/release_notes.md
echo "" >> /tmp/release_notes.md
fi
echo "---" >> /tmp/release_notes.md
echo "" >> /tmp/release_notes.md
echo "🚀 发布于 $(date +'%Y/%m/%d')" >> /tmp/release_notes.md
echo "=== Generated Release Notes ==="
cat /tmp/release_notes.md
echo "=== End ==="
- name: Create Release
uses: softprops/action-gh-release@v2
with:
name: ${{ github.ref_name }}
body_path: /tmp/release_notes.md
draft: false
prerelease: ${{ contains(github.ref_name, 'beta') || contains(github.ref_name, 'rc') }}
Step 2: Code Submission Specification
This workflow depends on Contracted Submission(Conventional commits) to classify changes. At first, I used a more casual commit format, and later found that the Release Notes generated in this way did not work very well, so I changed it to the standard format:
<类型>(<范围>): <描述>
Type description:
| Types of | Explanation | Give typical examples |
|---|---|---|
feat | New function | feat(api): add Baidu translation API support |
fix | bug fix | fix(csv): Fix Chinese slug decoding problem |
docs | Documentation Update | docs(readme): Update directory structure description |
Refactor | code refactoring | refactor(utils): optimize cache logic |
perf | performance optimization | perf(cache): reduce API calls |
Test | test related | test(merge): add merge function test |
chore | Build/Tools | Chore(Docker): Update Dockerfile |
Style | code style | style(go): Format Go code |
Step 3: Submit workflow files
After configuring the workflow file, I submitted it to the repository:
git add .github/workflows/release.yml
git commit -m "chore(release): 添加 Release 自动生成流程"
git push origin main
Step 4: Create a tag to trigger the Release
Finally, create a tag to test it out:
# 创建标签
git tag -a v1.0.0 -m "Release v1.0.0"
# 推送标签
git push origin v1.0.0
After pushing the tag, the GitHub Actions will start to execute automatically, and after a few minutes, you can see the automatically generated Release on the Releases page.
🔍 Workflow Principle
trigger condition
The workflow I configured will be pushed to v Triggered when the label at the beginning:
on:
push:
tags:
- "v*"
core process
The whole process is probably as follows:
- checkout: Get the full code history (
fetch-depth: 0) - Generate Release Notes:
- Get the previous label version
- Generate version comparison link
- parse and categorize by type
- Generate a formatted Markdown document
- Create Release: use
softprops/action-gh-releaseCreate Release
Automatic classification logic
The workflow will automatically group commits in groups, and this logic is what I implemented with the bash script:
# 提取类型
TYPE=$(echo "$COMMIT_LINE" | awk '{print $2}' | sed 's/^\([a-z]*\).*/\1/')
# 分类输出
case "$TYPE" in
feat) echo "- ..." >> /tmp/feat.txt ;;
fix) echo "- ..." >> /tmp/fix.txt ;;
# ...
esac
At first, I tried to use the official GitHub automatic generation function, but I found that it was not flexible enough, and finally I wrote a script myself.
🎯 Advanced configuration
Pre-release version support
The workflow automatically identifies pre-release versions:
prerelease: ${{ contains(github.ref_name, 'beta') || contains(github.ref_name, 'rc') }}
Example:
v1.0.0-beta.1→ Pre-release versionv1.0.0-rc.1→ Pre-release versionv1.0.0→ Official version
I used this function a lot when I released the test version, which can avoid the update notification of the beta version for official users.
Add custom categories
If you need to add custom categories, you can modify Generate Release Notes Steps:
# 添加自定义分类
> /tmp/custom.txt
# 在 case 语句中添加
case "$TYPE" in
# ... 其他类型
custom) echo "- ${SCOPE:+**$SCOPE** }$DESCRIPTION" >> /tmp/custom.txt ;;
esac
# 在输出部分添加
if [ -s /tmp/custom.txt ]; then
echo "🔖 自定义分类" >> /tmp/release_notes.md
echo "" >> /tmp/release_notes.md
cat /tmp/custom.txt >> /tmp/release_notes.md
echo "" >> /tmp/release_notes.md
fi
Add attachment
If you need to add a binary file in Release, you can add the following steps:
- name: Build
run: |
# 构建项目
go build -o myapp main.go
- name: Create Release
uses: softprops/action-gh-release@v2
with:
name: ${{ github.ref_name }}
body_path: /tmp/release_notes.md
files: |
myapp
I use this function to automatically compile and publish binary files in the Go project, and the user can run it directly after downloading, which is very convenient.
🐛 FAQ
Q1: Release is not automatically generated?
I have encountered this problem and checked several aspects:
- Whether the label name is
vbeginning (such asv1.0.0) .github/workflows/release.ymlWhether the file exists- Is the GitHub Actions running status normal?
Q2: Is Release Notes content empty?
Reason: The commit message does not conform to the convention submission format.
Solution: Use the canonical commit format:
git commit -m "feat(api): 添加新功能" # ✅ 正确
git commit -m "add new feature" # ❌ 不推荐
I also encountered this problem at the beginning, and then I changed to the canonical commit format.
Q3: How to update the published release?
# 删除旧标签
git tag -d v1.0.0
git push origin :v1.0.0
# 创建新标签
git tag -a v1.0.0 -m "Release v1.0.0"
git push origin v1.0.0
Q4: How to skip automatic release?
Add in tag name no-release Suffix:
git tag -a v1.0.0-no-release -m "内部测试版本"
📊 Best practices
1. Use semantic version control
v<主版本号>.<次版本号>.<修订号>
- Main version number: Incompatible API changes
- Sub-version number: New features for backward compatibility
- revision number: Backward compatible bug fixes
I didn’t pay much attention to the version number specification at first, but later I found that when the user reported the problem, the inconsistent version number would indeed bring some trouble, and now I strictly follow this specification.
2. Keep the COMMIT granularity moderate
Each commit should be a separate, verifiable change unit:
# ✅ 推荐
git commit -m "feat(api): 添加用户注册接口"
# ❌ 不推荐
git commit -m "修改了很多东西"
I used to put a lot of changes in a commit, and later found that the Release Notes generated in this way are very readable, and now I will try to keep the granularity of each commit in a moderate size.
3. Clean up old labels regularly
# 查看所有标签
git tag
# 删除本地标签
git tag -d v0.1.0
# 删除远程标签
git push origin :v0.1.0
After the project is long, there will be more and more labels, and I usually clean up some old labels that I no longer need to keep the warehouse clean.
📚 Reference link
- GitHub Actions Documentation
- Contracted Submission Specification
- semantic version control
- softprops/action-gh-release
📝 Summary
Through the configuration of this article, I have implemented:
- ✅ Automatically generate Release description: Automatic classification based on commit history
- ✅ Version comparison link: Automatic generation comparison with the previous version
- ✅ Pre-release version support: Automatically identify beta/RC versions
- ✅ fully automated: Push tags can be triggered
Now you can focus on code development and let GitHub Actions help you with the tedious work of Release! 🚀
If you have any questions or suggestions, please leave a message in the comment area to discuss!
📌 Related articles