name: Auto Release Pipeline on: push: branches: - main workflow_dispatch: # 支持手动触发 permissions: contents: write packages: write jobs: release-pipeline: runs-on: ubuntu-latest # 跳过由GitHub Actions创建的提交,避免死循环 if: github.event.pusher.name != 'github-actions[bot]' && !contains(github.event.head_commit.message, '[skip ci]') steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - name: Check if version bump is needed id: check run: | # 检查提交消息是否包含强制发布标记([force release]) COMMIT_MSG=$(git log -1 --pretty=%B | tr -d '\r') echo "Latest commit message:" echo "$COMMIT_MSG" FORCE_RELEASE=false if echo "$COMMIT_MSG" | grep -qi "\[force release\]"; then echo "Detected [force release] marker, forcing version bump" FORCE_RELEASE=true fi # 检测是否是合并提交 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/ ]] && [[ "$file" != "VERSION" ]] && [[ "$file" != ".gitignore" ]] && [[ "$file" != "LICENSE" ]]; then echo "Found significant change in: $file" SIGNIFICANT_CHANGES=true break fi done <<< "$CHANGED_FILES" # 检查是否是手动触发 if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then echo "Manual workflow trigger detected, forcing version bump" echo "needs_bump=true" >> $GITHUB_OUTPUT elif [ "$FORCE_RELEASE" = true ]; then echo "Force release marker detected, forcing version bump" echo "needs_bump=true" >> $GITHUB_OUTPUT elif [ "$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' 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' id: next_version run: | VERSION="${{ steps.get_version.outputs.current_version }}" # 分割版本号 IFS='.' read -r -a version_parts <<< "$VERSION" MAJOR="${version_parts[0]:-0}" MINOR="${version_parts[1]:-0}" PATCH="${version_parts[2]:-0}" # 默认递增patch版本 NEW_PATCH=$((PATCH + 1)) NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}" echo "New version: $NEW_VERSION" echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT echo "new_tag=v$NEW_VERSION" >> $GITHUB_OUTPUT - name: Update VERSION file if: steps.check.outputs.needs_bump == 'true' run: | echo "${{ steps.next_version.outputs.new_version }}" > VERSION # 配置git git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" # 提交VERSION文件 - 添加 [skip ci] 以避免再次触发 git add VERSION git commit -m "chore: sync VERSION file with release ${{ steps.next_version.outputs.new_tag }} [skip ci]" # 构建前端并推送到 web-dist 分支 - name: Setup Node.js if: steps.check.outputs.needs_bump == 'true' uses: actions/setup-node@v4 with: node-version: '18' cache: 'npm' cache-dependency-path: web/admin-spa/package-lock.json - name: Build Frontend if: steps.check.outputs.needs_bump == 'true' run: | echo "Building frontend for version ${{ steps.next_version.outputs.new_version }}..." cd web/admin-spa npm ci npm run build echo "Frontend build completed" - name: Push Frontend Build to web-dist Branch if: steps.check.outputs.needs_bump == 'true' run: | # 创建临时目录 TEMP_DIR=$(mktemp -d) echo "Using temp directory: $TEMP_DIR" # 复制构建产物到临时目录 cp -r web/admin-spa/dist/* "$TEMP_DIR/" # 检查 web-dist 分支是否存在 if git ls-remote --heads origin web-dist | grep -q web-dist; then echo "Checking out existing web-dist branch" git fetch origin web-dist:web-dist git checkout web-dist else echo "Creating new web-dist branch" git checkout --orphan web-dist fi # 清空当前目录(保留 .git) git rm -rf . 2>/dev/null || true # 复制构建产物 cp -r "$TEMP_DIR"/* . # 添加 README cat > README.md << EOF # Claude Relay Service - Web Frontend Build This branch contains the pre-built frontend assets for Claude Relay Service. **DO NOT EDIT FILES IN THIS BRANCH DIRECTLY** These files are automatically generated by the CI/CD pipeline. Version: ${{ steps.next_version.outputs.new_version }} Build Date: $(date -u +"%Y-%m-%d %H:%M:%S UTC") EOF # 创建 .gitignore 文件以排除 node_modules cat > .gitignore << EOF node_modules/ *.log .DS_Store .env EOF # 只添加必要的文件,排除 node_modules git add --all -- ':!node_modules' git commit -m "chore: update frontend build for v${{ steps.next_version.outputs.new_version }} [skip ci]" git push origin web-dist --force # 切换回主分支 git checkout main # 清理临时目录 rm -rf "$TEMP_DIR" echo "Frontend build pushed to web-dist branch successfully" - name: Install git-cliff if: steps.check.outputs.needs_bump == 'true' run: | wget -q https://github.com/orhun/git-cliff/releases/download/v1.4.0/git-cliff-1.4.0-x86_64-unknown-linux-gnu.tar.gz tar -xzf git-cliff-1.4.0-x86_64-unknown-linux-gnu.tar.gz chmod +x git-cliff-1.4.0/git-cliff sudo mv git-cliff-1.4.0/git-cliff /usr/local/bin/ - name: Generate changelog if: steps.check.outputs.needs_bump == 'true' id: changelog run: | # 获取上一个tag以来的更新日志 LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") if [ -n "$LATEST_TAG" ]; then # 排除VERSION文件的提交 CHANGELOG=$(git-cliff --config .github/cliff.toml $LATEST_TAG..HEAD --strip header | grep -v "bump version" | sed '/^$/d' || echo "- 代码优化和改进") else CHANGELOG=$(git-cliff --config .github/cliff.toml --strip header || echo "- 初始版本发布") fi echo "content<> $GITHUB_OUTPUT echo "$CHANGELOG" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT - name: Create and push tag if: steps.check.outputs.needs_bump == 'true' 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' run: | DOCKER_USERNAME="${{ secrets.DOCKERHUB_USERNAME }}" if [ -z "$DOCKER_USERNAME" ]; then DOCKER_USERNAME="weishaw" fi DOCKER_IMAGE=$(echo "${DOCKER_USERNAME}/claude-relay-service" | tr '[:upper:]' '[:lower:]') GHCR_IMAGE=$(echo "ghcr.io/${{ github.repository_owner }}/claude-relay-service" | tr '[:upper:]' '[:lower:]') { echo "docker_image=${DOCKER_IMAGE}" echo "ghcr_image=${GHCR_IMAGE}" } >> "$GITHUB_OUTPUT" - name: Create GitHub Release if: steps.check.outputs.needs_bump == 'true' uses: softprops/action-gh-release@v1 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.docker_image }}:${{ steps.next_version.outputs.new_tag }} docker pull ${{ steps.image_names.outputs.docker_image }}:latest docker pull ${{ steps.image_names.outputs.ghcr_image }}:${{ steps.next_version.outputs.new_tag }} docker pull ${{ steps.image_names.outputs.ghcr_image }}:latest ``` ## 📦 主要更新 ${{ steps.changelog.outputs.content }} ## 📋 完整更新日志 查看 [所有版本](https://github.com/${{ github.repository }}/releases) draft: false prerelease: false generate_release_notes: true # 自动清理旧的tags和releases(保持最近50个) - name: Cleanup old tags and releases if: steps.check.outputs.needs_bump == 'true' continue-on-error: true env: TAGS_TO_KEEP: 50 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' uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx if: steps.check.outputs.needs_bump == 'true' uses: docker/setup-buildx-action@v3 - name: Log in to Docker Hub if: steps.check.outputs.needs_bump == 'true' uses: docker/login-action@v3 with: registry: docker.io username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Log in to GitHub Container Registry if: steps.check.outputs.needs_bump == 'true' 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' uses: docker/build-push-action@v6 with: context: . platforms: linux/amd64,linux/arm64 push: true tags: | ${{ steps.image_names.outputs.docker_image }}:${{ steps.next_version.outputs.new_tag }} ${{ steps.image_names.outputs.docker_image }}:latest ${{ steps.image_names.outputs.docker_image }}:${{ steps.next_version.outputs.new_version }} ${{ steps.image_names.outputs.ghcr_image }}:${{ steps.next_version.outputs.new_tag }} ${{ steps.image_names.outputs.ghcr_image }}:latest ${{ 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 - name: Send Telegram Notification if: steps.check.outputs.needs_bump == 'true' && env.TELEGRAM_BOT_TOKEN != '' && env.TELEGRAM_CHAT_ID != '' env: TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} DOCKER_IMAGE: ${{ steps.image_names.outputs.docker_image }} GHCR_IMAGE: ${{ steps.image_names.outputs.ghcr_image }} continue-on-error: true run: | VERSION="${{ steps.next_version.outputs.new_version }}" TAG="${{ steps.next_version.outputs.new_tag }}" REPO="${{ github.repository }}" # 获取更新内容并限制长度 CHANGELOG="${{ steps.changelog.outputs.content }}" CHANGELOG_TRUNCATED=$(echo "$CHANGELOG" | head -c 1000) if [ ${#CHANGELOG} -gt 1000 ]; then CHANGELOG_TRUNCATED="${CHANGELOG_TRUNCATED}..." fi # 构建消息内容 MESSAGE="🚀 *Claude Relay Service 新版本发布!*"$'\n'$'\n' MESSAGE+="📦 版本号: \`${VERSION}\`"$'\n'$'\n' MESSAGE+="📝 *更新内容:*"$'\n' MESSAGE+="${CHANGELOG_TRUNCATED}"$'\n'$'\n' MESSAGE+="🐳 *Docker 部署:*"$'\n' MESSAGE+="\`\`\`bash"$'\n' MESSAGE+="docker pull ${DOCKER_IMAGE}:${TAG}"$'\n' MESSAGE+="docker pull ${DOCKER_IMAGE}:latest"$'\n' MESSAGE+="docker pull ${GHCR_IMAGE}:${TAG}"$'\n' MESSAGE+="docker pull ${GHCR_IMAGE}:latest"$'\n' MESSAGE+="\`\`\`"$'\n'$'\n' MESSAGE+="🔗 *相关链接:*"$'\n' MESSAGE+="• [GitHub Release](https://github.com/${REPO}/releases/tag/${TAG})"$'\n' MESSAGE+="• [完整更新日志](https://github.com/${REPO}/releases)"$'\n' MESSAGE+="• [Docker Hub](https://hub.docker.com/r/${DOCKER_IMAGE%/*}/claude-relay-service)"$'\n' MESSAGE+="• [GHCR](https://ghcr.io/${GHCR_IMAGE#ghcr.io/})"$'\n'$'\n' MESSAGE+="#ClaudeRelay #Update #v${VERSION//./_}" # 使用 jq 构建 JSON 并发送 jq -n \ --arg chat_id "${TELEGRAM_CHAT_ID}" \ --arg text "${MESSAGE}" \ '{ chat_id: $chat_id, text: $text, parse_mode: "Markdown", disable_web_page_preview: false }' | \ curl -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ -H "Content-Type: application/json" \ -d @-