| name: Auto Release Pipeline |
|
|
| on: |
| push: |
| branches: |
| - main |
|
|
| permissions: |
| contents: write |
| packages: write |
|
|
| jobs: |
| release-pipeline: |
| runs-on: ubuntu-latest |
| |
| 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: | |
| # 检测是否是合并提交 |
| 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 |
| |
| 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" |
| |
| |
| 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 [ "$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} |
| |
| |
| FILE_VERSION=$(cat VERSION | tr -d '[:space:]') |
| echo "VERSION file: $FILE_VERSION" |
| |
| |
| 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}" |
| |
| |
| 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 config user.name "github-actions[bot]" |
| git config user.email "github-actions[bot]@users.noreply.github.com" |
| |
| |
| git add VERSION |
| git commit -m "chore: sync VERSION file with release ${{ steps.next_version.outputs.new_tag }} [skip ci]" |
|
|
| |
| - 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/" |
| |
| |
| 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 rm -rf . 2>/dev/null || true |
| |
| |
| cp -r "$TEMP_DIR"/* . |
| |
| |
| cat > README.md << EOF |
| |
| |
| 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 |
| |
| |
| cat > .gitignore << EOF |
| node_modules/ |
| *.log |
| .DS_Store |
| .env |
| EOF |
| |
| |
| 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<<EOF" >> $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 |
|
|
| |
| - 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..." |
| |
| |
| 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) |
| |
| |
| 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_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 ... " |
| |
| |
| if gh release view "$tag" >/dev/null 2>&1; then |
| |
| 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 |
| |
| 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 |
|
|
| |
| - 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_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 |
| |
| |
| 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 @- |