| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538 |
- 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 "=========================================="
|