name: Auto Release Pipeline on: push: branches: - main workflow_dispatch: inputs: version_type: description: 'Release type' required: true default: 'patch' type: choice options: - patch - minor - major - beta - rc - release prerelease_number: description: 'Beta/RC number (only for beta/rc types)' required: false default: '1' permissions: contents: write packages: write jobs: release-pipeline: runs-on: ubuntu-latest # 跳过由GitHub Actions创建的提交,避免死循环 (仅对push事件生效) if: | github.event_name == 'workflow_dispatch' || (github.event.pusher.name != 'github-actions[bot]' && !contains(github.event.head_commit.message, '[skip ci]')) steps: - name: Checkout code uses: actions/checkout@v5 with: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - name: Check if version bump is needed id: check run: | # 检测是否是合并提交 PARENT_COUNT=$(git rev-list --parents -n 1 HEAD | wc -w) PARENT_COUNT=$((PARENT_COUNT - 1)) echo "Parent count: $PARENT_COUNT" if [ "$PARENT_COUNT" -gt 1 ]; then # 合并提交:获取合并进来的所有文件变更 echo "Detected merge commit, getting all merged changes" # 获取合并基准点 MERGE_BASE=$(git merge-base HEAD^1 HEAD^2 2>/dev/null || echo "") if [ -n "$MERGE_BASE" ]; then # 获取从合并基准到 HEAD 的所有变更 CHANGED_FILES=$(git diff --name-only $MERGE_BASE..HEAD) else # 如果无法获取合并基准,使用第二个父提交 CHANGED_FILES=$(git diff --name-only HEAD^2..HEAD) fi else # 普通提交:获取相对于上一个提交的变更 CHANGED_FILES=$(git diff --name-only HEAD~1..HEAD 2>/dev/null || git diff --name-only $(git rev-list --max-parents=0 HEAD)..HEAD) fi echo "Changed files:" echo "$CHANGED_FILES" # 检查是否只有无关文件(.md, docs/, .github/等) SIGNIFICANT_CHANGES=false while IFS= read -r file; do # 跳过空行 [ -z "$file" ] && continue # 检查是否是需要忽略的文件 if [[ ! "$file" =~ \.(md|txt)$ ]] && [[ ! "$file" =~ ^docs/ ]] && [[ ! "$file" =~ ^\.github/workflows/ ]] && [[ "$file" != "VERSION" ]] && [[ "$file" != ".gitignore" ]] && [[ "$file" != "LICENSE" ]]; then echo "Found significant change in: $file" SIGNIFICANT_CHANGES=true break fi done <<< "$CHANGED_FILES" if [ "$SIGNIFICANT_CHANGES" = true ]; then echo "Significant changes detected, version bump needed" echo "needs_bump=true" >> $GITHUB_OUTPUT else echo "No significant changes, skipping version bump" echo "needs_bump=false" >> $GITHUB_OUTPUT fi - name: Get current version if: steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch' id: get_version run: | # 获取最新的tag版本 LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") echo "Latest tag: $LATEST_TAG" TAG_VERSION=${LATEST_TAG#v} # 获取VERSION文件中的版本 FILE_VERSION=$(cat VERSION | tr -d '[:space:]') echo "VERSION file: $FILE_VERSION" # 比较tag版本和文件版本,取较大值 function version_gt() { test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"; } if version_gt "$FILE_VERSION" "$TAG_VERSION"; then VERSION="$FILE_VERSION" echo "Using VERSION file: $VERSION (newer than tag)" else VERSION="$TAG_VERSION" echo "Using tag version: $VERSION (newer or equal to file)" fi echo "Current version: $VERSION" echo "current_version=$VERSION" >> $GITHUB_OUTPUT - name: Calculate next version if: steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch' id: next_version env: VERSION_TYPE: ${{ github.event.inputs.version_type || 'patch' }} PRERELEASE_NUM: ${{ github.event.inputs.prerelease_number || '1' }} run: | VERSION="${{ steps.get_version.outputs.current_version }}" # 移除可能存在的 prerelease 后缀以获取基础版本 BASE_VERSION=$(echo "$VERSION" | sed 's/-.*$//') # 分割版本号 IFS='.' read -r -a version_parts <<< "$BASE_VERSION" MAJOR="${version_parts[0]:-0}" MINOR="${version_parts[1]:-0}" PATCH="${version_parts[2]:-0}" echo "Base version: $MAJOR.$MINOR.$PATCH" echo "Version type: $VERSION_TYPE" case "$VERSION_TYPE" in major) NEW_VERSION="$((MAJOR + 1)).0.0" IS_PRERELEASE=false ;; minor) NEW_VERSION="${MAJOR}.$((MINOR + 1)).0" IS_PRERELEASE=false ;; patch) NEW_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))" IS_PRERELEASE=false ;; beta) NEW_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))-beta.${PRERELEASE_NUM}" IS_PRERELEASE=true ;; rc) NEW_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))-rc.${PRERELEASE_NUM}" IS_PRERELEASE=true ;; release) # Convert prerelease to stable release (e.g., 0.4.1-rc.1 -> 0.4.1) NEW_VERSION="${BASE_VERSION}" IS_PRERELEASE=false ;; *) # 默认 patch NEW_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))" IS_PRERELEASE=false ;; esac echo "New version: $NEW_VERSION" echo "Is prerelease: $IS_PRERELEASE" echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT echo "new_tag=v$NEW_VERSION" >> $GITHUB_OUTPUT echo "is_prerelease=$IS_PRERELEASE" >> $GITHUB_OUTPUT - name: Update VERSION file if: (steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch') && steps.next_version.outputs.is_prerelease != 'true' run: | echo "${{ steps.next_version.outputs.new_version }}" > VERSION - name: Setup Node.js for formatting if: steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch' uses: actions/setup-node@v4 with: node-version: "20" - name: Setup Bun if: steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch' uses: oven-sh/setup-bun@v2 - name: Install dependencies and format code if: steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch' run: | bun install bun run format - name: Commit VERSION and formatted code if: steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch' run: | # 配置git git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" # 添加所有更改(VERSION文件 + 格式化后的代码) git add -A # 排除 .github/ 目录的更改(workflow 文件不需要自动提交,且需要特殊权限) git restore --staged .github/ 2>/dev/null || true # 检查是否有更改需要提交 if git diff --cached --quiet; then echo "No changes to commit" else # 提交所有更改 - 添加 [skip ci] 以避免再次触发 git commit -m "chore: sync VERSION file with release ${{ steps.next_version.outputs.new_tag }} [skip ci]" fi - name: Create and push tag if: steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch' run: | NEW_TAG="${{ steps.next_version.outputs.new_tag }}" git tag -a "$NEW_TAG" -m "Release $NEW_TAG" git push origin HEAD:main "$NEW_TAG" - name: Prepare image names id: image_names if: steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch' run: | GHCR_IMAGE=$(echo "ghcr.io/${{ github.repository_owner }}/claude-code-hub" | tr '[:upper:]' '[:lower:]') echo "ghcr_image=${GHCR_IMAGE}" >> "$GITHUB_OUTPUT" - name: Create GitHub Release if: steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch' uses: softprops/action-gh-release@v2 with: tag_name: ${{ steps.next_version.outputs.new_tag }} name: Release ${{ steps.next_version.outputs.new_version }} body: | ## Docker ```bash docker pull ${{ steps.image_names.outputs.ghcr_image }}:${{ steps.next_version.outputs.new_tag }} ${{ steps.next_version.outputs.is_prerelease != 'true' && format('docker pull {0}:latest', steps.image_names.outputs.ghcr_image) || '' }} ``` --- Release notes will be auto-generated... draft: false prerelease: ${{ steps.next_version.outputs.is_prerelease == 'true' }} generate_release_notes: false # 自清理旧的tags和releases(保持最近50个) - 仅清理正式版本 - name: Cleanup old tags and releases if: (steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch') && steps.next_version.outputs.is_prerelease != 'true' continue-on-error: true env: TAGS_TO_KEEP: 50 GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | echo "🧹 自动清理旧版本,保持最近 $TAGS_TO_KEEP 个tag..." # 获取所有版本tag并按版本号排序(从旧到新) echo "正在获取所有tags..." ALL_TAGS=$(git ls-remote --tags origin | grep -E 'refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$' | awk '{print $2}' | sed 's|refs/tags/||' | sort -V) # 检查是否获取到tags if [ -z "$ALL_TAGS" ]; then echo "⚠️ 未找到任何版本tag" exit 0 fi TOTAL_COUNT=$(echo "$ALL_TAGS" | wc -l) echo "📊 当前tag统计:" echo "- 总数: $TOTAL_COUNT" echo "- 配置保留: $TAGS_TO_KEEP" if [ "$TOTAL_COUNT" -gt "$TAGS_TO_KEEP" ]; then DELETE_COUNT=$((TOTAL_COUNT - TAGS_TO_KEEP)) echo "- 将要删除: $DELETE_COUNT 个最旧的tag" # 获取要删除的tags(最老的) TAGS_TO_DELETE=$(echo "$ALL_TAGS" | head -n "$DELETE_COUNT") # 显示将要删除的版本范围 OLDEST_TO_DELETE=$(echo "$TAGS_TO_DELETE" | head -1) NEWEST_TO_DELETE=$(echo "$TAGS_TO_DELETE" | tail -1) echo "" echo "🗑️ 将要删除的版本范围:" echo "- 从: $OLDEST_TO_DELETE" echo "- 到: $NEWEST_TO_DELETE" echo "" echo "开始执行删除..." SUCCESS_COUNT=0 FAIL_COUNT=0 for tag in $TAGS_TO_DELETE; do echo -n " 删除 $tag ... " # 先检查release是否存在 if gh release view "$tag" >/dev/null 2>&1; then # Release存在,删除release会同时删除tag if gh release delete "$tag" --yes --cleanup-tag 2>/dev/null; then echo "(release+tag)" SUCCESS_COUNT=$((SUCCESS_COUNT + 1)) else echo "❌ (release删除失败)" FAIL_COUNT=$((FAIL_COUNT + 1)) fi else # Release不存在,只删除tag if git push origin --delete "$tag" 2>/dev/null; then echo "(仅tag)" SUCCESS_COUNT=$((SUCCESS_COUNT + 1)) else echo "⏭️ (已不存在)" FAIL_COUNT=$((FAIL_COUNT + 1)) fi fi done echo "" echo "📊 清理结果:" echo "- 成功删除: $SUCCESS_COUNT" echo "- 失败/跳过: $FAIL_COUNT" # 重新获取并显示保留的版本范围 echo "" echo "正在验证清理结果..." REMAINING_TAGS=$(git ls-remote --tags origin | grep -E 'refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$' | awk '{print $2}' | sed 's|refs/tags/||' | sort -V) REMAINING_COUNT=$(echo "$REMAINING_TAGS" | wc -l) OLDEST=$(echo "$REMAINING_TAGS" | head -1) NEWEST=$(echo "$REMAINING_TAGS" | tail -1) echo "清理完成!" echo "" echo "📌 当前保留的版本:" echo "- 最旧版本: $OLDEST" echo "- 最新版本: $NEWEST" echo "- 版本总数: $REMAINING_COUNT" # 验证是否达到预期 if [ "$REMAINING_COUNT" -le "$TAGS_TO_KEEP" ]; then echo "- 状态: 符合预期(≤$TAGS_TO_KEEP)" else echo "- 状态: ⚠️ 超出预期(某些tag可能删除失败)" fi else echo "当前tag数量($TOTAL_COUNT)未超过限制($TAGS_TO_KEEP),无需清理" fi # Docker构建步骤 - name: Set up QEMU if: steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch' uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx if: steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch' uses: docker/setup-buildx-action@v3 - name: Log in to GitHub Container Registry if: steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch' uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push Docker image if: steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch' uses: docker/build-push-action@v6 with: context: . file: ./deploy/Dockerfile platforms: linux/amd64,linux/arm64 push: true build-args: | APP_VERSION=${{ steps.next_version.outputs.new_version }} tags: | ${{ steps.image_names.outputs.ghcr_image }}:${{ steps.next_version.outputs.new_tag }} ${{ steps.next_version.outputs.is_prerelease != 'true' && format('{0}:latest', steps.image_names.outputs.ghcr_image) || '' }} ${{ steps.image_names.outputs.ghcr_image }}:${{ steps.next_version.outputs.new_version }} labels: | org.opencontainers.image.version=${{ steps.next_version.outputs.new_version }} org.opencontainers.image.revision=${{ github.sha }} org.opencontainers.image.source=https://github.com/${{ github.repository }} cache-from: type=gha cache-to: type=gha,mode=max # 同步 main 分支到 dev 分支 (rebase dev onto main) - 仅正式版本触发 - name: Sync main to dev branch if: (steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch') && steps.next_version.outputs.is_prerelease != 'true' id: sync_dev continue-on-error: true env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -e echo "==========================================" echo "Starting dev branch sync after release..." echo "==========================================" # 配置 git git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" # 确保在 main 分支的最新状态 git fetch origin main git checkout main git reset --hard origin/main # 获取 dev 分支 echo "Fetching dev branch..." if ! git fetch origin dev:dev 2>/dev/null; then echo "::warning::dev branch not found, skipping sync" echo "sync_status=skipped" >> $GITHUB_OUTPUT echo "sync_reason=dev branch not found" >> $GITHUB_OUTPUT exit 0 fi # 检查 dev 是否有领先于 main 的 commit git checkout dev AHEAD_COUNT=$(git rev-list --count main..dev) BEHIND_COUNT=$(git rev-list --count dev..main) echo "Dev branch status:" echo " - Ahead of main: $AHEAD_COUNT commits" echo " - Behind main: $BEHIND_COUNT commits" # 如果 dev 没有落后于 main,跳过同步 if [ "$BEHIND_COUNT" -eq 0 ]; then echo "::notice::dev branch is already up to date with main" echo "sync_status=skipped" >> $GITHUB_OUTPUT echo "sync_reason=already up to date" >> $GITHUB_OUTPUT exit 0 fi echo "" echo "Attempting to rebase dev onto main..." echo "This will preserve $AHEAD_COUNT commits from dev" # 尝试 rebase if git rebase main; then echo "Rebase successful!" # 使用 --force-with-lease 安全推送 echo "Pushing rebased dev branch..." if git push origin dev --force-with-lease; then echo "::notice::Successfully synced main to dev branch (rebased $AHEAD_COUNT commits)" echo "sync_status=success" >> $GITHUB_OUTPUT echo "ahead_count=$AHEAD_COUNT" >> $GITHUB_OUTPUT else echo "::error::Failed to push rebased dev branch" echo "sync_status=push_failed" >> $GITHUB_OUTPUT echo "sync_reason=push failed, possibly due to concurrent changes" >> $GITHUB_OUTPUT exit 1 fi else echo "::warning::Rebase failed due to conflicts, will trigger autofix" git rebase --abort echo "sync_status=conflict" >> $GITHUB_OUTPUT echo "sync_reason=merge conflicts detected" >> $GITHUB_OUTPUT exit 1 fi # 如果同步失败(冲突),触发 autofix workflow - name: Trigger autofix for sync conflicts if: (steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch') && steps.sync_dev.outputs.sync_status == 'conflict' env: GH_TOKEN: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }} run: | echo "Triggering Claude CI Auto-Fix workflow for dev sync..." # 使用 workflow 文件名而非名称,避免重命名后触发失败 if gh workflow run claude-ci-autofix.yml \ --field task_type=sync-dev \ --field target_branch=dev \ --field source_branch=main \ --field release_tag=${{ steps.next_version.outputs.new_tag }}; then echo "::notice::Autofix workflow triggered to resolve conflicts" else echo "::error::Failed to trigger autofix workflow. Manual intervention required." echo "::error::Please run: gh workflow run claude-ci-autofix.yml --field task_type=sync-dev --field target_branch=dev --field source_branch=main" exit 1 fi # 同步结果汇总 - 仅正式版本显示 - name: Sync summary if: (steps.check.outputs.needs_bump == 'true' || github.event_name == 'workflow_dispatch') && steps.next_version.outputs.is_prerelease != 'true' && always() run: | echo "==========================================" echo "Dev Branch Sync Summary" echo "==========================================" SYNC_STATUS="${{ steps.sync_dev.outputs.sync_status }}" SYNC_REASON="${{ steps.sync_dev.outputs.sync_reason }}" AHEAD_COUNT="${{ steps.sync_dev.outputs.ahead_count }}" case "$SYNC_STATUS" in "success") echo "Status: SUCCESS" echo "Preserved $AHEAD_COUNT commits from dev branch" ;; "skipped") echo "Status: SKIPPED" echo "Reason: $SYNC_REASON" ;; "conflict") echo "Status: CONFLICT - Autofix triggered" echo "Reason: $SYNC_REASON" echo "The Claude CI Auto-Fix workflow has been triggered to resolve conflicts" ;; "push_failed") echo "Status: PUSH FAILED" echo "Reason: $SYNC_REASON" echo "Manual intervention may be required" ;; *) echo "Status: UNKNOWN ($SYNC_STATUS)" ;; esac echo "=========================================="