release.yml 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  1. name: Auto Release Pipeline
  2. on:
  3. push:
  4. branches:
  5. - main
  6. permissions:
  7. contents: write
  8. packages: write
  9. jobs:
  10. release-pipeline:
  11. runs-on: ubuntu-latest
  12. # 跳过由GitHub Actions创建的提交,避免死循环
  13. if: github.event.pusher.name != 'github-actions[bot]' && !contains(github.event.head_commit.message, '[skip ci]')
  14. steps:
  15. - name: Checkout code
  16. uses: actions/checkout@v5
  17. with:
  18. fetch-depth: 0
  19. token: ${{ secrets.GITHUB_TOKEN }}
  20. - name: Check if version bump is needed
  21. id: check
  22. run: |
  23. # 检测是否是合并提交
  24. PARENT_COUNT=$(git rev-list --parents -n 1 HEAD | wc -w)
  25. PARENT_COUNT=$((PARENT_COUNT - 1))
  26. echo "Parent count: $PARENT_COUNT"
  27. if [ "$PARENT_COUNT" -gt 1 ]; then
  28. # 合并提交:获取合并进来的所有文件变更
  29. echo "Detected merge commit, getting all merged changes"
  30. # 获取合并基准点
  31. MERGE_BASE=$(git merge-base HEAD^1 HEAD^2 2>/dev/null || echo "")
  32. if [ -n "$MERGE_BASE" ]; then
  33. # 获取从合并基准到 HEAD 的所有变更
  34. CHANGED_FILES=$(git diff --name-only $MERGE_BASE..HEAD)
  35. else
  36. # 如果无法获取合并基准,使用第二个父提交
  37. CHANGED_FILES=$(git diff --name-only HEAD^2..HEAD)
  38. fi
  39. else
  40. # 普通提交:获取相对于上一个提交的变更
  41. CHANGED_FILES=$(git diff --name-only HEAD~1..HEAD 2>/dev/null || git diff --name-only $(git rev-list --max-parents=0 HEAD)..HEAD)
  42. fi
  43. echo "Changed files:"
  44. echo "$CHANGED_FILES"
  45. # 检查是否只有无关文件(.md, docs/, .github/等)
  46. SIGNIFICANT_CHANGES=false
  47. while IFS= read -r file; do
  48. # 跳过空行
  49. [ -z "$file" ] && continue
  50. # 检查是否是需要忽略的文件
  51. if [[ ! "$file" =~ \.(md|txt)$ ]] &&
  52. [[ ! "$file" =~ ^docs/ ]] &&
  53. [[ ! "$file" =~ ^\.github/workflows/ ]] &&
  54. [[ "$file" != "VERSION" ]] &&
  55. [[ "$file" != ".gitignore" ]] &&
  56. [[ "$file" != "LICENSE" ]]; then
  57. echo "Found significant change in: $file"
  58. SIGNIFICANT_CHANGES=true
  59. break
  60. fi
  61. done <<< "$CHANGED_FILES"
  62. if [ "$SIGNIFICANT_CHANGES" = true ]; then
  63. echo "Significant changes detected, version bump needed"
  64. echo "needs_bump=true" >> $GITHUB_OUTPUT
  65. else
  66. echo "No significant changes, skipping version bump"
  67. echo "needs_bump=false" >> $GITHUB_OUTPUT
  68. fi
  69. - name: Get current version
  70. if: steps.check.outputs.needs_bump == 'true'
  71. id: get_version
  72. run: |
  73. # 获取最新的tag版本
  74. LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
  75. echo "Latest tag: $LATEST_TAG"
  76. TAG_VERSION=${LATEST_TAG#v}
  77. # 获取VERSION文件中的版本
  78. FILE_VERSION=$(cat VERSION | tr -d '[:space:]')
  79. echo "VERSION file: $FILE_VERSION"
  80. # 比较tag版本和文件版本,取较大值
  81. function version_gt() { test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"; }
  82. if version_gt "$FILE_VERSION" "$TAG_VERSION"; then
  83. VERSION="$FILE_VERSION"
  84. echo "Using VERSION file: $VERSION (newer than tag)"
  85. else
  86. VERSION="$TAG_VERSION"
  87. echo "Using tag version: $VERSION (newer or equal to file)"
  88. fi
  89. echo "Current version: $VERSION"
  90. echo "current_version=$VERSION" >> $GITHUB_OUTPUT
  91. - name: Calculate next version
  92. if: steps.check.outputs.needs_bump == 'true'
  93. id: next_version
  94. run: |
  95. VERSION="${{ steps.get_version.outputs.current_version }}"
  96. # 分割版本号
  97. IFS='.' read -r -a version_parts <<< "$VERSION"
  98. MAJOR="${version_parts[0]:-0}"
  99. MINOR="${version_parts[1]:-0}"
  100. PATCH="${version_parts[2]:-0}"
  101. # 默认递增patch版本
  102. NEW_PATCH=$((PATCH + 1))
  103. NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}"
  104. echo "New version: $NEW_VERSION"
  105. echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
  106. echo "new_tag=v$NEW_VERSION" >> $GITHUB_OUTPUT
  107. - name: Update VERSION file
  108. if: steps.check.outputs.needs_bump == 'true'
  109. run: |
  110. echo "${{ steps.next_version.outputs.new_version }}" > VERSION
  111. - name: Setup Node.js for formatting
  112. if: steps.check.outputs.needs_bump == 'true'
  113. uses: actions/setup-node@v4
  114. with:
  115. node-version: "20"
  116. - name: Setup Bun
  117. if: steps.check.outputs.needs_bump == 'true'
  118. uses: oven-sh/setup-bun@v2
  119. with:
  120. bun-version: '1.3.2'
  121. - name: Install dependencies and format code
  122. if: steps.check.outputs.needs_bump == 'true'
  123. run: |
  124. bun install --frozen-lockfile
  125. bun run format
  126. - name: Update seed price table
  127. if: steps.check.outputs.needs_bump == 'true'
  128. continue-on-error: true
  129. run: |
  130. echo "📦 正在下载最新的 LiteLLM 价格表..."
  131. # 确保目录存在
  132. mkdir -p public/seed
  133. # 下载最新价格表
  134. if curl -sSL -f "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json" -o public/seed/litellm-prices.json; then
  135. echo "价格表下载成功"
  136. # 验证 JSON 格式
  137. if node -e "JSON.parse(require('fs').readFileSync('public/seed/litellm-prices.json', 'utf-8'))"; then
  138. echo "JSON 格式验证通过"
  139. # 显示统计信息
  140. TOTAL_MODELS=$(node -e "console.log(Object.keys(JSON.parse(require('fs').readFileSync('public/seed/litellm-prices.json', 'utf-8'))).length)")
  141. CLAUDE_MODELS=$(node -e "console.log(Object.keys(JSON.parse(require('fs').readFileSync('public/seed/litellm-prices.json', 'utf-8'))).filter(m => m.toLowerCase().startsWith('claude-')).length)")
  142. echo "📊 价格表统计: 总模型数 $TOTAL_MODELS, Claude 模型 $CLAUDE_MODELS"
  143. else
  144. echo "❌ JSON 格式验证失败,保留旧文件"
  145. git checkout public/seed/litellm-prices.json
  146. fi
  147. else
  148. echo "⚠️ 价格表下载失败,保留现有文件"
  149. fi
  150. - name: Commit VERSION and formatted code
  151. if: steps.check.outputs.needs_bump == 'true'
  152. run: |
  153. # 配置git
  154. git config user.name "github-actions[bot]"
  155. git config user.email "github-actions[bot]@users.noreply.github.com"
  156. # 添加所有更改(VERSION文件 + 格式化后的代码 + 价格表)
  157. git add -A
  158. # 排除 .github/ 目录的更改(workflow 文件不需要自动提交,且需要特殊权限)
  159. git restore --staged .github/ 2>/dev/null || true
  160. # 检查是否有更改需要提交
  161. if git diff --cached --quiet; then
  162. echo "No changes to commit"
  163. else
  164. # 提交所有更改 - 添加 [skip ci] 以避免再次触发
  165. git commit -m "chore: sync VERSION file with release ${{ steps.next_version.outputs.new_tag }} [skip ci]"
  166. fi
  167. - name: Create and push tag
  168. if: steps.check.outputs.needs_bump == 'true'
  169. run: |
  170. NEW_TAG="${{ steps.next_version.outputs.new_tag }}"
  171. git tag -a "$NEW_TAG" -m "Release $NEW_TAG"
  172. git push origin HEAD:main "$NEW_TAG"
  173. - name: Prepare image names
  174. id: image_names
  175. if: steps.check.outputs.needs_bump == 'true'
  176. run: |
  177. GHCR_IMAGE=$(echo "ghcr.io/${{ github.repository_owner }}/claude-code-hub" | tr '[:upper:]' '[:lower:]')
  178. echo "ghcr_image=${GHCR_IMAGE}" >> "$GITHUB_OUTPUT"
  179. - name: Create GitHub Release
  180. if: steps.check.outputs.needs_bump == 'true'
  181. uses: softprops/action-gh-release@v2
  182. with:
  183. tag_name: ${{ steps.next_version.outputs.new_tag }}
  184. name: Release ${{ steps.next_version.outputs.new_version }}
  185. body: |
  186. ## 🐳 Docker 镜像
  187. ```bash
  188. docker pull ${{ steps.image_names.outputs.ghcr_image }}:${{ steps.next_version.outputs.new_tag }}
  189. docker pull ${{ steps.image_names.outputs.ghcr_image }}:latest
  190. ```
  191. ---
  192. 📝 详细更新日志将由 Claude 自动生成...
  193. draft: false
  194. prerelease: false
  195. generate_release_notes: false
  196. # 自动清理旧的tags和releases(保持最近50个)
  197. - name: Cleanup old tags and releases
  198. if: steps.check.outputs.needs_bump == 'true'
  199. continue-on-error: true
  200. env:
  201. TAGS_TO_KEEP: 50
  202. GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  203. run: |
  204. echo "🧹 自动清理旧版本,保持最近 $TAGS_TO_KEEP 个tag..."
  205. # 获取所有版本tag并按版本号排序(从旧到新)
  206. echo "正在获取所有tags..."
  207. 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)
  208. # 检查是否获取到tags
  209. if [ -z "$ALL_TAGS" ]; then
  210. echo "⚠️ 未找到任何版本tag"
  211. exit 0
  212. fi
  213. TOTAL_COUNT=$(echo "$ALL_TAGS" | wc -l)
  214. echo "📊 当前tag统计:"
  215. echo "- 总数: $TOTAL_COUNT"
  216. echo "- 配置保留: $TAGS_TO_KEEP"
  217. if [ "$TOTAL_COUNT" -gt "$TAGS_TO_KEEP" ]; then
  218. DELETE_COUNT=$((TOTAL_COUNT - TAGS_TO_KEEP))
  219. echo "- 将要删除: $DELETE_COUNT 个最旧的tag"
  220. # 获取要删除的tags(最老的)
  221. TAGS_TO_DELETE=$(echo "$ALL_TAGS" | head -n "$DELETE_COUNT")
  222. # 显示将要删除的版本范围
  223. OLDEST_TO_DELETE=$(echo "$TAGS_TO_DELETE" | head -1)
  224. NEWEST_TO_DELETE=$(echo "$TAGS_TO_DELETE" | tail -1)
  225. echo ""
  226. echo "🗑️ 将要删除的版本范围:"
  227. echo "- 从: $OLDEST_TO_DELETE"
  228. echo "- 到: $NEWEST_TO_DELETE"
  229. echo ""
  230. echo "开始执行删除..."
  231. SUCCESS_COUNT=0
  232. FAIL_COUNT=0
  233. for tag in $TAGS_TO_DELETE; do
  234. echo -n " 删除 $tag ... "
  235. # 先检查release是否存在
  236. if gh release view "$tag" >/dev/null 2>&1; then
  237. # Release存在,删除release会同时删除tag
  238. if gh release delete "$tag" --yes --cleanup-tag 2>/dev/null; then
  239. echo "(release+tag)"
  240. SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
  241. else
  242. echo "❌ (release删除失败)"
  243. FAIL_COUNT=$((FAIL_COUNT + 1))
  244. fi
  245. else
  246. # Release不存在,只删除tag
  247. if git push origin --delete "$tag" 2>/dev/null; then
  248. echo "(仅tag)"
  249. SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
  250. else
  251. echo "⏭️ (已不存在)"
  252. FAIL_COUNT=$((FAIL_COUNT + 1))
  253. fi
  254. fi
  255. done
  256. echo ""
  257. echo "📊 清理结果:"
  258. echo "- 成功删除: $SUCCESS_COUNT"
  259. echo "- 失败/跳过: $FAIL_COUNT"
  260. # 重新获取并显示保留的版本范围
  261. echo ""
  262. echo "正在验证清理结果..."
  263. 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)
  264. REMAINING_COUNT=$(echo "$REMAINING_TAGS" | wc -l)
  265. OLDEST=$(echo "$REMAINING_TAGS" | head -1)
  266. NEWEST=$(echo "$REMAINING_TAGS" | tail -1)
  267. echo "清理完成!"
  268. echo ""
  269. echo "📌 当前保留的版本:"
  270. echo "- 最旧版本: $OLDEST"
  271. echo "- 最新版本: $NEWEST"
  272. echo "- 版本总数: $REMAINING_COUNT"
  273. # 验证是否达到预期
  274. if [ "$REMAINING_COUNT" -le "$TAGS_TO_KEEP" ]; then
  275. echo "- 状态: 符合预期(≤$TAGS_TO_KEEP)"
  276. else
  277. echo "- 状态: ⚠️ 超出预期(某些tag可能删除失败)"
  278. fi
  279. else
  280. echo "当前tag数量($TOTAL_COUNT)未超过限制($TAGS_TO_KEEP),无需清理"
  281. fi
  282. # Docker构建步骤
  283. - name: Set up QEMU
  284. if: steps.check.outputs.needs_bump == 'true'
  285. uses: docker/setup-qemu-action@v3
  286. - name: Set up Docker Buildx
  287. if: steps.check.outputs.needs_bump == 'true'
  288. uses: docker/setup-buildx-action@v3
  289. - name: Log in to GitHub Container Registry
  290. if: steps.check.outputs.needs_bump == 'true'
  291. uses: docker/login-action@v3
  292. with:
  293. registry: ghcr.io
  294. username: ${{ github.repository_owner }}
  295. password: ${{ secrets.GITHUB_TOKEN }}
  296. - name: Build and push Docker image
  297. if: steps.check.outputs.needs_bump == 'true'
  298. uses: docker/build-push-action@v6
  299. with:
  300. context: .
  301. file: ./deploy/Dockerfile
  302. platforms: linux/amd64,linux/arm64
  303. push: true
  304. build-args: |
  305. APP_VERSION=${{ steps.next_version.outputs.new_version }}
  306. tags: |
  307. ${{ steps.image_names.outputs.ghcr_image }}:${{ steps.next_version.outputs.new_tag }}
  308. ${{ steps.image_names.outputs.ghcr_image }}:latest
  309. ${{ steps.image_names.outputs.ghcr_image }}:${{ steps.next_version.outputs.new_version }}
  310. labels: |
  311. org.opencontainers.image.version=${{ steps.next_version.outputs.new_version }}
  312. org.opencontainers.image.revision=${{ github.sha }}
  313. org.opencontainers.image.source=https://github.com/${{ github.repository }}
  314. cache-from: type=gha
  315. cache-to: type=gha,mode=max
  316. # 同步 main 分支到 dev 分支 (rebase dev onto main)
  317. - name: Sync main to dev branch
  318. if: steps.check.outputs.needs_bump == 'true'
  319. id: sync_dev
  320. continue-on-error: true
  321. env:
  322. GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  323. run: |
  324. set -e
  325. echo "=========================================="
  326. echo "Starting dev branch sync after release..."
  327. echo "=========================================="
  328. # 配置 git
  329. git config user.name "github-actions[bot]"
  330. git config user.email "github-actions[bot]@users.noreply.github.com"
  331. # 确保在 main 分支的最新状态
  332. git fetch origin main
  333. git checkout main
  334. git reset --hard origin/main
  335. # 获取 dev 分支
  336. echo "Fetching dev branch..."
  337. if ! git fetch origin dev:dev 2>/dev/null; then
  338. echo "::warning::dev branch not found, skipping sync"
  339. echo "sync_status=skipped" >> $GITHUB_OUTPUT
  340. echo "sync_reason=dev branch not found" >> $GITHUB_OUTPUT
  341. exit 0
  342. fi
  343. # 检查 dev 是否有领先于 main 的 commit
  344. git checkout dev
  345. AHEAD_COUNT=$(git rev-list --count main..dev)
  346. BEHIND_COUNT=$(git rev-list --count dev..main)
  347. echo "Dev branch status:"
  348. echo " - Ahead of main: $AHEAD_COUNT commits"
  349. echo " - Behind main: $BEHIND_COUNT commits"
  350. # 如果 dev 没有落后于 main,跳过同步
  351. if [ "$BEHIND_COUNT" -eq 0 ]; then
  352. echo "::notice::dev branch is already up to date with main"
  353. echo "sync_status=skipped" >> $GITHUB_OUTPUT
  354. echo "sync_reason=already up to date" >> $GITHUB_OUTPUT
  355. exit 0
  356. fi
  357. echo ""
  358. echo "Attempting to rebase dev onto main..."
  359. echo "This will preserve $AHEAD_COUNT commits from dev"
  360. # 尝试 rebase
  361. if git rebase main; then
  362. echo "Rebase successful!"
  363. # 使用 --force-with-lease 安全推送
  364. echo "Pushing rebased dev branch..."
  365. if git push origin dev --force-with-lease; then
  366. echo "::notice::Successfully synced main to dev branch (rebased $AHEAD_COUNT commits)"
  367. echo "sync_status=success" >> $GITHUB_OUTPUT
  368. echo "ahead_count=$AHEAD_COUNT" >> $GITHUB_OUTPUT
  369. else
  370. echo "::error::Failed to push rebased dev branch"
  371. echo "sync_status=push_failed" >> $GITHUB_OUTPUT
  372. echo "sync_reason=push failed, possibly due to concurrent changes" >> $GITHUB_OUTPUT
  373. exit 1
  374. fi
  375. else
  376. echo "::warning::Rebase failed due to conflicts, will trigger autofix"
  377. git rebase --abort
  378. echo "sync_status=conflict" >> $GITHUB_OUTPUT
  379. echo "sync_reason=merge conflicts detected" >> $GITHUB_OUTPUT
  380. exit 1
  381. fi
  382. # 如果同步失败(冲突),触发 autofix workflow
  383. - name: Trigger autofix for sync conflicts
  384. if: steps.check.outputs.needs_bump == 'true' && steps.sync_dev.outputs.sync_status == 'conflict'
  385. env:
  386. GH_TOKEN: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }}
  387. run: |
  388. echo "Triggering Claude CI Auto-Fix workflow for dev sync..."
  389. # 使用 workflow 文件名而非名称,避免重命名后触发失败
  390. if gh workflow run claude-ci-autofix.yml \
  391. --field task_type=sync-dev \
  392. --field target_branch=dev \
  393. --field source_branch=main \
  394. --field release_tag=${{ steps.next_version.outputs.new_tag }}; then
  395. echo "::notice::Autofix workflow triggered to resolve conflicts"
  396. else
  397. echo "::error::Failed to trigger autofix workflow. Manual intervention required."
  398. echo "::error::Please run: gh workflow run claude-ci-autofix.yml --field task_type=sync-dev --field target_branch=dev --field source_branch=main"
  399. exit 1
  400. fi
  401. # 同步结果汇总
  402. - name: Sync summary
  403. if: steps.check.outputs.needs_bump == 'true' && always()
  404. run: |
  405. echo "=========================================="
  406. echo "Dev Branch Sync Summary"
  407. echo "=========================================="
  408. SYNC_STATUS="${{ steps.sync_dev.outputs.sync_status }}"
  409. SYNC_REASON="${{ steps.sync_dev.outputs.sync_reason }}"
  410. AHEAD_COUNT="${{ steps.sync_dev.outputs.ahead_count }}"
  411. case "$SYNC_STATUS" in
  412. "success")
  413. echo "Status: SUCCESS"
  414. echo "Preserved $AHEAD_COUNT commits from dev branch"
  415. ;;
  416. "skipped")
  417. echo "Status: SKIPPED"
  418. echo "Reason: $SYNC_REASON"
  419. ;;
  420. "conflict")
  421. echo "Status: CONFLICT - Autofix triggered"
  422. echo "Reason: $SYNC_REASON"
  423. echo "The Claude CI Auto-Fix workflow has been triggered to resolve conflicts"
  424. ;;
  425. "push_failed")
  426. echo "Status: PUSH FAILED"
  427. echo "Reason: $SYNC_REASON"
  428. echo "Manual intervention may be required"
  429. ;;
  430. *)
  431. echo "Status: UNKNOWN ($SYNC_STATUS)"
  432. ;;
  433. esac
  434. echo "=========================================="