actions-template/.gitea/workflows/changelog_and_release.yml

1227 lines
46 KiB
YAML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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: "ci[bot]"
GIT_USER_EMAIL: "ci[bot]@tinysoft.com.cn"
# 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)
# 动态过滤配置的 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\]" | \
grep -v "^${{ env.GIT_USER_NAME }}$" | \
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\]" | \
grep -v "^${{ env.GIT_USER_NAME }}$" | \
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}<a href=\"${{ env.GITEA_SERVER }}/${name}\">\n"
NEW_ENTRY="${NEW_ENTRY} <img src=\"${{ env.GITEA_SERVER }}/${name}.png\" alt=\"${name}\" width=\"35\" height=\"35\" style=\"border-radius: 50%;\" onerror=\"this.src='${{ env.GITEA_SERVER }}/assets/img/avatar_default.png'\" />\n"
NEW_ENTRY="${NEW_ENTRY}</a>\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}\]/ {
if (match($0, /\[([0-9a-f]{7})\]/)) {
hash = substr($0, RSTART+1, RLENGTH-2)
print hash
}
}
' 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 已作为附件上传
💡 **说明**: 检测到这是重建已删除的 tagCHANGELOG 中已包含所有提交内容,因此直接从现有内容创建 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)
---
<div align="center">
*📊 由 [GitHub Actions](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions) 自动生成和更新*
EOFEND
echo "*🤖 生成时间: $(date -u '+%Y-%m-%d %H:%M:%S UTC')*" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "</div>" >> $GITHUB_STEP_SUMMARY
echo ""
echo "✅ Workflow summary 生成完成"
echo "======================================"
echo ""
- name: 💡 输出使用说明
if: success()
run: |
echo "======================================"
echo "✅ Tag Release 工作流执行完成!"
echo "======================================"
echo ""
if [ "${{ steps.check_bot.outputs.is_bot_commit }}" = "true" ]; then
echo "⏭️ 已跳过: Bot 提交检测"
echo " 原因: 检测到 [skip ci] 标记,防止无限循环"
elif [ "${{ steps.check_version.outputs.version_exists }}" = "true" ]; then
echo "⏭️ 已跳过: 版本已存在"
echo " 版本: ${{ env.CHANGELOG_VERSION }}"
elif [ "${{ steps.changelog.outputs.changelog_updated }}" = "true" ]; then
echo "📊 执行结果:"
echo " - Tag: ${{ github.ref_name }}"
echo " - CHANGELOG 版本: ${{ env.CHANGELOG_VERSION }}"
echo " - 主分支: ${{ env.MAIN_BRANCH }}"
echo " - Release 标题: ${{ env.RELEASE_TITLE_PROCESSED }}"
echo " - Pre-release: ${{ env.RELEASE_IS_PRERELEASE }}"
echo ""
if [ "${{ steps.changelog.outputs.content_changed }}" = "true" ]; then
echo "✅ 已完成的操作:"
echo " - CHANGELOG.md 已更新并推送到 ${{ env.MAIN_BRANCH }} 分支"
echo " - Release 已创建"
echo " - CHANGELOG.md 已上传为附件"
else
echo "✅ 已完成的操作:"
echo " - Release 已创建(从现有 CHANGELOG 内容)"
echo " - CHANGELOG.md 已上传为附件"
echo ""
echo "💡 说明: 检测到这是重建已删除的 tagCHANGELOG 中已包含"
echo " 所有提交内容,因此直接从现有内容创建 Release。"
fi
else
echo " 无新内容需要更新"
echo ""
echo "可能的原因:"
echo " - 在标签之间未找到有效的提交记录"
echo " - 或者版本 ${{ env.CHANGELOG_VERSION }} 已包含所有相关提交"
echo ""
echo "💡 提示: 如果你删除了 tag 后重新创建CHANGELOG 中的"
echo " 内容已经存在,这是正常现象,无需重复添加。"
fi
echo ""
echo "🔗 快速链接:"
echo " CHANGELOG: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/src/branch/${{ env.MAIN_BRANCH }}/CHANGELOG.md"
echo " Releases: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/releases"
echo ""
echo "======================================"
- name: 🧹 清理工作区
if: always()
run: |
echo "🧹 清理临时文件..."
rm -rf /tmp/commits.txt /tmp/changelog_updated.txt /tmp/content_changed.txt /tmp/release-body.txt /tmp/payload.json /tmp/release_response.json /tmp/upload_response_*.json
echo "✅ 清理完成"