name: 📦 自动发布
on:
push:
tags:
- "[0-9]*"
concurrency:
group: release-${{ github.repository }}
cancel-in-progress: false
# ==========================================
# 🔧 配置区域 - 根据你的项目修改
# ==========================================
env:
# ===== Token 配置 =====
# Gitea/GitHub 访问令牌
# 用途:
# 1. Git 操作:克隆仓库、推送提交
# 2. API 调用:创建 Release、上传附件
# 权限要求:
# - repo (仓库完整访问权限)
# - write:packages (可选,如需上传包)
# 配置位置:仓库 Settings > Secrets > Actions
# Secret 名称:WORKFLOW (或其他自定义名称)
#
# 💡 提示:同一个 token 可用于所有操作
# 如需分离权限,可配置两个不同的 token:
# - GIT_TOKEN: 用于 git clone/push
# - API_TOKEN: 用于 API 调用
ACCESS_TOKEN: ${{ secrets.WORKFLOW }}
# ===== 工作区配置 =====
# 完整克隆的工作目录 - 自建的runner可以复用工作区
WORKSPACE_DIR: "/home/workspace"
# ===== 分支配置 =====
# 主分支名称(用于推送 CHANGELOG 更新)
# 如果留空,会自动检测(main 或 master)
MAIN_BRANCH: "main"
# ===== 服务器配置 =====
# Gitea 服务器地址(用于生成头像链接和 API 调用)
GITEA_SERVER: "https://git.mytsl.cn"
# ===== CHANGELOG 配置 =====
# CHANGELOG 变更列表标题
CHANGELOG_SECTION_TITLE: "### :pencil: What's Changed"
# CHANGELOG 贡献者列表标题
CHANGELOG_CONTRIBUTORS_TITLE: "### :busts_in_silhouette: Contributors"
# CHANGELOG 版本号处理方式
# - "full": 使用完整 tag 名称(2025.10.31-1-beta → ## :bookmark: 2025.10.31-1-beta)
# - "strip": 去除 pre-release 后缀(1.0.0-beta1 → ## :bookmark: 1.0.0)
# ⚠️ 推荐使用 "strip" 模式以支持多个开发版本叠加到同一CHANGELOG区域
CHANGELOG_VERSION_MODE: "strip"
# ===== RELEASE 配置 =====
# Release 标题模板
# 可用变量: {version} - 将被替换为实际的版本号
RELEASE_TITLE: "{version}"
# Pre-release 配置
# - "false": 正式版本(会被标记为 latest)
# - "true": 预发布版本(会被标记为 pre-release)
# - "auto": 自动判断(根据 tag 名称中是否包含 alpha/beta/rc)
RELEASE_PRERELEASE_MODE: "auto"
# Draft 配置(是否创建为草稿)
# - "true": 创建为草稿,不会立即发布
# - "false": 立即发布 Release
RELEASE_IS_DRAFT: "false"
# 统一管理 pre-release 关键词
PRERELEASE_KEYWORDS: "alpha|beta|rc|pre|preview|dev|test"
# 额外要上传到 Release 的文件(空格分隔)
# 例如: "README.md LICENSE docs/guide.pdf"
ADDITIONAL_RELEASE_FILES: ""
# ===== Git 提交配置 =====
# Git 用户配置
GIT_USER_NAME: "github-actions[bot]"
GIT_USER_EMAIL: "github-actions[bot]@users.noreply.github.com"
# Git 提交消息模板
# 可用变量: {version} - 将被替换为实际的版本号
# [skip ci] 标记防止触发无限循环
COMMIT_MESSAGE: ":memo: Auto update CHANGELOG for {version} [skip ci]"
# ===== 提交过滤配置 =====
# 需要忽略的提交模式(支持正则表达式)
# 这些提交不会被添加到 CHANGELOG 中
# 使用 | 分隔多个模式
#
# ⚠️ 重要说明:
# - 使用 ^ 表示行首匹配,$ 表示行尾匹配
# - [skip ci] 和 [ci skip] 仅匹配行尾,避免误过滤其他提交
# - 如果要完全匹配某个字符串,使用 ^...$
IGNORE_PATTERNS: >-
^Merge |
^:memo: Auto update CHANGELOG|
\\[skip ci\\]|
\\[ci skip\\]|
^Bump version|
^Release v|
^chore: update config$|
^style: |
^chore: format|
^chore: lint|
^Initial commit$
# ========================================
# 📌 版本号规范说明 (SemVer)
# ========================================
#
# 格式: MAJOR.MINOR.PATCH[-prerelease]
#
# 开发版本示例:
# - 1.0.0-beta1, 1.0.0-beta2, 1.0.0-beta10 等
# - 1.2.0-alpha1, 1.2.0-rc1 等
#
# 正式版本示例:
# - 1.0.0 (首个稳定版,PATCH必须从0开始)
# - 1.1.0 (新增功能,向后兼容)
# - 1.1.1 (Bug修复)
# - 2.0.0 (破坏性变更,不向后兼容)
#
# ⚠️ 重要规则:
# 1. 开发版本都以 .0-beta[N] 或 .0-alpha[N] 结尾
# 2. 正式发布必须从 .0 开始(如 1.0.0, 1.1.0, 2.0.0)
# 3. 多个开发版本(如1.0.0-beta1, 1.0.0-beta2)会累加到同一个CHANGELOG区域(1.0.0)
# 4. CHANGELOG内容是叠加而非覆盖
jobs:
changelog-and-release:
runs-on: ubuntu-22.04
permissions:
contents: write
steps:
- name: ⚙️ 加载配置
id: config
run: |
echo "======================================"
echo "⚙️ 加载工作流配置"
echo "======================================"
# 从环境变量读取配置
VERSION="${{ github.ref_name }}"
KEYWORDS="${{ env.PRERELEASE_KEYWORDS }}"
STRIP_REGEX="-($KEYWORDS)[0-9]*$" # 用于 sed 删除后缀
MATCH_REGEX="($KEYWORDS)" # 用于 bash =~ 匹配
# 处理 CHANGELOG 版本号
if [[ "${{ env.CHANGELOG_VERSION_MODE }}" == "strip" ]]; then
# 去除 pre-release 后缀(-beta, -rc1, -alpha 等)
CHANGELOG_VERSION=$(echo "$VERSION" | sed -E "s/${STRIP_REGEX}//")
echo "📝 CHANGELOG version mode: strip"
echo " Tag: $VERSION → CHANGELOG: $CHANGELOG_VERSION"
else
# 使用完整 tag 名称
CHANGELOG_VERSION="$VERSION"
echo "📝 CHANGELOG version mode: full"
echo " Tag: $VERSION → CHANGELOG: $CHANGELOG_VERSION"
fi
# 处理 Release 标题
RELEASE_TITLE_PROCESSED="${{ env.RELEASE_TITLE }}"
RELEASE_TITLE_PROCESSED="${RELEASE_TITLE_PROCESSED//\{version\}/$VERSION}"
# 处理提交消息
COMMIT_MSG="${{ env.COMMIT_MESSAGE }}"
COMMIT_MSG="${COMMIT_MSG//\{version\}/$VERSION}"
# 智能判断 pre-release
if [[ "${{ env.RELEASE_PRERELEASE_MODE }}" == "auto" ]]; then
if [[ "$VERSION" =~ $MATCH_REGEX ]]; then
RELEASE_IS_PRERELEASE="true"
PRERELEASE_DETECTED="yes (auto-detected: $VERSION contains pre-release keyword)"
else
RELEASE_IS_PRERELEASE="false"
PRERELEASE_DETECTED="no (auto-detected: stable release)"
fi
else
RELEASE_IS_PRERELEASE="${{ env.RELEASE_PRERELEASE_MODE }}"
PRERELEASE_DETECTED="${{ env.RELEASE_PRERELEASE_MODE }} (manually configured)"
fi
# 处理忽略模式(去除空格,保持管道分隔格式)
echo "🔧 处理忽略模式..."
PROCESSED_PATTERNS=""
IFS='|' read -ra PATTERNS_ARRAY <<< "${{ env.IGNORE_PATTERNS }}"
for pattern in "${PATTERNS_ARRAY[@]}"; do
# 使用 sed 去除首尾空格,保留转义字符
pattern=$(echo "$pattern" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
if [ -n "$pattern" ]; then
if [ -z "$PROCESSED_PATTERNS" ]; then
PROCESSED_PATTERNS="$pattern"
else
PROCESSED_PATTERNS="$PROCESSED_PATTERNS|$pattern"
fi
fi
done
echo "✅ 已处理 $(echo "$PROCESSED_PATTERNS" | grep -o '|' | wc -l | awk '{print $1+1}') 个忽略模式"
# 导出处理后的配置到 GitHub 环境变量
echo "CHANGELOG_VERSION=$CHANGELOG_VERSION" >> $GITHUB_ENV
echo "RELEASE_TITLE_PROCESSED=$RELEASE_TITLE_PROCESSED" >> $GITHUB_ENV
echo "RELEASE_IS_PRERELEASE=$RELEASE_IS_PRERELEASE" >> $GITHUB_ENV
echo "COMMIT_MSG=$COMMIT_MSG" >> $GITHUB_ENV
echo "PRERELEASE_STRIP_REGEX=$STRIP_REGEX" >> $GITHUB_ENV
echo "PRERELEASE_MATCH_REGEX=$MATCH_REGEX" >> $GITHUB_ENV
echo "IGNORE_PATTERNS_PROCESSED=$PROCESSED_PATTERNS" >> $GITHUB_ENV
# 显示配置摘要
echo ""
echo "📋 Configuration Summary:"
echo " 🏷️ Tag version: $VERSION"
echo " 📝 CHANGELOG version: $CHANGELOG_VERSION"
echo " 🌐 Gitea Server: ${{ env.GITEA_SERVER }}"
echo " 📄 CHANGELOG Title: ${{ env.CHANGELOG_SECTION_TITLE }}"
echo " 🚀 Release Title: $RELEASE_TITLE_PROCESSED"
echo " 🏷️ Pre-release: $PRERELEASE_DETECTED"
echo " 📄 Draft: ${{ env.RELEASE_IS_DRAFT }}"
echo " 💬 Commit Message: $COMMIT_MSG"
echo " 📎 Additional Files: ${ADDITIONAL_RELEASE_FILES:-none}"
echo ""
echo "✅ Configuration loaded successfully"
echo "======================================"
echo ""
- name: 📥 克隆仓库
id: clone
run: |
echo "======================================"
echo "🚀 开始准备仓库"
echo "======================================"
REPO_NAME="${{ github.event.repository.name }}"
REPO_DIR="${{ env.WORKSPACE_DIR }}/$REPO_NAME"
echo "📁 仓库名称: $REPO_NAME"
echo "📍 目标目录: $REPO_DIR"
echo "🌐 服务器: ${GITHUB_SERVER_URL}"
echo ""
# 检查仓库状态
if [ -d "$REPO_DIR" ]; then
echo "📂 目录已存在,检查 Git 仓库状态..."
if [ -d "$REPO_DIR/.git" ]; then
# 目录存在且是有效的 Git 仓库
echo "✓ 发现有效的 Git 仓库,执行增量更新..."
cd "$REPO_DIR"
# 清理工作区
git clean -fdx
git reset --hard
# 获取最新代码和标签,同时清理远程已删除的 tag
echo "📥 拉取最新代码和标签..."
git fetch --all --tags --force --prune --prune-tags
echo "✓ 已同步远程状态(包括已删除的 tag)"
echo "✓ 仓库已更新"
else
# 目录存在但不是 Git 仓库(可能之前运行失败)
echo "⚠️ 目录存在但不是有效的 Git 仓库"
echo "🧹 清理损坏的目录..."
rm -rf "$REPO_DIR"
echo "✓ 已清理"
# 重新克隆
echo "📥 克隆仓库..."
mkdir -p "${{ env.WORKSPACE_DIR }}"
git clone \
https://oauth2:${{ env.ACCESS_TOKEN }}@${GITHUB_SERVER_URL#https://}/${{ github.repository }}.git \
"$REPO_DIR"
if [ $? -ne 0 ]; then
echo "❌ 克隆失败"
cat /tmp/git_clone.log
# 清理残留
if [ -d "$REPO_DIR" ]; then
rm -rf "$REPO_DIR"
fi
exit 1
fi
cd "$REPO_DIR"
echo "✓ 仓库已克隆"
fi
else
# 目录不存在,首次克隆
echo "📥 克隆仓库(首次)..."
mkdir -p "${{ env.WORKSPACE_DIR }}"
git clone \
https://oauth2:${{ env.ACCESS_TOKEN }}@${GITHUB_SERVER_URL#https://}/${{ github.repository }}.git \
"$REPO_DIR"
if [ $? -eq 0 ]; then
cd "$REPO_DIR"
echo "✓ 仓库已克隆"
else
echo "❌ 克隆失败"
exit 1
fi
fi
echo ""
echo "🔍 验证主分支配置..."
# 检查是否配置了主分支
if [ -z "${{ env.MAIN_BRANCH }}" ]; then
echo "❌ 错误: 未配置主分支"
echo ""
echo "请在 workflow 配置文件的 env 区域设置 MAIN_BRANCH:"
echo ""
echo "env:"
echo " MAIN_BRANCH: \"main\" # 或 \"master\""
echo ""
exit 1
fi
MAIN_BRANCH="${{ env.MAIN_BRANCH }}"
echo "✓ 使用配置的主分支: $MAIN_BRANCH"
# 验证分支是否存在
if ! git show-ref --verify --quiet refs/remotes/origin/$MAIN_BRANCH; then
echo "❌ 错误: 配置的分支 '$MAIN_BRANCH' 不存在"
echo ""
echo "可用的远程分支:"
git branch -r | grep -v HEAD
echo ""
echo "请在 workflow 配置文件中修改 MAIN_BRANCH 为正确的分支名"
exit 1
fi
echo "✓ 分支 '$MAIN_BRANCH' 已验证存在"
# 切换到主分支
echo ""
echo "🌿 切换到主分支: $MAIN_BRANCH"
git checkout $MAIN_BRANCH
git pull origin $MAIN_BRANCH
echo "✓ 已切换到主分支并更新到最新"
# 导出到环境变量供后续步骤使用
echo "REPO_DIR=$REPO_DIR" >> $GITHUB_ENV
echo "REPO_NAME=$REPO_NAME" >> $GITHUB_ENV
echo "MAIN_BRANCH=$MAIN_BRANCH" >> $GITHUB_ENV
echo ""
echo "✅ 仓库准备完成"
echo "======================================"
echo ""
- name: 🔍 检查 Bot 提交
id: check_bot
run: |
echo "======================================"
echo "🤖 检查是否为 Bot 提交"
echo "======================================"
cd ${{ env.REPO_DIR }}
COMMIT_MSG=$(git log -1 --pretty=%B ${{ github.ref_name }})
echo "📝 Tag commit message:"
echo "$COMMIT_MSG"
echo ""
# 检查是否包含 [skip ci] 或由 bot 创建
if echo "$COMMIT_MSG" | grep -qE '\[skip ci\]|\[ci skip\]'; then
echo "⏭️ Bot commit detected (contains [skip ci])"
echo "is_bot_commit=true" >> $GITHUB_OUTPUT
else
echo "✅ Not a bot commit"
echo "is_bot_commit=false" >> $GITHUB_OUTPUT
fi
echo "======================================"
echo ""
- name: 📝 生成 CHANGELOG
id: changelog
if: steps.check_bot.outputs.is_bot_commit != 'true'
run: |
echo "======================================"
echo "📝 生成 CHANGELOG"
echo "======================================"
cd ${{ env.REPO_DIR }}
VERSION="${{ github.ref_name }}"
CHANGELOG_VERSION="${{ env.CHANGELOG_VERSION }}"
echo "🏷️ Tag: $VERSION"
echo "📝 CHANGELOG Version: $CHANGELOG_VERSION"
echo ""
# 查找前一个 tag
# 排除所有属于当前 CHANGELOG 版本的 tag(例如 0.0.2-rc1, 0.0.2-rc2 都属于 0.0.2)
echo "🔍 查找前一个版本的 tag..."
# 获取所有 tag
ALL_TAGS=$(git tag --sort=-version:refname)
# 遍历查找第一个不属于当前 CHANGELOG 版本的 tag
PREVIOUS_TAG=""
while IFS= read -r tag; do
# 跳过空行
[ -z "$tag" ] && continue
# 对每个 tag 执行 strip 操作(去除 pre-release 后缀)
if [[ "${{ env.CHANGELOG_VERSION_MODE }}" == "strip" ]]; then
tag_stripped=$(echo "$tag" | sed -E "s/${{ env.PRERELEASE_STRIP_REGEX }}//")
else
tag_stripped="$tag"
fi
# 如果这个 tag 的 CHANGELOG 版本不等于当前版本,就是我们要找的
if [ "$tag_stripped" != "$CHANGELOG_VERSION" ]; then
PREVIOUS_TAG="$tag"
break
fi
done <<< "$ALL_TAGS"
if [ -z "$PREVIOUS_TAG" ]; then
echo "ℹ️ 未找到前一个tag,将使用所有提交"
COMMIT_RANGE="HEAD"
else
echo "📍 前一个tag: $PREVIOUS_TAG"
# 对 previous tag 也执行 strip,显示对应的 CHANGELOG 版本
if [[ "${{ env.CHANGELOG_VERSION_MODE }}" == "strip" ]]; then
PREV_CHANGELOG_VERSION=$(echo "$PREVIOUS_TAG" | sed -E "s/${{ env.PRERELEASE_STRIP_REGEX }}//")
echo " (对应 CHANGELOG 版本: $PREV_CHANGELOG_VERSION)"
fi
COMMIT_RANGE="${PREVIOUS_TAG}..${VERSION}"
fi
echo ""
echo "🔍 扫描提交记录..."
TEMP_COMMITS=$(mktemp)
git log $COMMIT_RANGE --no-merges --reverse --format="COMMIT_START%n%H%n%an%n%s%nBODY_START%n%b%nBODY_END" > "$TEMP_COMMITS"
declare -a COMMITS_HASH
declare -a COMMITS_AUTHOR
declare -a COMMITS_MESSAGE
declare -a COMMITS_BODY
# 从环境变量加载忽略模式 (转换为 bash 数组)
IFS='|' read -ra IGNORE_PATTERNS <<< "${{ env.IGNORE_PATTERNS_PROCESSED }}"
echo "📋 已加载 ${#IGNORE_PATTERNS[@]} 个忽略模式"
COMMIT_INDEX=0
CURRENT_HASH=""
CURRENT_AUTHOR=""
CURRENT_MESSAGE=""
CURRENT_BODY=""
IN_BODY=false
SKIPPED_COUNT=0
while IFS= read -r line; do
if [[ "$line" == "COMMIT_START" ]]; then
# 处理前一个 commit
if [ -n "$CURRENT_HASH" ]; then
SHOULD_SKIP=false
for pattern in "${IGNORE_PATTERNS[@]}"; do
[ -z "$pattern" ] && continue
if echo "$CURRENT_MESSAGE" | grep -qiE "$pattern"; then
SHOULD_SKIP=true
echo " ⏭️ 跳过: ${CURRENT_MESSAGE:0:70}... (匹配: ${pattern})"
SKIPPED_COUNT=$((SKIPPED_COUNT + 1))
break
fi
done
if [ "$SHOULD_SKIP" = false ]; then
COMMITS_HASH[$COMMIT_INDEX]="$CURRENT_HASH"
COMMITS_AUTHOR[$COMMIT_INDEX]="$CURRENT_AUTHOR"
COMMITS_MESSAGE[$COMMIT_INDEX]="$CURRENT_MESSAGE"
COMMITS_BODY[$COMMIT_INDEX]="$CURRENT_BODY"
COMMIT_INDEX=$((COMMIT_INDEX + 1))
fi
fi
# 重置状态
CURRENT_HASH=""
CURRENT_AUTHOR=""
CURRENT_MESSAGE=""
CURRENT_BODY=""
IN_BODY=false
STATE="HASH"
elif [[ "$line" == "BODY_START" ]]; then
IN_BODY=true
CURRENT_BODY=""
elif [[ "$line" == "BODY_END" ]]; then
IN_BODY=false
elif [ "$IN_BODY" = true ]; then
if [ -n "$CURRENT_BODY" ]; then
CURRENT_BODY="${CURRENT_BODY}"$'\n'"${line}"
else
CURRENT_BODY="$line"
fi
else
if [ -z "$CURRENT_HASH" ]; then
CURRENT_HASH="$line"
elif [ -z "$CURRENT_AUTHOR" ]; then
CURRENT_AUTHOR="$line"
elif [ -z "$CURRENT_MESSAGE" ]; then
CURRENT_MESSAGE="$line"
fi
fi
done < "$TEMP_COMMITS"
# 处理最后一个 commit
if [ -n "$CURRENT_HASH" ]; then
SHOULD_SKIP=false
for pattern in "${IGNORE_PATTERNS[@]}"; do
[ -z "$pattern" ] && continue
if echo "$CURRENT_MESSAGE" | grep -qiE "$pattern"; then
SHOULD_SKIP=true
echo " ⏭️ 跳过: ${CURRENT_MESSAGE:0:70}... (匹配: ${pattern})"
SKIPPED_COUNT=$((SKIPPED_COUNT + 1))
break
fi
done
if [ "$SHOULD_SKIP" = false ]; then
COMMITS_HASH[$COMMIT_INDEX]="$CURRENT_HASH"
COMMITS_AUTHOR[$COMMIT_INDEX]="$CURRENT_AUTHOR"
COMMITS_MESSAGE[$COMMIT_INDEX]="$CURRENT_MESSAGE"
COMMITS_BODY[$COMMIT_INDEX]="$CURRENT_BODY"
COMMIT_INDEX=$((COMMIT_INDEX + 1))
fi
fi
rm -f "$TEMP_COMMITS"
VALID_COUNT=$COMMIT_INDEX
echo ""
echo "📊 提交统计:"
echo " ✅ 有效提交: $VALID_COUNT"
echo " ⏭️ 已跳过: $SKIPPED_COUNT"
echo " 📝 总计: $((VALID_COUNT + SKIPPED_COUNT))"
echo ""
if [ $VALID_COUNT -eq 0 ]; then
echo "⚠️ 未找到有效的提交"
echo "changelog_updated=false" >> $GITHUB_OUTPUT
echo "content_changed=false" >> $GITHUB_OUTPUT
exit 0
fi
# ============================================
# 构建 CHANGELOG 条目
# ============================================
echo "📄 构建 CHANGELOG 条目..."
REPO_URL="${{ github.server_url }}/${{ github.repository }}"
# 初始化条目内容
NEW_ENTRY="## :bookmark: ${CHANGELOG_VERSION}\n\n"
NEW_ENTRY="${NEW_ENTRY}${CHANGELOG_SECTION_TITLE}\n\n"
# 处理每个提交
for ((i=0; i<$VALID_COUNT; i++)); do
hash="${COMMITS_HASH[$i]}"
author="${COMMITS_AUTHOR[$i]}"
message="${COMMITS_MESSAGE[$i]}"
body="${COMMITS_BODY[$i]}"
echo " [$((i+1))/$VALID_COUNT] ${hash:0:7}: ${message:0:60}..."
# 构建提交链接 - 使用短 hash 格式
short_hash="${hash:0:7}"
COMMIT_LINK="([${short_hash}](${REPO_URL}/commit/${hash}))"
# 构建作者信息
AUTHOR_INFO=""
if [ -n "$author" ]; then
AUTHOR_INFO=" by @${author}"
fi
# 处理 body (如果存在)
if [ -n "$body" ]; then
# 移除首尾空行
cleaned_body=$(echo "$body" | sed -e :a -e '/^\s*$/d;')
if [ -n "$cleaned_body" ]; then
# 有 body: 主题在第一行,body 内容缩进显示,链接和作者放在一起
NEW_ENTRY="${NEW_ENTRY}- ${message} \n"
# 将 body 按行添加,每行缩进两个空格,使用 Markdown 换行
while IFS= read -r line; do
[ -z "$line" ] && continue
NEW_ENTRY="${NEW_ENTRY} ${line} \n"
done <<< "$cleaned_body"
# 链接和作者放在缩进行
NEW_ENTRY="${NEW_ENTRY} ${COMMIT_LINK}${AUTHOR_INFO}\n"
else
# body 为空 (只有空白): 单行格式
NEW_ENTRY="${NEW_ENTRY}- ${message} ${COMMIT_LINK}${AUTHOR_INFO}\n"
fi
else
# 没有 body: 单行格式
NEW_ENTRY="${NEW_ENTRY}- ${message} ${COMMIT_LINK}${AUTHOR_INFO}\n"
fi
done
echo ""
echo "👥 收集贡献者..."
# 收集贡献者 (去除 bot)
if [ "$COMMIT_RANGE" = "HEAD" ]; then
CONTRIBUTORS=$(git log --pretty=format:"%an" --no-merges | \
grep -v "github-actions\[bot\]" | \
grep -v "dependabot\[bot\]" | \
grep -v "renovate\[bot\]" | \
sort -u)
else
CONTRIBUTORS=$(git log $COMMIT_RANGE --pretty=format:"%an" --no-merges | \
grep -v "github-actions\[bot\]" | \
grep -v "dependabot\[bot\]" | \
grep -v "renovate\[bot\]" | \
sort -u)
fi
if [ -n "$CONTRIBUTORS" ]; then
CONTRIBUTOR_COUNT=$(echo "$CONTRIBUTORS" | wc -l)
echo "✓ 找到 ${CONTRIBUTOR_COUNT} 个贡献者"
NEW_ENTRY="${NEW_ENTRY}\n${{ env.CHANGELOG_CONTRIBUTORS_TITLE }}\n\n"
while IFS= read -r name; do
[ -z "$name" ] && continue
NEW_ENTRY="${NEW_ENTRY}\n"
NEW_ENTRY="${NEW_ENTRY} \n"
NEW_ENTRY="${NEW_ENTRY}\n"
done <<< "$CONTRIBUTORS"
fi
NEW_ENTRY="${NEW_ENTRY}\n---\n"
echo ""
echo "💾 写入 CHANGELOG.md..."
# 切换到主分支
git checkout ${{ env.MAIN_BRANCH }}
# ============================================
# 更新或创建 CHANGELOG.md
# ============================================
if [ -f CHANGELOG.md ]; then
# CHANGELOG 文件已存在
if grep -q "^## :bookmark: ${CHANGELOG_VERSION}$" CHANGELOG.md; then
# 版本已存在 - 内容叠加模式
echo "🔄 版本 ${CHANGELOG_VERSION} 已存在,执行内容叠加..."
# 提取现有版本区域中的 commit hashes (用于去重)
EXISTING_HASHES=$(awk -v version="${CHANGELOG_VERSION}" '
BEGIN { in_version=0 }
/^## :bookmark: / && $0 ~ version"$" { in_version=1; next }
/^## :bookmark: / && in_version { in_version=0 }
in_version && /\(\[([0-9a-f]{7})\]/ {
match($0, /\(\[([0-9a-f]{7})\]/, arr)
print arr[1]
}
' CHANGELOG.md | sort -u)
# 找出真正的新 commits (基于 hash 去重)
NEW_COMMIT_ENTRIES=""
NEW_COMMIT_COUNT=0
for ((i=0; i<$VALID_COUNT; i++)); do
hash="${COMMITS_HASH[$i]}"
short_hash="${hash:0:7}"
# 检查这个 commit 是否已存在
if echo "$EXISTING_HASHES" | grep -q "^${short_hash}$"; then
echo " ℹ️ 跳过已存在: ${short_hash} ${COMMITS_MESSAGE[$i]:0:50}..."
continue
fi
# 这是一个新的 commit,添加它
author="${COMMITS_AUTHOR[$i]}"
message="${COMMITS_MESSAGE[$i]}"
body="${COMMITS_BODY[$i]}"
COMMIT_LINK="([${short_hash}](${REPO_URL}/commit/${hash}))"
# 构建作者信息
AUTHOR_INFO=""
if [ -n "$author" ]; then
AUTHOR_INFO=" by @${author}"
fi
if [ -n "$body" ]; then
cleaned_body=$(echo "$body" | sed -e :a -e '/^\s*$/d;')
if [ -n "$cleaned_body" ]; then
NEW_COMMIT_ENTRIES="${NEW_COMMIT_ENTRIES}- ${message} \n"
while IFS= read -r line; do
[ -z "$line" ] && continue
NEW_COMMIT_ENTRIES="${NEW_COMMIT_ENTRIES} ${line} \n"
done <<< "$cleaned_body"
NEW_COMMIT_ENTRIES="${NEW_COMMIT_ENTRIES} ${COMMIT_LINK}${AUTHOR_INFO}\n"
else
NEW_COMMIT_ENTRIES="${NEW_COMMIT_ENTRIES}- ${message} ${COMMIT_LINK}${AUTHOR_INFO}\n"
fi
else
NEW_COMMIT_ENTRIES="${NEW_COMMIT_ENTRIES}- ${message} ${COMMIT_LINK}${AUTHOR_INFO}\n"
fi
NEW_COMMIT_COUNT=$((NEW_COMMIT_COUNT + 1))
done
if [ $NEW_COMMIT_COUNT -gt 0 ]; then
echo "✓ 发现 ${NEW_COMMIT_COUNT} 个新的commits需要添加"
# 在 Contributors 部分之前插入新的 commits
TEMP_FILE=$(mktemp)
IN_VERSION=false
ADDED=false
while IFS= read -r line; do
# 检测版本标题
if [[ "$line" =~ ^##[[:space:]]:bookmark:[[:space:]]${CHANGELOG_VERSION}$ ]]; then
echo "$line" >> "$TEMP_FILE"
IN_VERSION=true
# 检测到 Contributors 标题,在它之前插入新 commits
elif [[ "$line" =~ ^###[[:space:]]:busts_in_silhouette: ]] && [ "$IN_VERSION" = true ] && [ "$ADDED" = false ]; then
echo "" >> "$TEMP_FILE"
echo -e "${NEW_COMMIT_ENTRIES}" >> "$TEMP_FILE"
echo "$line" >> "$TEMP_FILE"
ADDED=true
# 遇到下一个版本标题
elif [[ "$line" =~ ^##[[:space:]]:bookmark: ]] && [ "$IN_VERSION" = true ]; then
# 如果还没添加(可能没有 Contributors 部分)
if [ "$ADDED" = false ]; then
echo "" >> "$TEMP_FILE"
echo -e "${NEW_COMMIT_ENTRIES}" >> "$TEMP_FILE"
ADDED=true
fi
echo "$line" >> "$TEMP_FILE"
IN_VERSION=false
else
echo "$line" >> "$TEMP_FILE"
fi
done < CHANGELOG.md
# 如果到文件末尾还没添加(版本在最后且没有 Contributors)
if [ "$IN_VERSION" = true ] && [ "$ADDED" = false ]; then
echo "" >> "$TEMP_FILE"
echo -e "${NEW_COMMIT_ENTRIES}" >> "$TEMP_FILE"
fi
mv "$TEMP_FILE" CHANGELOG.md
echo "✅ 已追加 ${NEW_COMMIT_COUNT} 个新commits到现有版本"
echo "changelog_updated=true" >> $GITHUB_OUTPUT
echo "content_changed=true" >> $GITHUB_OUTPUT
else
echo "ℹ️ 所有commits都已存在于CHANGELOG中"
echo " (通常发生在重建已删除的 tag)"
echo " 将从现有内容创建 Release"
echo "changelog_updated=true" >> $GITHUB_OUTPUT
echo "content_changed=false" >> $GITHUB_OUTPUT
fi
else
# 版本不存在 - 添加新版本条目
echo "➕ 添加新版本条目: ${CHANGELOG_VERSION}"
TEMP_FILE=$(mktemp)
# 读取第一行 (CHANGELOG 标题)
head -n 1 CHANGELOG.md > "$TEMP_FILE"
echo "" >> "$TEMP_FILE"
# 插入新条目
echo -e "${NEW_ENTRY}" >> "$TEMP_FILE"
# 追加剩余内容 (从第3行开始,跳过标题后的空行)
tail -n +3 CHANGELOG.md >> "$TEMP_FILE"
mv "$TEMP_FILE" CHANGELOG.md
echo "✅ 已添加新的CHANGELOG条目"
echo "changelog_updated=true" >> $GITHUB_OUTPUT
echo "content_changed=true" >> $GITHUB_OUTPUT
fi
else
# CHANGELOG 文件不存在 - 创建新文件
echo "📄 创建新的 CHANGELOG.md"
echo "# :memo: CHANGELOG" > CHANGELOG.md
echo "" >> CHANGELOG.md
echo -e "${NEW_ENTRY}" >> CHANGELOG.md
echo "✅ 已创建新的CHANGELOG.md"
echo "changelog_updated=true" >> $GITHUB_OUTPUT
echo "content_changed=true" >> $GITHUB_OUTPUT
fi
echo ""
echo "✅ CHANGELOG 生成完成"
echo "======================================"
echo ""
- name: 📤 提交 CHANGELOG 到主分支
id: commit
if: steps.changelog.outputs.content_changed == 'true'
run: |
echo "======================================"
echo "📤 提交 CHANGELOG 更新"
echo "======================================"
cd ${{ env.REPO_DIR }}
# 配置 Git 用户信息
git config user.name "${{ env.GIT_USER_NAME }}"
git config user.email "${{ env.GIT_USER_EMAIL }}"
# 添加 CHANGELOG.md
git add CHANGELOG.md
# 检查是否有变更
if git diff --staged --quiet; then
echo "📌 没有变更,跳过提交"
else
echo "📝 提交 CHANGELOG 更新..."
git commit -m "${{ env.COMMIT_MSG }}"
echo "📤 推送到远程 ${{ env.MAIN_BRANCH }} 分支..."
git push https://oauth2:${{ env.ACCESS_TOKEN }}@${GITHUB_SERVER_URL#https://}/${{ github.repository }}.git ${{ env.MAIN_BRANCH }}
if [ $? -eq 0 ]; then
echo "✅ 推送成功"
else
echo "❌ 推送失败"
exit 1
fi
fi
echo "======================================"
echo ""
- name: 📄 提取 Release Notes
id: extract_notes
if: steps.changelog.outputs.changelog_updated == 'true'
run: |
echo "======================================"
echo "📄 提取 Release Notes"
echo "======================================"
cd ${{ env.REPO_DIR }}
# 确保使用最新的 CHANGELOG
git pull origin ${{ env.MAIN_BRANCH }}
python3 << 'PYSCRIPT'
import re
changelog_version = "${{ env.CHANGELOG_VERSION }}"
with open('CHANGELOG.md', 'r', encoding='utf-8') as f:
content = f.read()
# 匹配版本区块
pattern = rf'## :bookmark: {re.escape(changelog_version)}\s*\n(.*?)(?=\n---\n|\n## :bookmark: |\Z)'
match = re.search(pattern, content, re.DOTALL)
if match:
version_content = match.group(1).strip()
# 清理多余的空行
version_content = re.sub(r'\n\s*\n\s*\n+', '\n\n', version_content).strip()
# 移除可能的分隔线
version_content = re.sub(r'^-{3,}\s*\n', '', version_content)
print(f"✓ Found changelog for version {changelog_version}")
print(f"📊 Content length: {len(version_content)} characters")
else:
version_content = "⚠️ No changelog found for this version"
print(f"⚠️ WARNING: Version {changelog_version} not found in CHANGELOG")
with open('/tmp/release-body.txt', 'w', encoding='utf-8') as f:
f.write(version_content)
print("✅ Release notes generated successfully")
PYSCRIPT
echo "======================================"
echo ""
- name: 🚀 创建 Release
id: create_release
if: steps.changelog.outputs.changelog_updated == 'true'
run: |
echo "======================================"
echo "🚀 创建 GitHub/Gitea Release"
echo "======================================"
cd ${{ env.REPO_DIR }}
echo "📦 准备 Release 数据..."
echo "📋 Release 标题: ${{ env.RELEASE_TITLE_PROCESSED }}"
echo "🏷️ Pre-release: ${{ env.RELEASE_IS_PRERELEASE }}"
echo "📄 Draft: ${{ env.RELEASE_IS_DRAFT }}"
echo ""
# 转换 bash 布尔值为 Python 布尔值
if [ "${{ env.RELEASE_IS_DRAFT }}" = "true" ]; then
PY_DRAFT="True"
else
PY_DRAFT="False"
fi
if [ "${{ env.RELEASE_IS_PRERELEASE }}" = "true" ]; then
PY_PRERELEASE="True"
else
PY_PRERELEASE="False"
fi
echo "🔄 转换布尔值: draft=${{ env.RELEASE_IS_DRAFT }} → $PY_DRAFT, prerelease=${{ env.RELEASE_IS_PRERELEASE }} → $PY_PRERELEASE"
echo ""
python3 << EOF
import json
with open('/tmp/release-body.txt', 'r') as f:
body = f.read()
payload = {
"tag_name": "${{ github.ref_name }}",
"name": "${{ env.RELEASE_TITLE_PROCESSED }}",
"body": body,
"draft": $PY_DRAFT,
"prerelease": $PY_PRERELEASE
}
with open('/tmp/payload.json', 'w', encoding='utf-8') as f:
json.dump(payload, f, ensure_ascii=False, indent=2)
print("✓ Payload prepared")
EOF
echo "🌐 发送 API 请求..."
HTTP_CODE=$(curl -s -o /tmp/release_response.json -w "%{http_code}" -X POST \
-H "Authorization: token ${{ env.ACCESS_TOKEN }}" \
-H "Content-Type: application/json" \
-d @/tmp/payload.json \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases")
echo ""
echo "📡 API 响应 (HTTP $HTTP_CODE):"
echo "---"
cat /tmp/release_response.json | python3 -m json.tool 2>/dev/null || cat /tmp/release_response.json
echo "---"
echo ""
if [ "$HTTP_CODE" != "201" ] && [ "$HTTP_CODE" != "200" ]; then
echo "❌ 错误: 创建 Release 失败"
exit 1
fi
echo "✅ Release 创建成功"
echo "======================================"
echo ""
- name: 📎 上传附件
id: upload_assets
if: steps.changelog.outputs.changelog_updated == 'true'
run: |
cd ${{ env.REPO_DIR }}
echo "🔍 提取 Release ID..."
RELEASE_ID=$(python3 << 'PYSCRIPT'
import json
import sys
try:
with open('/tmp/release_response.json', 'r') as f:
data = json.load(f)
if 'id' not in data:
print("❌ 错误: 响应中没有 'id' 字段", file=sys.stderr)
sys.exit(1)
print(data['id'])
except Exception as e:
print(f"❌ 错误: {e}", file=sys.stderr)
sys.exit(1)
PYSCRIPT
)
if [ -z "$RELEASE_ID" ]; then
echo "❌ 错误: 无法获取 Release ID"
exit 1
fi
echo "✓ Release ID: $RELEASE_ID"
echo ""
# 上传额外的文件
if [ -n "${{ env.ADDITIONAL_RELEASE_FILES }}" ]; then
echo ""
echo "📦 上传额外文件..."
FILE_INDEX=2
TOTAL_FILES=$((1 + $(echo "${{ env.ADDITIONAL_RELEASE_FILES }}" | wc -w)))
for file in ${{ env.ADDITIONAL_RELEASE_FILES }}; do
if [ -f "$file" ]; then
echo "📤 [$FILE_INDEX/$TOTAL_FILES] 上传 $(basename $file)..."
HTTP_CODE=$(curl -s -o /tmp/upload_response_${FILE_INDEX}.json -w "%{http_code}" -X POST \
-H "Authorization: token ${{ env.ACCESS_TOKEN }}" \
-F "attachment=@${file}" \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases/$RELEASE_ID/assets?name=$(basename $file)")
if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "200" ]; then
echo "✅ $(basename $file) 上传成功"
else
echo "⚠️ 警告: $(basename $file) 上传失败 (HTTP $HTTP_CODE)"
fi
FILE_INDEX=$((FILE_INDEX + 1))
else
echo "⚠️ 警告: 文件不存在: $file"
fi
done
fi
echo ""
echo "✅ 所有附件处理完成"
echo "======================================"
echo ""
- name: 📊 生成 Workflow Summary
if: always()
run: |
echo "======================================"
echo "📊 生成 Workflow Summary"
echo "======================================"
cat >> $GITHUB_STEP_SUMMARY << 'EOFSUMMARY'
# 📦 Tag Release 工作流执行报告
## ✅ 执行结果
| 项目 | 结果 |
|------|------|
| 🏷️ Tag 版本 | **`${{ github.ref_name }}`** |
| 📝 CHANGELOG 版本 | **`${{ env.CHANGELOG_VERSION }}`** |
| 🌿 主分支 | **`${{ env.MAIN_BRANCH }}`** |
| 🌐 Gitea 服务器 | **`${{ env.GITEA_SERVER }}`** |
EOFSUMMARY
if [ "${{ steps.check_bot.outputs.is_bot_commit }}" = "true" ]; then
cat >> $GITHUB_STEP_SUMMARY << 'EOFBOT'
| 📋 执行状态 | ⏭️ 已跳过 (Bot 提交) |
---
⏭️ **工作流已跳过**
检测到此提交由 Bot 创建(包含 `[skip ci]` 标记),为防止无限循环,已跳过执行。
EOFBOT
elif [ "${{ steps.check_version.outputs.version_exists }}" = "true" ]; then
cat >> $GITHUB_STEP_SUMMARY << 'EOFEXIST'
| 📋 执行状态 | ⏭️ 已跳过 (版本已存在) |
---
⏭️ **工作流已跳过**
版本 `${{ env.CHANGELOG_VERSION }}` 已存在于 CHANGELOG.md 中。
EOFEXIST
elif [ "${{ steps.changelog.outputs.changelog_updated }}" = "true" ]; then
if [ "${{ steps.changelog.outputs.content_changed }}" = "true" ]; then
cat >> $GITHUB_STEP_SUMMARY << 'EOFSUCCESS'
| 📋 执行状态 | ✅ 执行成功 |
---
### ✅ 完成的操作
- ✅ CHANGELOG.md 已更新
- ✅ 更改已推送到主分支 (`${{ env.MAIN_BRANCH }}`)
- ✅ Release 已创建
- ✅ CHANGELOG.md 已作为附件上传
EOFSUCCESS
else
cat >> $GITHUB_STEP_SUMMARY << 'EOFREBUILD'
| 📋 执行状态 | ✅ 执行成功 (重建 Tag) |
---
### ✅ 完成的操作
- ℹ️ CHANGELOG.md 无需更新(内容已存在)
- ✅ Release 已创建
- ✅ CHANGELOG.md 已作为附件上传
💡 **说明**: 检测到这是重建已删除的 tag,CHANGELOG 中已包含所有提交内容,因此直接从现有内容创建 Release。
EOFREBUILD
fi
if [ -n "${{ env.ADDITIONAL_RELEASE_FILES }}" ]; then
echo "- ✅ 额外文件已上传" >> $GITHUB_STEP_SUMMARY
fi
else
cat >> $GITHUB_STEP_SUMMARY << 'EOFNOCOMMIT'
| 📋 执行状态 | ℹ️ 无更新 |
---
ℹ️ **无新内容需要更新**
可能的原因:
- 在标签之间未找到有效的提交记录
- 或者版本 \`${{ env.CHANGELOG_VERSION }}\` 已包含所有相关提交(常见于重新创建已删除的 tag)
💡 如果你删除了 tag 后重新创建,CHANGELOG 中的内容已经存在,无需重复添加。
EOFNOCOMMIT
fi
cat >> $GITHUB_STEP_SUMMARY << 'EOFEND'
---
## 🔗 快速链接
- 📝 [查看 CHANGELOG](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/src/branch/${{ env.MAIN_BRANCH }}/CHANGELOG.md)
- 🚀 [查看 Releases](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/releases)
- 🔧 [查看 Workflow 配置](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/blob/${{ env.MAIN_BRANCH }}/.github/workflows/changelog_and_release.yml)
---