mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 03:52:26 +08:00
409 lines
16 KiB
YAML
409 lines
16 KiB
YAML
name: Release
|
|
|
|
on:
|
|
push:
|
|
tags:
|
|
- 'v*'
|
|
workflow_dispatch:
|
|
inputs:
|
|
tag:
|
|
description: 'Tag to release (or a branch/ref for dry run)'
|
|
required: true
|
|
type: string
|
|
simple_release:
|
|
description: 'Simple release: only x86_64 GHCR image, skip other artifacts'
|
|
required: false
|
|
type: boolean
|
|
default: false
|
|
dry_run:
|
|
description: 'Build and verify all selected artifacts without publishing'
|
|
required: false
|
|
type: boolean
|
|
default: false
|
|
|
|
concurrency:
|
|
group: release-${{ github.repository }}
|
|
cancel-in-progress: false
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
env:
|
|
SIMPLE_RELEASE: ${{ inputs.simple_release == true || vars.SIMPLE_RELEASE == 'true' }}
|
|
DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run == true }}
|
|
|
|
jobs:
|
|
prepare:
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
sha: ${{ steps.plan.outputs.sha }}
|
|
owner_lower: ${{ steps.plan.outputs.owner_lower }}
|
|
tag: ${{ steps.plan.outputs.tag }}
|
|
version: ${{ steps.plan.outputs.version }}
|
|
date: ${{ steps.plan.outputs.date }}
|
|
matrix: ${{ steps.plan.outputs.matrix }}
|
|
simple: ${{ steps.plan.outputs.simple }}
|
|
dry_run: ${{ steps.plan.outputs.dry_run }}
|
|
steps:
|
|
- name: Checkout workflow tooling
|
|
uses: actions/checkout@v6
|
|
with:
|
|
ref: ${{ github.sha }}
|
|
- name: Preserve tooling for releases of older tags
|
|
run: cp -R .github/release-tools "$RUNNER_TEMP/release-tools"
|
|
- name: Checkout selected application source
|
|
uses: actions/checkout@v6
|
|
with:
|
|
ref: ${{ inputs.tag || github.ref }}
|
|
fetch-depth: 0
|
|
- uses: actions/setup-python@v6
|
|
with:
|
|
python-version: '3.12'
|
|
- run: python -m pip install -r "$RUNNER_TEMP/release-tools/requirements-release.txt"
|
|
- name: Resolve one source commit and target matrix
|
|
id: plan
|
|
env:
|
|
REQUESTED_REF: ${{ inputs.tag || github.ref_name }}
|
|
run: |
|
|
args=(--ref "$REQUESTED_REF")
|
|
if [[ "$SIMPLE_RELEASE" == true ]]; then args+=(--simple); fi
|
|
if [[ "$DRY_RUN" == true ]]; then args+=(--dry-run); fi
|
|
python "$RUNNER_TEMP/release-tools/release_matrix.py" plan "${args[@]}"
|
|
- uses: actions/upload-artifact@v7
|
|
with:
|
|
name: release-tooling
|
|
path: ${{ runner.temp }}/release-tools/
|
|
retention-days: 1
|
|
- uses: actions/upload-artifact@v7
|
|
with:
|
|
name: version-file
|
|
path: backend/cmd/server/VERSION
|
|
retention-days: 1
|
|
|
|
build-frontend:
|
|
needs: prepare
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
with:
|
|
ref: ${{ needs.prepare.outputs.sha }}
|
|
- uses: pnpm/action-setup@v6
|
|
with:
|
|
version: 9
|
|
- uses: actions/setup-node@v6
|
|
with:
|
|
node-version: '20'
|
|
cache: pnpm
|
|
cache-dependency-path: frontend/pnpm-lock.yaml
|
|
- run: pnpm install --frozen-lockfile
|
|
working-directory: frontend
|
|
- run: pnpm run build
|
|
working-directory: frontend
|
|
- uses: actions/upload-artifact@v7
|
|
with:
|
|
name: frontend-dist
|
|
path: backend/internal/web/dist/
|
|
retention-days: 1
|
|
|
|
build-binaries:
|
|
needs: [prepare, build-frontend]
|
|
runs-on: ubuntu-latest
|
|
strategy:
|
|
fail-fast: false
|
|
matrix: ${{ fromJSON(needs.prepare.outputs.matrix) }}
|
|
env:
|
|
RELEASE_VERSION: ${{ needs.prepare.outputs.version }}
|
|
RELEASE_DATE: ${{ needs.prepare.outputs.date }}
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
with:
|
|
ref: ${{ needs.prepare.outputs.sha }}
|
|
fetch-depth: 0
|
|
- uses: actions/download-artifact@v8
|
|
with:
|
|
name: release-tooling
|
|
path: ${{ runner.temp }}/release-tools/
|
|
- uses: actions/download-artifact@v8
|
|
with:
|
|
name: version-file
|
|
path: backend/cmd/server/
|
|
- uses: actions/download-artifact@v8
|
|
with:
|
|
name: frontend-dist
|
|
path: backend/internal/web/dist/
|
|
- uses: actions/setup-go@v6
|
|
with:
|
|
go-version-file: backend/go.mod
|
|
check-latest: false
|
|
cache: false
|
|
- name: Verify Go version
|
|
run: go version | grep -q 'go1.27.2'
|
|
- name: Restore target-specific Go cache
|
|
id: go-cache
|
|
uses: actions/cache/restore@v6
|
|
with:
|
|
path: |
|
|
~/go/pkg/mod
|
|
~/.cache/go-build
|
|
key: release-go-${{ runner.os }}-${{ runner.arch }}-${{ matrix.goos }}-${{ matrix.goarch }}-${{ hashFiles('backend/go.mod', 'backend/go.sum') }}-${{ needs.prepare.outputs.sha }}
|
|
restore-keys: |
|
|
release-go-${{ runner.os }}-${{ runner.arch }}-${{ matrix.goos }}-${{ matrix.goarch }}-${{ hashFiles('backend/go.mod', 'backend/go.sum') }}-
|
|
release-go-${{ runner.os }}-${{ runner.arch }}-${{ matrix.goos }}-${{ matrix.goarch }}-
|
|
- uses: actions/setup-python@v6
|
|
with:
|
|
python-version: '3.12'
|
|
- run: python -m pip install -r "$RUNNER_TEMP/release-tools/requirements-release.txt"
|
|
- name: Select one GoReleaser target
|
|
env:
|
|
TARGET_OS: ${{ matrix.goos }}
|
|
TARGET_ARCH: ${{ matrix.goarch }}
|
|
run: python "$RUNNER_TEMP/release-tools/release_matrix.py" config build --goos "$TARGET_OS" --goarch "$TARGET_ARCH" --output "$RUNNER_TEMP/goreleaser.matrix.yaml"
|
|
- name: Build and archive without publishing
|
|
uses: goreleaser/goreleaser-action@v7
|
|
with:
|
|
version: '~> v2'
|
|
args: release --snapshot --clean --skip=validate,publish,docker,announce --config=${{ runner.temp }}/goreleaser.matrix.yaml
|
|
env:
|
|
GITHUB_REPO_OWNER: ${{ github.repository_owner }}
|
|
GITHUB_REPO_OWNER_LOWER: ${{ needs.prepare.outputs.owner_lower }}
|
|
GITHUB_REPO_NAME: ${{ github.event.repository.name }}
|
|
DOCKERHUB_USERNAME: skip
|
|
TAG_MESSAGE: ''
|
|
- name: Verify and collect the target archive
|
|
env:
|
|
TARGET_OS: ${{ matrix.goos }}
|
|
TARGET_ARCH: ${{ matrix.goarch }}
|
|
RELEASE_SHA: ${{ needs.prepare.outputs.sha }}
|
|
run: python "$RUNNER_TEMP/release-tools/release_matrix.py" collect --version "$RELEASE_VERSION" --sha "$RELEASE_SHA" --goos "$TARGET_OS" --goarch "$TARGET_ARCH" --output matrix-output
|
|
- uses: actions/upload-artifact@v7
|
|
with:
|
|
name: release-binary-${{ matrix.goos }}-${{ matrix.goarch }}
|
|
path: matrix-output/
|
|
if-no-files-found: error
|
|
retention-days: 1
|
|
- name: Save updated target cache
|
|
if: ${{ steps.go-cache.outputs.cache-hit != 'true' }}
|
|
uses: actions/cache/save@v6
|
|
with:
|
|
path: |
|
|
~/go/pkg/mod
|
|
~/.cache/go-build
|
|
key: ${{ steps.go-cache.outputs.cache-primary-key }}
|
|
|
|
release:
|
|
needs: [prepare, build-binaries]
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write
|
|
packages: write
|
|
env:
|
|
SIMPLE_RELEASE: ${{ needs.prepare.outputs.simple }}
|
|
DRY_RUN: ${{ needs.prepare.outputs.dry_run }}
|
|
RELEASE_TAG: ${{ needs.prepare.outputs.tag }}
|
|
RELEASE_VERSION: ${{ needs.prepare.outputs.version }}
|
|
RELEASE_SHA: ${{ needs.prepare.outputs.sha }}
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
with:
|
|
ref: ${{ needs.prepare.outputs.sha }}
|
|
fetch-depth: 0
|
|
- uses: actions/download-artifact@v8
|
|
with:
|
|
name: release-tooling
|
|
path: ${{ runner.temp }}/release-tools/
|
|
- uses: actions/download-artifact@v8
|
|
with:
|
|
pattern: release-binary-*
|
|
path: release-input
|
|
merge-multiple: true
|
|
- uses: actions/setup-python@v6
|
|
with:
|
|
python-version: '3.12'
|
|
- run: python -m pip install -r "$RUNNER_TEMP/release-tools/requirements-release.txt"
|
|
- name: Verify complete matrix and prepare Docker contexts
|
|
run: |
|
|
args=(--version "$RELEASE_VERSION" --sha "$RELEASE_SHA" --input release-input --output .release-context)
|
|
if [[ "$SIMPLE_RELEASE" == true ]]; then args+=(--simple); fi
|
|
python "$RUNNER_TEMP/release-tools/release_matrix.py" contexts "${args[@]}"
|
|
- uses: actions/setup-go@v6
|
|
with:
|
|
go-version-file: backend/go.mod
|
|
cache: false
|
|
- name: Set up QEMU
|
|
uses: docker/setup-qemu-action@v3
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@v3
|
|
- name: Login to DockerHub
|
|
if: ${{ env.DRY_RUN != 'true' && env.SIMPLE_RELEASE != 'true' && env.DOCKERHUB_USERNAME != '' }}
|
|
uses: docker/login-action@v3
|
|
env:
|
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
with:
|
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
- name: Login to GitHub Container Registry
|
|
if: ${{ env.DRY_RUN != 'true' }}
|
|
uses: docker/login-action@v3
|
|
with:
|
|
registry: ghcr.io
|
|
username: ${{ github.repository_owner }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
- name: Build images and publish manifests
|
|
env:
|
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME || 'skip' }}
|
|
run: bash "$RUNNER_TEMP/release-tools/release-images.sh"
|
|
- name: Get annotated tag notes
|
|
id: tag_message
|
|
run: |
|
|
TAG_MESSAGE=$(git tag -l --format='%(contents:body)' "$RELEASE_TAG")
|
|
DELIMITER="TAG_MESSAGE_$(openssl rand -hex 16)"
|
|
{
|
|
echo "message<<$DELIMITER"
|
|
echo "$TAG_MESSAGE"
|
|
echo "$DELIMITER"
|
|
} >> "$GITHUB_OUTPUT"
|
|
- name: Prepare publication-only GoReleaser config
|
|
run: |
|
|
args=(publish --output "$RUNNER_TEMP/goreleaser.publisher.yaml")
|
|
if [[ "$SIMPLE_RELEASE" == true ]]; then args+=(--simple); fi
|
|
python "$RUNNER_TEMP/release-tools/release_matrix.py" config "${args[@]}"
|
|
- name: Publish existing archives and release notes
|
|
uses: goreleaser/goreleaser-action@v7
|
|
with:
|
|
version: '~> v2'
|
|
args: release --clean --skip=validate,before,docker ${{ env.DRY_RUN == 'true' && '--snapshot --skip=publish,announce' || '' }} --config=${{ runner.temp }}/goreleaser.publisher.yaml
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
GORELEASER_CURRENT_TAG: ${{ needs.prepare.outputs.tag }}
|
|
TAG_MESSAGE: ${{ steps.tag_message.outputs.message }}
|
|
GITHUB_REPO_OWNER: ${{ github.repository_owner }}
|
|
GITHUB_REPO_NAME: ${{ github.event.repository.name }}
|
|
GITHUB_REPO_OWNER_LOWER: ${{ needs.prepare.outputs.owner_lower }}
|
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME || 'skip' }}
|
|
- name: Upload dry-run checksums and metadata
|
|
if: ${{ env.DRY_RUN == 'true' }}
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: release-dry-run-report
|
|
path: |
|
|
dist/checksums.txt
|
|
dist/artifacts.json
|
|
dist/metadata.json
|
|
if-no-files-found: error
|
|
retention-days: 3
|
|
|
|
# Update DockerHub description
|
|
- name: Update DockerHub description
|
|
if: ${{ env.DRY_RUN != 'true' && env.SIMPLE_RELEASE != 'true' && env.DOCKERHUB_USERNAME != '' }}
|
|
uses: peter-evans/dockerhub-description@v5
|
|
env:
|
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
with:
|
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
repository: ${{ secrets.DOCKERHUB_USERNAME }}/sub2api
|
|
short-description: "Sub2API - AI API Gateway Platform"
|
|
readme-filepath: ./deploy/DOCKER.md
|
|
|
|
# Send Telegram notification
|
|
- name: Send Telegram Notification
|
|
if: ${{ env.DRY_RUN != 'true' && env.SIMPLE_RELEASE != 'true' }}
|
|
env:
|
|
TAG_MESSAGE: ${{ steps.tag_message.outputs.message }}
|
|
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
|
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
continue-on-error: true
|
|
run: |
|
|
# 检查必要的环境变量
|
|
if [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; then
|
|
echo "Telegram credentials not configured, skipping notification"
|
|
exit 0
|
|
fi
|
|
|
|
TAG_NAME="$RELEASE_TAG"
|
|
VERSION=${TAG_NAME#v}
|
|
REPO="${{ github.repository }}"
|
|
GHCR_IMAGE="ghcr.io/${REPO,,}" # ${,,} converts to lowercase
|
|
|
|
# 获取 tag message 内容并转义 Markdown 特殊字符
|
|
TAG_MESSAGE=$(echo "$TAG_MESSAGE" | sed 's/\([_*`\[]\)/\\\1/g')
|
|
|
|
# 限制消息长度(Telegram 消息限制 4096 字符,预留空间给头尾固定内容)
|
|
if [ ${#TAG_MESSAGE} -gt 3500 ]; then
|
|
TAG_MESSAGE="${TAG_MESSAGE:0:3500}..."
|
|
fi
|
|
|
|
# 构建消息内容
|
|
MESSAGE="🚀 *Sub2API 新版本发布!*"$'\n'$'\n'
|
|
MESSAGE+="📦 版本号: \`${VERSION}\`"$'\n'$'\n'
|
|
|
|
# 添加更新内容
|
|
if [ -n "$TAG_MESSAGE" ]; then
|
|
MESSAGE+="${TAG_MESSAGE}"$'\n'$'\n'
|
|
fi
|
|
|
|
MESSAGE+="🐳 *Docker 部署:*"$'\n'
|
|
MESSAGE+="\`\`\`bash"$'\n'
|
|
# 根据是否配置 DockerHub 动态生成
|
|
if [ -n "$DOCKERHUB_USERNAME" ]; then
|
|
DOCKER_IMAGE="${DOCKERHUB_USERNAME}/sub2api"
|
|
MESSAGE+="# Docker Hub"$'\n'
|
|
MESSAGE+="docker pull ${DOCKER_IMAGE}:${VERSION}"$'\n'
|
|
MESSAGE+="# GitHub Container Registry"$'\n'
|
|
fi
|
|
MESSAGE+="docker pull ${GHCR_IMAGE}:${VERSION}"$'\n'
|
|
MESSAGE+="\`\`\`"$'\n'$'\n'
|
|
MESSAGE+="🔗 *相关链接:*"$'\n'
|
|
MESSAGE+="• [GitHub Release](https://github.com/${REPO}/releases/tag/${TAG_NAME})"$'\n'
|
|
if [ -n "$DOCKERHUB_USERNAME" ]; then
|
|
MESSAGE+="• [Docker Hub](https://hub.docker.com/r/${DOCKER_IMAGE})"$'\n'
|
|
fi
|
|
MESSAGE+="• [GitHub Packages](https://github.com/${REPO}/pkgs/container/sub2api)"$'\n'$'\n'
|
|
MESSAGE+="#Sub2API #Release #${TAG_NAME//./_}"
|
|
|
|
# 发送消息
|
|
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$(jq -n \
|
|
--arg chat_id "${TELEGRAM_CHAT_ID}" \
|
|
--arg text "${MESSAGE}" \
|
|
'{
|
|
chat_id: $chat_id,
|
|
text: $text,
|
|
parse_mode: "Markdown",
|
|
disable_web_page_preview: true
|
|
}')"
|
|
|
|
sync-version-file:
|
|
needs: [prepare, release]
|
|
if: ${{ needs.release.result == 'success' && needs.prepare.outputs.dry_run != 'true' }}
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write
|
|
env:
|
|
RELEASE_VERSION: ${{ needs.prepare.outputs.version }}
|
|
steps:
|
|
- name: Checkout default branch
|
|
uses: actions/checkout@v6
|
|
with:
|
|
ref: ${{ github.event.repository.default_branch }}
|
|
|
|
- name: Sync VERSION file to released tag
|
|
run: |
|
|
VERSION="$RELEASE_VERSION"
|
|
|
|
CURRENT_VERSION=$(tr -d '\r\n' < backend/cmd/server/VERSION || true)
|
|
if [ "$CURRENT_VERSION" = "$VERSION" ]; then
|
|
echo "VERSION file already matches $VERSION"
|
|
exit 0
|
|
fi
|
|
|
|
echo "$VERSION" > backend/cmd/server/VERSION
|
|
|
|
git config user.name "github-actions[bot]"
|
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
git add backend/cmd/server/VERSION
|
|
git commit -m "chore: sync VERSION to ${VERSION} [skip ci]"
|
|
git push origin HEAD:${{ github.event.repository.default_branch }}
|