release.yml 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. name: Release
  2. on:
  3. push:
  4. tags:
  5. - 'v*'
  6. workflow_dispatch:
  7. inputs:
  8. tag:
  9. description: 'Tag to release (e.g., v1.0.0)'
  10. required: true
  11. type: string
  12. simple_release:
  13. description: 'Simple release: only x86_64 GHCR image, skip other artifacts'
  14. required: false
  15. type: boolean
  16. default: false
  17. # 环境变量:合并 workflow_dispatch 输入和 repository variable
  18. # tag push 触发时读取 vars.SIMPLE_RELEASE,workflow_dispatch 时使用输入参数
  19. env:
  20. SIMPLE_RELEASE: ${{ github.event.inputs.simple_release == 'true' || vars.SIMPLE_RELEASE == 'true' }}
  21. permissions:
  22. contents: write
  23. packages: write
  24. jobs:
  25. # Update VERSION file with tag version
  26. update-version:
  27. runs-on: ubuntu-latest
  28. steps:
  29. - name: Checkout
  30. uses: actions/checkout@v6
  31. - name: Update VERSION file
  32. run: |
  33. if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
  34. VERSION=${{ github.event.inputs.tag }}
  35. VERSION=${VERSION#v}
  36. else
  37. VERSION=${GITHUB_REF#refs/tags/v}
  38. fi
  39. echo "$VERSION" > backend/cmd/server/VERSION
  40. echo "Updated VERSION file to: $VERSION"
  41. - name: Upload VERSION artifact
  42. uses: actions/upload-artifact@v7
  43. with:
  44. name: version-file
  45. path: backend/cmd/server/VERSION
  46. retention-days: 1
  47. build-frontend:
  48. runs-on: ubuntu-latest
  49. steps:
  50. - name: Checkout
  51. uses: actions/checkout@v6
  52. - name: Setup pnpm
  53. uses: pnpm/action-setup@v6
  54. with:
  55. version: 9
  56. - name: Setup Node.js
  57. uses: actions/setup-node@v6
  58. with:
  59. node-version: '20'
  60. cache: 'pnpm'
  61. cache-dependency-path: frontend/pnpm-lock.yaml
  62. - name: Install dependencies
  63. run: pnpm install --frozen-lockfile
  64. working-directory: frontend
  65. - name: Build frontend
  66. run: pnpm run build
  67. working-directory: frontend
  68. - name: Upload frontend artifact
  69. uses: actions/upload-artifact@v7
  70. with:
  71. name: frontend-dist
  72. path: backend/internal/web/dist/
  73. retention-days: 1
  74. release:
  75. needs: [update-version, build-frontend]
  76. runs-on: ubuntu-latest
  77. steps:
  78. - name: Checkout
  79. uses: actions/checkout@v6
  80. with:
  81. fetch-depth: 0
  82. ref: ${{ github.event.inputs.tag || github.ref }}
  83. - name: Download VERSION artifact
  84. uses: actions/download-artifact@v8
  85. with:
  86. name: version-file
  87. path: backend/cmd/server/
  88. - name: Download frontend artifact
  89. uses: actions/download-artifact@v8
  90. with:
  91. name: frontend-dist
  92. path: backend/internal/web/dist/
  93. - name: Setup Go
  94. uses: actions/setup-go@v6
  95. with:
  96. go-version-file: backend/go.mod
  97. check-latest: false
  98. cache-dependency-path: backend/go.sum
  99. - name: Verify Go version
  100. run: |
  101. go version | grep -q 'go1.27.0'
  102. # Docker setup for GoReleaser
  103. - name: Set up QEMU
  104. uses: docker/setup-qemu-action@v3
  105. - name: Set up Docker Buildx
  106. uses: docker/setup-buildx-action@v3
  107. - name: Login to DockerHub
  108. if: ${{ env.DOCKERHUB_USERNAME != '' }}
  109. uses: docker/login-action@v3
  110. env:
  111. DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
  112. with:
  113. username: ${{ secrets.DOCKERHUB_USERNAME }}
  114. password: ${{ secrets.DOCKERHUB_TOKEN }}
  115. - name: Login to GitHub Container Registry
  116. uses: docker/login-action@v3
  117. with:
  118. registry: ghcr.io
  119. username: ${{ github.repository_owner }}
  120. password: ${{ secrets.GITHUB_TOKEN }}
  121. - name: Fetch tags with annotations
  122. run: |
  123. # 确保获取完整的 annotated tag 信息
  124. git fetch --tags --force
  125. - name: Get tag message
  126. id: tag_message
  127. run: |
  128. if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
  129. TAG_NAME=${{ github.event.inputs.tag }}
  130. else
  131. TAG_NAME=${GITHUB_REF#refs/tags/}
  132. fi
  133. echo "Processing tag: $TAG_NAME"
  134. # 获取完整的 tag message(跳过第一行标题)
  135. TAG_MESSAGE=$(git tag -l --format='%(contents:body)' "$TAG_NAME")
  136. # 调试输出
  137. echo "Tag message length: ${#TAG_MESSAGE}"
  138. echo "Tag message preview:"
  139. echo "$TAG_MESSAGE" | head -10
  140. # 使用 EOF 分隔符处理多行内容
  141. echo "message<<EOF" >> $GITHUB_OUTPUT
  142. echo "$TAG_MESSAGE" >> $GITHUB_OUTPUT
  143. echo "EOF" >> $GITHUB_OUTPUT
  144. - name: Set lowercase owner for GHCR
  145. id: lowercase
  146. run: echo "owner=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
  147. - name: Run GoReleaser
  148. uses: goreleaser/goreleaser-action@v7
  149. with:
  150. version: '~> v2'
  151. args: release --clean --skip=validate ${{ env.SIMPLE_RELEASE == 'true' && '--config=.goreleaser.simple.yaml' || '' }}
  152. env:
  153. GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  154. TAG_MESSAGE: ${{ steps.tag_message.outputs.message }}
  155. GITHUB_REPO_OWNER: ${{ github.repository_owner }}
  156. GITHUB_REPO_OWNER_LOWER: ${{ steps.lowercase.outputs.owner }}
  157. GITHUB_REPO_NAME: ${{ github.event.repository.name }}
  158. DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME || 'skip' }}
  159. # Update DockerHub description
  160. - name: Update DockerHub description
  161. if: ${{ env.SIMPLE_RELEASE != 'true' && env.DOCKERHUB_USERNAME != '' }}
  162. uses: peter-evans/dockerhub-description@v5
  163. env:
  164. DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
  165. with:
  166. username: ${{ secrets.DOCKERHUB_USERNAME }}
  167. password: ${{ secrets.DOCKERHUB_TOKEN }}
  168. repository: ${{ secrets.DOCKERHUB_USERNAME }}/sub2api
  169. short-description: "Sub2API - AI API Gateway Platform"
  170. readme-filepath: ./deploy/DOCKER.md
  171. # Send Telegram notification
  172. - name: Send Telegram Notification
  173. if: ${{ env.SIMPLE_RELEASE != 'true' }}
  174. env:
  175. TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
  176. TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
  177. DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
  178. continue-on-error: true
  179. run: |
  180. # 检查必要的环境变量
  181. if [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; then
  182. echo "Telegram credentials not configured, skipping notification"
  183. exit 0
  184. fi
  185. if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
  186. TAG_NAME=${{ github.event.inputs.tag }}
  187. else
  188. TAG_NAME=${GITHUB_REF#refs/tags/}
  189. fi
  190. VERSION=${TAG_NAME#v}
  191. REPO="${{ github.repository }}"
  192. GHCR_IMAGE="ghcr.io/${REPO,,}" # ${,,} converts to lowercase
  193. # 获取 tag message 内容并转义 Markdown 特殊字符
  194. TAG_MESSAGE='${{ steps.tag_message.outputs.message }}'
  195. TAG_MESSAGE=$(echo "$TAG_MESSAGE" | sed 's/\([_*`\[]\)/\\\1/g')
  196. # 限制消息长度(Telegram 消息限制 4096 字符,预留空间给头尾固定内容)
  197. if [ ${#TAG_MESSAGE} -gt 3500 ]; then
  198. TAG_MESSAGE="${TAG_MESSAGE:0:3500}..."
  199. fi
  200. # 构建消息内容
  201. MESSAGE="🚀 *Sub2API 新版本发布!*"$'\n'$'\n'
  202. MESSAGE+="📦 版本号: \`${VERSION}\`"$'\n'$'\n'
  203. # 添加更新内容
  204. if [ -n "$TAG_MESSAGE" ]; then
  205. MESSAGE+="${TAG_MESSAGE}"$'\n'$'\n'
  206. fi
  207. MESSAGE+="🐳 *Docker 部署:*"$'\n'
  208. MESSAGE+="\`\`\`bash"$'\n'
  209. # 根据是否配置 DockerHub 动态生成
  210. if [ -n "$DOCKERHUB_USERNAME" ]; then
  211. DOCKER_IMAGE="${DOCKERHUB_USERNAME}/sub2api"
  212. MESSAGE+="# Docker Hub"$'\n'
  213. MESSAGE+="docker pull ${DOCKER_IMAGE}:${VERSION}"$'\n'
  214. MESSAGE+="# GitHub Container Registry"$'\n'
  215. fi
  216. MESSAGE+="docker pull ${GHCR_IMAGE}:${VERSION}"$'\n'
  217. MESSAGE+="\`\`\`"$'\n'$'\n'
  218. MESSAGE+="🔗 *相关链接:*"$'\n'
  219. MESSAGE+="• [GitHub Release](https://github.com/${REPO}/releases/tag/${TAG_NAME})"$'\n'
  220. if [ -n "$DOCKERHUB_USERNAME" ]; then
  221. MESSAGE+="• [Docker Hub](https://hub.docker.com/r/${DOCKER_IMAGE})"$'\n'
  222. fi
  223. MESSAGE+="• [GitHub Packages](https://github.com/${REPO}/pkgs/container/sub2api)"$'\n'$'\n'
  224. MESSAGE+="#Sub2API #Release #${TAG_NAME//./_}"
  225. # 发送消息
  226. curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
  227. -H "Content-Type: application/json" \
  228. -d "$(jq -n \
  229. --arg chat_id "${TELEGRAM_CHAT_ID}" \
  230. --arg text "${MESSAGE}" \
  231. '{
  232. chat_id: $chat_id,
  233. text: $text,
  234. parse_mode: "Markdown",
  235. disable_web_page_preview: true
  236. }')"
  237. sync-version-file:
  238. needs: [release]
  239. if: ${{ needs.release.result == 'success' }}
  240. runs-on: ubuntu-latest
  241. steps:
  242. - name: Checkout default branch
  243. uses: actions/checkout@v6
  244. with:
  245. ref: ${{ github.event.repository.default_branch }}
  246. - name: Sync VERSION file to released tag
  247. run: |
  248. if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
  249. VERSION=${{ github.event.inputs.tag }}
  250. VERSION=${VERSION#v}
  251. else
  252. VERSION=${GITHUB_REF#refs/tags/v}
  253. fi
  254. CURRENT_VERSION=$(tr -d '\r\n' < backend/cmd/server/VERSION || true)
  255. if [ "$CURRENT_VERSION" = "$VERSION" ]; then
  256. echo "VERSION file already matches $VERSION"
  257. exit 0
  258. fi
  259. echo "$VERSION" > backend/cmd/server/VERSION
  260. git config user.name "github-actions[bot]"
  261. git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
  262. git add backend/cmd/server/VERSION
  263. git commit -m "chore: sync VERSION to ${VERSION} [skip ci]"
  264. git push origin HEAD:${{ github.event.repository.default_branch }}