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}\n" NEW_ENTRY="${NEW_ENTRY} \"${name}\"\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}\]/ { 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 已作为附件上传 💡 **说明**: 检测到这是重建已删除的 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) ---
*📊 由 [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 "
" >> $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 "💡 说明: 检测到这是重建已删除的 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 "✅ 清理完成"