name: 📦 自动发布 on: push: tags: - "[0-9]*" # ========================================== # 🔧 配置区域 - 根据你的项目修改 # ========================================== 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: "" # ===== 服务器配置 ===== # 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" # 额外要上传到 Release 的文件(空格分隔) # 例如: "README.md LICENSE docs/guide.pdf" ADDITIONAL_RELEASE_FILES: "" # ===== Git 提交配置 ===== # Git 用户配置 GIT_USER_NAME: "github-actions[bot]" GIT_USER_EMAIL: "github-actions[bot]@users.noreply.github.com" # Git 提交消息模板 # 可用变量: {version} - 将被替换为实际的版本号 # [skip ci] 标记防止触发无限循环 COMMIT_MESSAGE: ":memo: Auto update CHANGELOG for {version} [skip ci]" # ===== 提交过滤配置 ===== # 需要忽略的提交模式(支持正则表达式) # 这些提交不会被添加到 CHANGELOG 中 # 使用 | 分隔多个模式 # # ⚠️ 重要说明: # - 使用 ^ 表示行首匹配,$ 表示行尾匹配 # - [skip ci] 和 [ci skip] 仅匹配行尾,避免误过滤其他提交 # - 如果要完全匹配某个字符串,使用 ^...$ IGNORE_PATTERNS: >- ^Merge | ^:memo: Auto update CHANGELOG| \[skip ci\]\s*$| \[ci skip\]\s*$| ^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 }}" # 处理 CHANGELOG 版本号 if [[ "${{ env.CHANGELOG_VERSION_MODE }}" == "strip" ]]; then # 去除 pre-release 后缀(-beta, -rc1, -alpha 等) CHANGELOG_VERSION=$(echo "$VERSION" | sed -E 's/-(alpha|beta|rc|pre|preview|dev|test)[0-9]*$//') 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" =~ (alpha|beta|rc|pre|preview|dev|test) ]]; 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 # 导出处理后的配置到 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 # 处理忽略模式(转换为数组) IFS='|' read -ra PATTERNS_ARRAY <<< "${{ env.IGNORE_PATTERNS }}" IGNORE_JSON="[" for pattern in "${PATTERNS_ARRAY[@]}"; do # 去除首尾空格 pattern=$(echo "$pattern" | xargs) if [ -n "$pattern" ]; then IGNORE_JSON="${IGNORE_JSON}\"${pattern}\"," fi done IGNORE_JSON="${IGNORE_JSON%,}]" echo "IGNORE_PATTERNS_JSON=$IGNORE_JSON" >> $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 "❌ 克隆失败" 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 [ -n "${{ env.MAIN_BRANCH }}" ]; then MAIN_BRANCH="${{ env.MAIN_BRANCH }}" echo "✓ 使用配置的主分支: $MAIN_BRANCH" else # 自动检测 main 或 master if git show-ref --verify --quiet refs/remotes/origin/main; then MAIN_BRANCH="main" echo "✓ 自动检测到主分支: main" elif git show-ref --verify --quiet refs/remotes/origin/master; then MAIN_BRANCH="master" echo "✓ 自动检测到主分支: master" else echo "❌ 错误: 无法检测主分支(main 或 master)" exit 1 fi fi # 切换到主分支 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: 🔍 检查版本是否已存在 id: check_version if: steps.check_bot.outputs.is_bot_commit != 'true' run: | echo "======================================" echo "🔍 检查 CHANGELOG 版本" echo "======================================" cd ${{ env.REPO_DIR }} if [ ! -f "CHANGELOG.md" ]; then echo "📝 CHANGELOG.md 不存在,将创建新文件" echo "version_exists=false" >> $GITHUB_OUTPUT else CHANGELOG_VERSION="${{ env.CHANGELOG_VERSION }}" echo "📝 检查版本: $CHANGELOG_VERSION" if grep -q "## :bookmark: $CHANGELOG_VERSION" CHANGELOG.md; then echo "⚠️ 版本 $CHANGELOG_VERSION 已存在于 CHANGELOG.md" echo "version_exists=true" >> $GITHUB_OUTPUT else echo "✅ 版本 $CHANGELOG_VERSION 不存在,可以继续" echo "version_exists=false" >> $GITHUB_OUTPUT fi fi echo "======================================" echo "" - name: 📝 生成 CHANGELOG id: changelog if: steps.check_bot.outputs.is_bot_commit != 'true' && steps.check_version.outputs.version_exists != '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/-(alpha|beta|rc|pre|preview|dev|test)[0-9]*$//') 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 "ℹ️ No previous tag found, using all commits" COMMIT_RANGE="" else echo "📍 Previous tag: $PREVIOUS_TAG" # 对 previous tag 也执行 strip,显示对应的 CHANGELOG 版本 if [[ "${{ env.CHANGELOG_VERSION_MODE }}" == "strip" ]]; then PREV_CHANGELOG_VERSION=$(echo "$PREVIOUS_TAG" | sed -E 's/-(alpha|beta|rc|pre|preview|dev|test)[0-9]*$//') echo " (对应 CHANGELOG 版本: $PREV_CHANGELOG_VERSION)" fi COMMIT_RANGE="${PREVIOUS_TAG}..${VERSION}" fi # 获取提交记录 echo "" echo "📋 获取提交记录..." if [ -z "$COMMIT_RANGE" ]; then git log --pretty=format:'%H|%s' > /tmp/commits.txt else git log --pretty=format:'%H|%s' "$COMMIT_RANGE" > /tmp/commits.txt fi TOTAL_COMMITS=$(wc -l < /tmp/commits.txt) echo "✓ 找到 $TOTAL_COMMITS 个提交" # Python 脚本生成 CHANGELOG python3 << 'PYSCRIPT' import re import json import sys from datetime import datetime # 读取配置 changelog_version = "${{ env.CHANGELOG_VERSION }}" gitea_server = "${{ env.GITEA_SERVER }}" repo = "${{ github.repository }}" section_title = "${{ env.CHANGELOG_SECTION_TITLE }}" contributors_title = "${{ env.CHANGELOG_CONTRIBUTORS_TITLE }}" # 读取忽略模式 ignore_patterns = json.loads('${{ env.IGNORE_PATTERNS_JSON }}') print(f"🔍 Loaded {len(ignore_patterns)} ignore patterns:") for pattern in ignore_patterns: print(f" - {pattern}") print() # 读取提交 with open('/tmp/commits.txt', 'r', encoding='utf-8') as f: lines = f.readlines() commits = [] skipped_commits = [] for line in lines: line = line.strip() if not line or '|' not in line: continue commit_hash, message = line.split('|', 1) # 检查是否应该忽略 should_ignore = False matched_pattern = None for pattern in ignore_patterns: if re.search(pattern, message): should_ignore = True matched_pattern = pattern break if should_ignore: skipped_commits.append((message, matched_pattern)) else: commits.append({'hash': commit_hash, 'message': message}) # 输出统计 print(f"📊 Commit Statistics:") print(f" Total: {len(lines)}") print(f" Valid: {len(commits)}") print(f" Skipped: {len(skipped_commits)}") print() if skipped_commits: print("⏭️ Skipped Commits:") for msg, pattern in skipped_commits: print(f" ⏭️ Skipping: {msg[:80]}... (matched: {pattern})") print() if not commits: print("⚠️ No valid commits found") with open('/tmp/changelog_updated.txt', 'w') as f: f.write('false') sys.exit(0) # 收集贡献者 contributors = set() for commit in commits: # 从提交消息中提取可能的用户名(如果使用了 @username 格式) # 这里简化处理,实际可以通过 git log --format='%an|%ae' 获取 pass # 生成 CHANGELOG 内容 changelog_content = f"## :bookmark: {changelog_version}\n\n" changelog_content += f"*Released on {datetime.now().strftime('%Y-%m-%d')}*\n\n" changelog_content += f"{section_title}\n\n" for commit in commits: short_hash = commit['hash'][:7] message = commit['message'] changelog_content += f"- {message} ([`{short_hash}`](https://github.com/{repo}/commit/{commit['hash']}))\n" # 如果有贡献者信息,可以在这里添加 # changelog_content += f"\n{contributors_title}\n\n" changelog_content += "\n---\n\n" # 更新或创建 CHANGELOG.md changelog_file = 'CHANGELOG.md' try: with open(changelog_file, 'r', encoding='utf-8') as f: existing_content = f.read() except FileNotFoundError: existing_content = "# :memo: CHANGELOG\n\n" # 检查版本是否已存在 if f"## :bookmark: {changelog_version}" in existing_content: print(f"ℹ️ Version {changelog_version} already exists, appending commits") # 找到版本区块,在其中追加新的提交 pattern = rf'(## :bookmark: {re.escape(changelog_version)}.*?\n{re.escape(section_title)}\n\n)(.*?)(\n---|\n## :bookmark: |\Z)' match = re.search(pattern, existing_content, re.DOTALL) if match: existing_commits = match.group(2).strip() new_commits = changelog_content.split(f"{section_title}\n\n")[1].split("\n---")[0].strip() # 合并提交,去重 all_commits_text = existing_commits + "\n" + new_commits # 简单去重(基于完整行) unique_lines = [] seen = set() for line in all_commits_text.split('\n'): if line.strip() and line not in seen: unique_lines.append(line) seen.add(line) merged_commits = '\n'.join(unique_lines) # 重新组装版本区块 updated_section = match.group(1) + merged_commits + "\n\n---\n" new_content = existing_content[:match.start()] + updated_section + existing_content[match.end():] else: print("⚠️ Could not find version section, prepending new content") # 尝试匹配各种可能的 CHANGELOG 标题格式 header_pattern = r'^#\s*(?::[\w_]+:)?\s*CHANGELOG\s*\n' match = re.search(header_pattern, existing_content, re.IGNORECASE | re.MULTILINE) if match: insert_pos = match.end() if not existing_content[insert_pos:insert_pos+1] == '\n': new_content = existing_content[:insert_pos] + '\n' + changelog_content + existing_content[insert_pos:] else: new_content = existing_content[:insert_pos] + '\n' + changelog_content + existing_content[insert_pos+1:] else: # 没有找到标题,在开头插入 new_content = f"# :memo: CHANGELOG\n\n{changelog_content}{existing_content}" else: # 插入新版本 # 尝试匹配各种可能的 CHANGELOG 标题格式 # 支持: # CHANGELOG, # :memo: CHANGELOG, # Changelog 等 # 尝试找到 CHANGELOG 标题(不区分大小写,可选 emoji) header_pattern = r'^#\s*(?::[\w_]+:)?\s*CHANGELOG\s*\n' match = re.search(header_pattern, existing_content, re.IGNORECASE | re.MULTILINE) if match: # 找到了 CHANGELOG 标题,在其后插入 insert_pos = match.end() # 确保标题后有空行 if not existing_content[insert_pos:insert_pos+1] == '\n': new_content = existing_content[:insert_pos] + '\n' + changelog_content + existing_content[insert_pos:] else: new_content = existing_content[:insert_pos] + '\n' + changelog_content + existing_content[insert_pos+1:] else: # 没有找到 CHANGELOG 标题,在文件开头插入标题和内容 if existing_content.strip(): # 文件有内容但没有 CHANGELOG 标题 new_content = f"# :memo: CHANGELOG\n\n{changelog_content}{existing_content}" else: # 文件为空 new_content = f"# :memo: CHANGELOG\n\n{changelog_content}" # 检查内容是否真的改变 content_changed = (new_content != existing_content) version_exists = f"## :bookmark: {changelog_version}" in existing_content if content_changed: with open(changelog_file, 'w', encoding='utf-8') as f: f.write(new_content) print(f"✅ CHANGELOG updated successfully") print(f" Added {len(commits)} commits to version {changelog_version}") with open('/tmp/changelog_updated.txt', 'w') as f: f.write('true') with open('/tmp/content_changed.txt', 'w') as f: f.write('true') elif version_exists: # 内容没有变化,但版本已存在 # 这通常发生在重建已删除的 tag # 我们仍然应该创建 Release(从现有 CHANGELOG 提取内容) print(f"ℹ️ No new content to add") print(f" Version {changelog_version} already contains all {len(commits)} commits") print(f" This usually happens when recreating a deleted tag") print(f" Will still create Release from existing CHANGELOG content") with open('/tmp/changelog_updated.txt', 'w') as f: f.write('true') # 返回 true 以触发 Release 创建 with open('/tmp/content_changed.txt', 'w') as f: f.write('false') # 但文件没有变化,不需要提交 else: # 既没有新内容,版本也不存在(不应该发生) print(f"⚠️ Unexpected state: no content change and version doesn't exist") with open('/tmp/changelog_updated.txt', 'w') as f: f.write('false') with open('/tmp/content_changed.txt', 'w') as f: f.write('false') PYSCRIPT UPDATED=$(cat /tmp/changelog_updated.txt) CONTENT_CHANGED=$(cat /tmp/content_changed.txt 2>/dev/null || echo "false") echo "changelog_updated=$UPDATED" >> $GITHUB_OUTPUT echo "content_changed=$CONTENT_CHANGED" >> $GITHUB_OUTPUT if [ "$UPDATED" = "true" ]; then echo "" echo "✅ CHANGELOG 生成完成" else echo "" echo "ℹ️ 没有有效的提交,跳过 CHANGELOG 更新" fi 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: | echo "======================================" echo "📎 上传 Release 附件" echo "======================================" 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 "" # 上传 CHANGELOG.md echo "📤 上传 CHANGELOG.md..." HTTP_CODE=$(curl -s -o /tmp/upload_response_changelog.json -w "%{http_code}" -X POST \ -H "Authorization: token ${{ env.ACCESS_TOKEN }}" \ -F "attachment=@CHANGELOG.md" \ "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases/$RELEASE_ID/assets?name=CHANGELOG.md") if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "200" ]; then echo "✅ CHANGELOG.md 上传成功" else echo "⚠️ 警告: CHANGELOG.md 上传失败 (HTTP $HTTP_CODE)" fi # 上传额外的文件 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) ---