Files
actions-template/.gitea/workflows/changelog_and_release.yml
T
csh 18c4dfa811 🔒 fix(ci): 修复 token 明文插值并加固脚本健壮性
- 安全: curl 的 Authorization 头改用 shell 变量 ${ACCESS_TOKEN},
  避免 secret 经 ${{ }} 明文展开进脚本文本(changelog、stats)
- 修复: stats 分支清理 find 显式排除 .git,与注释一致,
  不再依赖 linked worktree 中 .git 为文件的隐含前提
- 健壮性: echo -e 前用 esc() 转义用户 message/body 中的反斜杠,
  防止字面 \n \t 被误展开破坏 Markdown
- 健壮性: 千分位格式化改用不依赖 locale 的 sed 实现,
  替换 printf "%'d"
- 健壮性: git log 的 range 变量补全引号
- 优化: prepare.yml 的 push 触发限定 main/master 分支与 tag,
  避免其它分支空跑
- 文档: route-release 标注 workflow_run.path 为 Gitea 特有字段
2026-07-21 11:13:52 +08:00

1388 lines
54 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
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:
workflow_run:
workflows: ["🧰 准备环境"]
types:
- completed
concurrency:
group: release-${{ github.repository }}
cancel-in-progress: false
# ==========================================
# 🔧 配置区域 - 根据你的项目修改
# ==========================================
env:
# ===== Token 配置 =====
# Gitea/GitHub 访问令牌
# 用途:
# 1. Git 操作:推送提交(仓库读取由 Prepare 完成)
# 2. API 调用:创建 Release、上传附件
# 权限要求:
# - repo (仓库完整访问权限)
# - write:packages (可选,如需上传包)
# 配置位置:仓库 Settings > Secrets > Actions
# Secret 名称:WORKFLOW (或其他自定义名称)
#
# 💡 提示:同一个 token 可用于所有操作
# 如需分离权限,可配置两个不同的 token:
# - GIT_TOKEN: 用于 git push
# - API_TOKEN: 用于 API 调用
ACCESS_TOKEN: ${{ secrets.WORKFLOW }}
# ===== 工作区配置 =====
WORKSPACE_ROOT: "/data/workspace"
WORKSPACE_SLOT: "release"
# ===== 分支配置 =====
# 主分支名称(用于推送 CHANGELOG 更新)
# 必须明确配置主分支(例如 main 或 master
MAIN_BRANCH: "main"
# ===== 服务器配置 =====
# Gitea 服务器地址(用于生成头像链接和 API 调用)
GITEA_SERVER: "${{ github.server_url }}"
# ===== 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:
route-release:
# ⚠️ 依赖 Gitea 特有的 workflow_run.path 字段格式(形如 "<workflow>@refs/tags/<tag>"
# 该字段非 GitHub Actions 标准,若 Gitea 升级改变其格式,发布会静默不触发。
# 已在 Gitea 1.22 上验证;升级后请复核此判据,必要时增加 fallback。
if: ${{ github.event.workflow_run.conclusion == 'success' && contains(github.event.workflow_run.path, '@refs/tags/') }}
runs-on: standard-ubuntu-22
permissions:
contents: read
outputs:
should_release: ${{ steps.route.outputs.should_release }}
version: ${{ steps.route.outputs.version }}
tag_ref: ${{ steps.route.outputs.tag_ref }}
source_sha: ${{ steps.route.outputs.source_sha }}
env:
UPSTREAM_PATH: ${{ github.event.workflow_run.path }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
steps:
- name: 🧭 恢复并验证 Tag
id: route
shell: bash
run: |
set -euo pipefail
if ! command -v git >/dev/null 2>&1; then
echo "Prepare 成功后仍缺少发布路由所需命令: git" >&2
exit 1
fi
echo "should_release=false" >> "$GITHUB_OUTPUT"
case "$HEAD_SHA" in
''|*[!0-9a-fA-F]*)
echo "无效的上游提交 SHA: $HEAD_SHA" >&2
exit 1
;;
esac
FULL_REF="${UPSTREAM_PATH##*@}"
case "$FULL_REF" in
refs/tags/*) ;;
*)
echo "workflow_run.path 未包含有效 Tag ref: $UPSTREAM_PATH" >&2
exit 1
;;
esac
VERSION="${FULL_REF#refs/tags/}"
case "$VERSION" in
[0-9]*) ;;
*)
echo "跳过非版本 Tag: $VERSION"
exit 0
;;
esac
if ! git check-ref-format "$FULL_REF"; then
echo "数字开头的 Tag ref 格式无效: $FULL_REF" >&2
exit 1
fi
REPO_NAME="${{ github.event.repository.name }}"
REPOSITORY_DIR="${WORKSPACE_ROOT}/${REPO_NAME}/repository.git"
if [ ! -d "$REPOSITORY_DIR" ] || \
[ "$(git --git-dir="$REPOSITORY_DIR" rev-parse --is-bare-repository 2>/dev/null)" != "true" ]; then
echo "Prepare 未创建有效共享 bare 仓库: $REPOSITORY_DIR" >&2
exit 1
fi
if ! git --git-dir="$REPOSITORY_DIR" cat-file -e "${HEAD_SHA}^{commit}"; then
echo "共享仓库中不存在上游提交: $HEAD_SHA" >&2
exit 1
fi
if ! tag_sha=$(git --git-dir="$REPOSITORY_DIR" rev-parse "${FULL_REF}^{commit}" 2>/dev/null); then
echo "共享仓库中不存在可解析的 Tag: $FULL_REF" >&2
exit 1
fi
if [ "$tag_sha" != "$HEAD_SHA" ]; then
echo "Tag/SHA 不匹配: $FULL_REF -> $tag_sha,上游为 $HEAD_SHA" >&2
exit 1
fi
echo "should_release=true" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag_ref=$FULL_REF" >> "$GITHUB_OUTPUT"
echo "source_sha=$HEAD_SHA" >> "$GITHUB_OUTPUT"
echo "发布路由通过: $VERSION @ $HEAD_SHA"
changelog-and-release:
needs: route-release
if: ${{ needs.route-release.outputs.should_release == 'true' }}
runs-on: standard-ubuntu-22
permissions:
contents: write
env:
VERSION: ${{ needs.route-release.outputs.version }}
TAG_REF: ${{ needs.route-release.outputs.tag_ref }}
SOURCE_SHA: ${{ needs.route-release.outputs.source_sha }}
steps:
- name: ⚙️ 加载配置
id: config
run: |
echo "======================================"
echo "⚙️ 加载工作流配置"
echo "======================================"
# 从环境变量读取配置
VERSION="${{ env.VERSION }}"
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: 📥 对齐发布 Worktree
id: prepare_worktree
shell: bash
run: |
set -euo pipefail
for command in git python3 curl flock realpath; do
if ! command -v "$command" >/dev/null 2>&1; then
echo "Prepare 成功后仍缺少发布所需命令: $command" >&2
exit 1
fi
done
REPO_NAME="${{ github.event.repository.name }}"
HEAD_SHA="${{ env.SOURCE_SHA }}"
MAIN_BRANCH="${{ env.MAIN_BRANCH }}"
REPOSITORY_DIR="${WORKSPACE_ROOT}/${REPO_NAME}/repository.git"
WORKTREE_DIR="${WORKSPACE_ROOT}/${REPO_NAME}/worktrees/release"
WORKTREE_LOCK="${WORKSPACE_ROOT}/${REPO_NAME}/worktree-admin.lock"
MAIN_REMOTE_REF="refs/remotes/origin/${MAIN_BRANCH}"
case "$HEAD_SHA" in
''|*[!0-9a-fA-F]*)
echo "无效的路由提交 SHA: $HEAD_SHA" >&2
exit 1
;;
esac
if [ -z "$MAIN_BRANCH" ]; then
echo "未配置 MAIN_BRANCH" >&2
exit 1
fi
if [ ! -d "$REPOSITORY_DIR" ] || \
[ "$(git --git-dir="$REPOSITORY_DIR" rev-parse --is-bare-repository 2>/dev/null)" != "true" ]; then
echo "Prepare 未创建有效共享 bare 仓库: $REPOSITORY_DIR" >&2
exit 1
fi
if ! git --git-dir="$REPOSITORY_DIR" cat-file -e "${HEAD_SHA}^{commit}"; then
echo "共享仓库中不存在发布提交: $HEAD_SHA" >&2
exit 1
fi
if ! git --git-dir="$REPOSITORY_DIR" show-ref --verify --quiet "$MAIN_REMOTE_REF"; then
echo "Prepare 未 fetch 配置的主分支: $MAIN_REMOTE_REF" >&2
exit 1
fi
if [ ! -f "$WORKTREE_DIR/.git" ]; then
exec 9>"$WORKTREE_LOCK"
flock 9
if [ ! -f "$WORKTREE_DIR/.git" ]; then
if [ -e "$WORKTREE_DIR" ]; then
echo "发布 Worktree 路径已存在但不是 linked worktree: $WORKTREE_DIR" >&2
exit 1
fi
mkdir -p "$(dirname "$WORKTREE_DIR")"
git --git-dir="$REPOSITORY_DIR" worktree add --detach "$WORKTREE_DIR" "$HEAD_SHA"
fi
flock -u 9
fi
common_dir=$(git -C "$WORKTREE_DIR" rev-parse --path-format=absolute --git-common-dir)
if [ "$common_dir" != "$(realpath "$REPOSITORY_DIR")" ]; then
echo "发布 Worktree 不属于共享仓库: $WORKTREE_DIR" >&2
exit 1
fi
git -C "$WORKTREE_DIR" checkout --detach --force "$HEAD_SHA"
git -C "$WORKTREE_DIR" reset --hard "$HEAD_SHA"
git -C "$WORKTREE_DIR" clean -ffdx
actual_sha=$(git -C "$WORKTREE_DIR" rev-parse HEAD)
if [ "$actual_sha" != "$HEAD_SHA" ]; then
echo "发布 Worktree 提交不匹配: 期望 $HEAD_SHA,实际 $actual_sha" >&2
exit 1
fi
git -C "$WORKTREE_DIR" config user.name "${{ env.GIT_USER_NAME }}"
git -C "$WORKTREE_DIR" config user.email "${{ env.GIT_USER_EMAIL }}"
echo "REPOSITORY_DIR=$REPOSITORY_DIR" >> "$GITHUB_ENV"
echo "REPO_DIR=$WORKTREE_DIR" >> "$GITHUB_ENV"
echo "REPO_NAME=$REPO_NAME" >> "$GITHUB_ENV"
echo "MAIN_REMOTE_REF=$MAIN_REMOTE_REF" >> "$GITHUB_ENV"
echo "发布 Worktree: $WORKTREE_DIR @ $actual_sha"
- name: 🤖 检查 Bot 提交
id: check_bot
run: |
echo "======================================"
echo "🤖 检查是否为 Bot 提交"
echo "======================================"
cd ${{ env.REPO_DIR }}
COMMIT_MSG=$(git log -1 --pretty=%B "${{ env.SOURCE_SHA }}")
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="${{ env.VERSION }}"
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
# 计算 Release Notes 的增量基准 tag,规则按当前发布是否为预发布(rc)区分:
# - 预发布(rc):基准 = 紧邻的上一个 tag(不跳过同版本 rc)
# 例如 0.0.5-rc2 的基准是 0.0.5-rc1Release Notes 只展示 rc1→rc2 的新提交,
# 避免同一版本各 rc 之间大量重复。
# - 正式版:基准 = 上一个"不同 CHANGELOG 版本"的 tag(即上面的 PREVIOUS_TAG
# 例如 0.0.5 正式版会跳过 0.0.5-rc1/rc2,基准落到 0.0.4-rc2
# Release Notes 覆盖整个 0.0.5 开发周期的全量提交。
# 注意:CHANGELOG.md 始终是整个版本的累积(strip 模式),与此处无关。
if [[ "${RELEASE_IS_PRERELEASE}" == "true" ]]; then
NOTES_BASE_TAG=""
while IFS= read -r tag; do
[ -z "$tag" ] && continue
if [ "$tag" != "$VERSION" ]; then
NOTES_BASE_TAG="$tag"
break
fi
done <<< "$ALL_TAGS"
echo "🏷️ 预发布:Release Notes 基准 = 紧邻上一个 tag"
else
NOTES_BASE_TAG="$PREVIOUS_TAG"
echo "🏷️ 正式版:Release Notes 基准 = 上一个不同版本 tag(覆盖整个版本周期)"
fi
if [ -z "$NOTES_BASE_TAG" ]; then
echo "️ 未找到基准 tagRelease Notes 将包含全部提交"
RELEASE_NOTES_RANGE="HEAD"
else
echo "📍 Release Notes 增量基准: $NOTES_BASE_TAG"
RELEASE_NOTES_RANGE="${NOTES_BASE_TAG}..${VERSION}"
fi
echo "RELEASE_NOTES_RANGE=$RELEASE_NOTES_RANGE" >> $GITHUB_ENV
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 }}"
# 初始化条目内容
# 转义用户内容中的反斜杠,避免后续 echo -e 误展开 message/body 里的
# 字面 \n \t \\ 等序列(我们自己插入的 \n 分隔符不经过此函数,仍正常换行)
esc() { printf '%s' "$1" | sed 's/\\/\\\\/g'; }
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=$(esc "${COMMITS_MESSAGE[$i]}")
body="${COMMITS_BODY[$i]}"
echo " [$((i+1))/$VALID_COUNT] ${hash:0:7}: ${COMMITS_MESSAGE[$i]: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;')
cleaned_body=$(esc "$cleaned_body")
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 -vxF "${{ 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 -vxF "${{ 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..."
# 从 Prepare 已 fetch 的远端引用准备主分支,再写入 CHANGELOG
MAIN_BRANCH="${{ env.MAIN_BRANCH }}"
MAIN_REMOTE_REF="${{ env.MAIN_REMOTE_REF }}"
git -C "${{ env.REPO_DIR }}" checkout --force -B "$MAIN_BRANCH" "$MAIN_REMOTE_REF"
git -C "${{ env.REPO_DIR }}" reset --hard "$MAIN_REMOTE_REF"
git -C "${{ env.REPO_DIR }}" clean -ffdx
# ============================================
# 更新或创建 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][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]\]/ {
if (match($0, /\[[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]\]/)) {
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=$(esc "${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;')
cleaned_body=$(esc "$cleaned_body")
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 }} 分支..."
if git -c credential.helper= \
-c 'credential.helper=!f() { test "$1" = get || exit 0; printf "%s\n" username=oauth2; printf "%s\n" "password=${ACCESS_TOKEN}"; }; f' \
-c credential.interactive=never \
-C "${{ env.REPO_DIR }}" push origin "${{ env.MAIN_BRANCH }}"; 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 }}
# Release Notes 只展示本次 rc 相对上一个 tag 的增量提交
# CHANGELOG.md 仍是整个版本的累积,二者用途不同)
RELEASE_NOTES_RANGE="${{ env.RELEASE_NOTES_RANGE }}"
REPO_URL="${{ github.server_url }}/${{ github.repository }}"
echo "📊 增量范围: $RELEASE_NOTES_RANGE"
TEMP_COMMITS=$(mktemp)
git log "$RELEASE_NOTES_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
IFS='|' read -ra IGNORE_PATTERNS <<< "${{ env.IGNORE_PATTERNS_PROCESSED }}"
COMMIT_INDEX=0
CURRENT_HASH=""
CURRENT_AUTHOR=""
CURRENT_MESSAGE=""
CURRENT_BODY=""
IN_BODY=false
flush_commit() {
[ -z "$CURRENT_HASH" ] && return
for pattern in "${IGNORE_PATTERNS[@]}"; do
[ -z "$pattern" ] && continue
if echo "$CURRENT_MESSAGE" | grep -qiE "$pattern"; then
return
fi
done
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))
}
while IFS= read -r line; do
if [[ "$line" == "COMMIT_START" ]]; then
flush_commit
CURRENT_HASH=""
CURRENT_AUTHOR=""
CURRENT_MESSAGE=""
CURRENT_BODY=""
IN_BODY=false
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"
flush_commit
rm -f "$TEMP_COMMITS"
VALID_COUNT=$COMMIT_INDEX
echo "✅ 增量有效提交: $VALID_COUNT"
# 转义用户内容中的反斜杠,避免后续 echo -e 误展开 message/body 里的字面序列
esc() { printf '%s' "$1" | sed 's/\\/\\\\/g'; }
NOTES="${CHANGELOG_SECTION_TITLE}\n\n"
for ((i=0; i<$VALID_COUNT; i++)); do
hash="${COMMITS_HASH[$i]}"
author="${COMMITS_AUTHOR[$i]}"
message=$(esc "${COMMITS_MESSAGE[$i]}")
body="${COMMITS_BODY[$i]}"
short_hash="${hash:0:7}"
COMMIT_LINK="([${short_hash}](${REPO_URL}/commit/${hash}))"
AUTHOR_INFO=""
[ -n "$author" ] && AUTHOR_INFO=" by @${author}"
if [ -n "$body" ]; then
cleaned_body=$(echo "$body" | sed -e :a -e '/^\s*$/d;')
cleaned_body=$(esc "$cleaned_body")
else
cleaned_body=""
fi
if [ -n "$cleaned_body" ]; then
NOTES="${NOTES}- ${message} \n"
while IFS= read -r bline; do
[ -z "$bline" ] && continue
NOTES="${NOTES} ${bline} \n"
done <<< "$cleaned_body"
NOTES="${NOTES} ${COMMIT_LINK}${AUTHOR_INFO}\n"
else
NOTES="${NOTES}- ${message} ${COMMIT_LINK}${AUTHOR_INFO}\n"
fi
done
# 增量贡献者(同样过滤 bot)
if [ "$RELEASE_NOTES_RANGE" = "HEAD" ]; then
CONTRIBUTORS=$(git log --pretty=format:"%an" --no-merges)
else
CONTRIBUTORS=$(git log "$RELEASE_NOTES_RANGE" --pretty=format:"%an" --no-merges)
fi
CONTRIBUTORS=$(echo "$CONTRIBUTORS" | \
grep -v "github-actions\[bot\]" | \
grep -v "dependabot\[bot\]" | \
grep -v "renovate\[bot\]" | \
grep -vxF "${{ env.GIT_USER_NAME }}" | \
sort -u)
if [ -n "$CONTRIBUTORS" ]; then
NOTES="${NOTES}\n${{ env.CHANGELOG_CONTRIBUTORS_TITLE }}\n\n"
while IFS= read -r name; do
[ -z "$name" ] && continue
NOTES="${NOTES}<a href=\"${{ env.GITEA_SERVER }}/${name}\">\n"
NOTES="${NOTES} <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"
NOTES="${NOTES}</a>\n"
done <<< "$CONTRIBUTORS"
fi
echo -e "$NOTES" > /tmp/release-body.txt
echo "✅ Release notes 生成完成 ($(wc -c < /tmp/release-body.txt) 字节)"
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 ""
python3 << 'PYSCRIPT'
import json
import os
with open('/tmp/release-body.txt', 'r', encoding='utf-8') as release_body_file:
body = release_body_file.read()
payload = {
"tag_name": os.environ["VERSION"],
"name": os.environ["RELEASE_TITLE_PROCESSED"],
"body": body,
"draft": os.environ["RELEASE_IS_DRAFT"] == "true",
"prerelease": os.environ["RELEASE_IS_PRERELEASE"] == "true",
}
with open('/tmp/payload.json', 'w', encoding='utf-8') as payload_file:
json.dump(payload, payload_file, ensure_ascii=False, indent=2)
print("✓ Payload prepared")
PYSCRIPT
echo "🌐 发送 API 请求..."
HTTP_CODE=$(curl -s -o /tmp/release_response.json -w "%{http_code}" -X POST \
-H "Authorization: token ${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 ${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 版本 | **`${{ env.VERSION }}`** |
| 📝 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.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 已创建
EOFSUCCESS
else
cat >> $GITHUB_STEP_SUMMARY << 'EOFREBUILD'
| 📋 执行状态 | ✅ 执行成功 (重建 Tag) |
---
### ✅ 完成的操作
- ️ CHANGELOG.md 无需更新(内容已存在)
- ✅ Release 已创建
💡 **说明**: 检测到这是重建已删除的 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}/src/branch/${{ env.MAIN_BRANCH }}/.gitea/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.changelog.outputs.changelog_updated }}" = "true" ]; then
echo "📊 执行结果:"
echo " - Tag: ${{ env.VERSION }}"
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 已创建"
else
echo "✅ 已完成的操作:"
echo " - Release 已创建(从现有 CHANGELOG 内容)"
echo ""
echo "💡 说明: 检测到这是重建已删除的 tag,CHANGELOG 中已包含"
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 "✅ 清理完成"