perf(jsplugin): 插件商店拉取结果缓存 5 分钟,翻页搜索不再重拉注册表 #562
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| push: | |
| tags: | |
| - 'v*' # 自动发正式版(songloft/songloft:X.Y.Z / :latest / :X.Y.Z-full / :full + 写 CHANGELOG) | |
| branches: | |
| - main # push 到 main 自动打测试包(songloft/songloft:dev / :dev-full + dev 的 prerelease) | |
| paths-ignore: | |
| - 'docs/**' | |
| - '**.md' | |
| - 'LICENSE' | |
| - '.gitignore' | |
| # HA 加载项子模块(裸路径 = gitlink 本身,带 /** 匹配不上)。 | |
| # 加载项不参与后端构建,bump 它的指针不该触发一整轮 dev release。 | |
| - 'home-assistant-addon' | |
| workflow_dispatch: # 手动触发同样走 dev 路径 | |
| env: | |
| GO_VERSION: '1.26' | |
| # 原生 bundle(桌面/移动)统一 stable 3.44.6(下方各 bundle job 复用此变量)。 | |
| # web 前端(build-frontend job)单独用 beta 3.47:含引擎修复 #185116 | |
| # (flutter/flutter#184683,切后台 WebGL context 丢失致 LateInitializationError | |
| # 崩溃/白屏),该修复尚未进 stable;待进 stable 后回退与 bundle 统一。 | |
| FLUTTER_VERSION: '3.44.6' | |
| DOCKER_USERNAME: songloft | |
| IMAGE_NAME: songloft | |
| RELEASE_REPO: ${{ github.repository }} | |
| jobs: | |
| # ============================================================ | |
| # Job 1: 解析版本号与发布类型,获取上一个 tag | |
| # ============================================================ | |
| prepare: | |
| name: Prepare | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.parse.outputs.version }} | |
| version_tag: ${{ steps.parse.outputs.version_tag }} | |
| is_prerelease: ${{ steps.parse.outputs.is_prerelease }} | |
| release_title: ${{ steps.parse.outputs.release_title }} | |
| previous_tag: ${{ steps.prev-tag.outputs.previous_tag }} | |
| build_time: ${{ steps.parse.outputs.build_time }} | |
| git_commit: ${{ steps.parse.outputs.git_commit }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # 获取完整历史以查找上一个 tag | |
| - name: Parse version info | |
| id: parse | |
| run: | | |
| if [[ "${{ github.ref_type }}" == "tag" ]]; then | |
| # tag 触发:v1.2.4 → 正式发布 | |
| VERSION="${{ github.ref_name }}" # v1.2.4 | |
| VERSION_TAG="${VERSION#v}" # 1.2.4 | |
| IS_PRERELEASE="false" | |
| RELEASE_TITLE="Release ${VERSION}" | |
| else | |
| # workflow_dispatch(任意分支触发):开发版统一用 dev 标识 | |
| VERSION="dev" | |
| VERSION_TAG="dev" | |
| IS_PRERELEASE="true" | |
| TIMESTAMP=$(date -u '+%Y-%m-%d %H:%M:%S') | |
| RELEASE_TITLE="Development Build dev from ${{ github.ref_name }} (${TIMESTAMP} UTC)" | |
| fi | |
| BUILD_TIME=$(date -u '+%Y-%m-%d_%H:%M:%S') | |
| GIT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown") | |
| echo "version=${VERSION}" >> $GITHUB_OUTPUT | |
| echo "version_tag=${VERSION_TAG}" >> $GITHUB_OUTPUT | |
| echo "is_prerelease=${IS_PRERELEASE}" >> $GITHUB_OUTPUT | |
| echo "release_title=${RELEASE_TITLE}" >> $GITHUB_OUTPUT | |
| echo "build_time=${BUILD_TIME}" >> $GITHUB_OUTPUT | |
| echo "git_commit=${GIT_COMMIT}" >> $GITHUB_OUTPUT | |
| echo "Version: ${VERSION}" | |
| echo "Version Tag: ${VERSION_TAG}" | |
| echo "Is Prerelease: ${IS_PRERELEASE}" | |
| echo "Build Time: ${BUILD_TIME}" | |
| echo "Git Commit: ${GIT_COMMIT}" | |
| echo "Release Title: ${RELEASE_TITLE}" | |
| - name: Get previous tag | |
| id: prev-tag | |
| run: | | |
| CURRENT_TAG="${{ steps.parse.outputs.version }}" | |
| HEAD_SHA=$(git rev-parse HEAD) | |
| if [[ "$CURRENT_TAG" == "dev" ]]; then | |
| # dev 构建:排除指向 HEAD 同一 commit 的 tag,防止 fromTag==toTag 导致 changelog 为空报错 | |
| PREV_TAG="" | |
| for tag in $(git --no-pager tag --sort=-creatordate --merged HEAD | grep '^v'); do | |
| TAG_SHA=$(git rev-list -n1 "$tag") | |
| if [[ "$TAG_SHA" != "$HEAD_SHA" ]]; then | |
| PREV_TAG="$tag" | |
| break | |
| fi | |
| done | |
| else | |
| # release 构建:按名称排除当前 tag | |
| PREV_TAG=$(git --no-pager tag --sort=creatordate --merged HEAD | grep '^v' | grep -v "^${CURRENT_TAG}$" | tail -1) | |
| fi | |
| if [ -z "$PREV_TAG" ]; then | |
| PREV_TAG=$(git rev-list --max-parents=0 HEAD) | |
| echo "No previous tag found, using first commit: ${PREV_TAG}" | |
| else | |
| echo "Previous tag: ${PREV_TAG}" | |
| fi | |
| echo "previous_tag=${PREV_TAG}" >> $GITHUB_OUTPUT | |
| # ============================================================ | |
| # Job 2: 构建 Flutter Web(嵌入模式) | |
| # ============================================================ | |
| build-frontend: | |
| name: Build Frontend | |
| needs: [prepare] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: false | |
| - name: Init songloft-player submodule | |
| run: git submodule update --init --recursive -- songloft-player | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| flutter-version: 3.47.0-0.1.pre | |
| channel: beta | |
| cache: true | |
| - name: Install brotli | |
| run: sudo apt-get install -y -qq brotli | |
| - name: Flutter pub get | |
| run: | | |
| cd songloft-player | |
| flutter pub get | |
| - name: Build Flutter Web (embedded mode) | |
| env: | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| FRONTEND_VERSION: ${{ needs.prepare.outputs.version_tag }} | |
| FRONTEND_BUILD_TIME: ${{ needs.prepare.outputs.build_time }} | |
| BUILD_TIME: ${{ needs.prepare.outputs.build_time }} | |
| run: make build-frontend-web-embedded | |
| - name: Upload frontend artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: frontend-web-embedded | |
| path: songloft-player-build/web-embedded | |
| retention-days: 1 | |
| # ============================================================ | |
| # Job 3: 多平台交叉编译(lite + full) | |
| # ============================================================ | |
| build-binaries: | |
| name: Build ${{ matrix.goos }}/${{ matrix.goarch }}${{ matrix.goarm && format('/v{0}', matrix.goarm) || '' }} | |
| needs: [prepare, build-frontend] | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - goos: linux | |
| goarch: amd64 | |
| suffix: linux-amd64 | |
| gen_version_json: true | |
| - goos: linux | |
| goarch: arm64 | |
| suffix: linux-arm64 | |
| - goos: linux | |
| goarch: arm | |
| goarm: '7' | |
| suffix: linux-armv7 | |
| - goos: darwin | |
| goarch: amd64 | |
| suffix: darwin-amd64 | |
| - goos: darwin | |
| goarch: arm64 | |
| suffix: darwin-arm64 | |
| - goos: windows | |
| goarch: amd64 | |
| suffix: windows-amd64 | |
| ext: .exe | |
| - goos: windows | |
| goarch: arm64 | |
| suffix: windows-arm64 | |
| ext: .exe | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: false | |
| - name: Init pkg/tag submodule | |
| run: git submodule update --init --recursive -- pkg/tag | |
| - name: Download frontend artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: frontend-web-embedded | |
| path: songloft-player-build/web-embedded | |
| - name: Setup Go | |
| uses: actions/setup-go@v5 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| cache: true | |
| - name: Download Go dependencies | |
| run: go mod download | |
| - name: Install UPX | |
| run: | | |
| sudo apt-get update -qq | |
| sudo apt-get install -y -qq upx | |
| - name: Build binaries (full + lite) | |
| env: | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| TRACELY_APP_ID: ${{ secrets.TRACELY_APP_ID }} | |
| TRACELY_APP_SECRET: ${{ secrets.TRACELY_APP_SECRET }} | |
| TRACELY_HOST: ${{ secrets.TRACELY_HOST }} | |
| run: | | |
| SUFFIX="${{ matrix.suffix }}" | |
| EXT="${{ matrix.ext }}" | |
| # VERSION=dev 时 Makefile 自动加 -tags dev(Swagger + pprof) | |
| make build-cross GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} \ | |
| GOARM=${{ matrix.goarm }} OUTPUT="songloft-${SUFFIX}${EXT}" | |
| make build-cross GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} \ | |
| GOARM=${{ matrix.goarm }} OUTPUT="songloft-${SUFFIX}-lite${EXT}" \ | |
| EXTRA_TAGS=lite BUILD_TYPE=lite | |
| ls -lh songloft-${SUFFIX}* | |
| - name: Generate version.json | |
| if: matrix.gen_version_json == true | |
| env: | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| BUILD_TIME: ${{ needs.prepare.outputs.build_time }} | |
| GIT_COMMIT: ${{ needs.prepare.outputs.git_commit }} | |
| run: | | |
| cat > version.json <<EOF | |
| { | |
| "version": "${VERSION}", | |
| "git_commit": "${GIT_COMMIT}", | |
| "build_time": "${BUILD_TIME}", | |
| "download_url_prefix": "https://github.com/${{ env.RELEASE_REPO }}/releases/download/${VERSION}/songloft", | |
| "release_notes": "Release ${VERSION}" | |
| } | |
| EOF | |
| cat version.json | |
| - name: Upload binary artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: binary-${{ matrix.suffix }} | |
| path: | | |
| songloft-${{ matrix.suffix }}${{ matrix.ext }} | |
| songloft-${{ matrix.suffix }}-lite${{ matrix.ext }} | |
| version.json | |
| retention-days: 1 | |
| # ============================================================ | |
| # Job 4: Docker 多架构镜像构建、推送并导出 tar 包 | |
| # ============================================================ | |
| docker-build-push: | |
| name: Docker Build & Push | |
| needs: [prepare, build-frontend] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: false | |
| - name: Init pkg/tag submodule | |
| run: git submodule update --init --recursive -- pkg/tag | |
| - name: Download frontend artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: frontend-web-embedded | |
| path: songloft-player-build/web-embedded | |
| - name: Set up QEMU | |
| uses: docker/setup-qemu-action@v3 | |
| with: | |
| platforms: arm64,arm | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| with: | |
| driver-opts: network=host | |
| - name: Login to Docker Hub | |
| uses: docker/login-action@v3 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Set build metadata | |
| id: meta | |
| env: | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| VERSION_TAG: ${{ needs.prepare.outputs.version_tag }} | |
| IS_PRERELEASE: ${{ needs.prepare.outputs.is_prerelease }} | |
| run: | | |
| GIT_COMMIT="${{ needs.prepare.outputs.git_commit }}" | |
| BUILD_TIME="${{ needs.prepare.outputs.build_time }}" | |
| echo "GIT_COMMIT=${GIT_COMMIT}" >> $GITHUB_OUTPUT | |
| echo "BUILD_TIME=${BUILD_TIME}" >> $GITHUB_OUTPUT | |
| # 计算 Docker 标签(默认 = 完整版,-lite = 精简版) | |
| if [[ "${IS_PRERELEASE}" == "false" ]]; then | |
| # tag 触发:完整版用 :版本号 + :latest,精简版用 :版本号-lite + :lite | |
| FULL_TAGS="${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:${VERSION_TAG},${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:latest" | |
| LITE_TAGS="${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:${VERSION_TAG}-lite,${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:lite" | |
| else | |
| # dispatch:仅打分支名标签 | |
| FULL_TAGS="${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:${VERSION_TAG}" | |
| LITE_TAGS="${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:${VERSION_TAG}-lite" | |
| fi | |
| echo "FULL_TAGS=${FULL_TAGS}" >> $GITHUB_OUTPUT | |
| echo "LITE_TAGS=${LITE_TAGS}" >> $GITHUB_OUTPUT | |
| echo "Full tags: ${FULL_TAGS}" | |
| echo "Lite tags: ${LITE_TAGS}" | |
| - name: Create output directory | |
| run: mkdir -p docker-tars | |
| # ---------- 逐平台构建完整版并导出 tar(写 GHA cache,下面 manifest 步骤复用)---------- | |
| - name: Build & save full linux/amd64 | |
| run: | | |
| docker buildx build \ | |
| --build-arg VERSION="${{ needs.prepare.outputs.version }}" \ | |
| --build-arg GIT_COMMIT="${{ steps.meta.outputs.GIT_COMMIT }}" \ | |
| --build-arg BUILD_TIME="${{ steps.meta.outputs.BUILD_TIME }}" \ | |
| --build-arg TRACELY_APP_ID="${{ secrets.TRACELY_APP_ID }}" \ | |
| --build-arg TRACELY_APP_SECRET="${{ secrets.TRACELY_APP_SECRET }}" \ | |
| --build-arg TRACELY_HOST="${{ secrets.TRACELY_HOST }}" \ | |
| --platform linux/amd64 \ | |
| --tag ${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:tmp-full-amd64 \ | |
| --cache-from type=gha,scope=full-linux-amd64 \ | |
| --cache-from type=registry,ref=${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:latest \ | |
| --cache-from type=registry,ref=${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:dev \ | |
| --cache-to type=gha,scope=full-linux-amd64,mode=max \ | |
| --load \ | |
| . | |
| docker save ${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:tmp-full-amd64 \ | |
| -o docker-tars/songloft-docker-linux-amd64.tar | |
| docker rmi ${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:tmp-full-amd64 2>/dev/null || true | |
| - name: Build & save full linux/arm64 | |
| run: | | |
| docker buildx build \ | |
| --build-arg VERSION="${{ needs.prepare.outputs.version }}" \ | |
| --build-arg GIT_COMMIT="${{ steps.meta.outputs.GIT_COMMIT }}" \ | |
| --build-arg BUILD_TIME="${{ steps.meta.outputs.BUILD_TIME }}" \ | |
| --build-arg TRACELY_APP_ID="${{ secrets.TRACELY_APP_ID }}" \ | |
| --build-arg TRACELY_APP_SECRET="${{ secrets.TRACELY_APP_SECRET }}" \ | |
| --build-arg TRACELY_HOST="${{ secrets.TRACELY_HOST }}" \ | |
| --platform linux/arm64 \ | |
| --tag ${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:tmp-full-arm64 \ | |
| --cache-from type=gha,scope=full-linux-arm64 \ | |
| --cache-from type=registry,ref=${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:latest \ | |
| --cache-from type=registry,ref=${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:dev \ | |
| --cache-to type=gha,scope=full-linux-arm64,mode=max \ | |
| --load \ | |
| . | |
| docker save ${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:tmp-full-arm64 \ | |
| -o docker-tars/songloft-docker-linux-arm64.tar | |
| docker rmi ${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:tmp-full-arm64 2>/dev/null || true | |
| - name: Build & save full linux/arm/v7 | |
| run: | | |
| docker buildx build \ | |
| --build-arg VERSION="${{ needs.prepare.outputs.version }}" \ | |
| --build-arg GIT_COMMIT="${{ steps.meta.outputs.GIT_COMMIT }}" \ | |
| --build-arg BUILD_TIME="${{ steps.meta.outputs.BUILD_TIME }}" \ | |
| --build-arg TRACELY_APP_ID="${{ secrets.TRACELY_APP_ID }}" \ | |
| --build-arg TRACELY_APP_SECRET="${{ secrets.TRACELY_APP_SECRET }}" \ | |
| --build-arg TRACELY_HOST="${{ secrets.TRACELY_HOST }}" \ | |
| --platform linux/arm/v7 \ | |
| --tag ${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:tmp-full-armv7 \ | |
| --cache-from type=gha,scope=full-linux-armv7 \ | |
| --cache-from type=registry,ref=${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:latest \ | |
| --cache-from type=registry,ref=${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:dev \ | |
| --cache-to type=gha,scope=full-linux-armv7,mode=max \ | |
| --load \ | |
| . | |
| docker save ${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:tmp-full-armv7 \ | |
| -o docker-tars/songloft-docker-linux-arm-v7.tar | |
| docker rmi ${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:tmp-full-armv7 2>/dev/null || true | |
| # ---------- 推送完整版多架构 manifest(命中上面的 GHA cache,无需重新编译)---------- | |
| - name: Push full multi-arch manifest | |
| run: | | |
| IFS=',' read -ra TAGS <<< "${{ steps.meta.outputs.FULL_TAGS }}" | |
| TAG_ARGS="" | |
| for tag in "${TAGS[@]}"; do | |
| TAG_ARGS="${TAG_ARGS} -t ${tag}" | |
| done | |
| docker buildx build ${TAG_ARGS} \ | |
| --build-arg VERSION="${{ needs.prepare.outputs.version }}" \ | |
| --build-arg GIT_COMMIT="${{ steps.meta.outputs.GIT_COMMIT }}" \ | |
| --build-arg BUILD_TIME="${{ steps.meta.outputs.BUILD_TIME }}" \ | |
| --build-arg TRACELY_APP_ID="${{ secrets.TRACELY_APP_ID }}" \ | |
| --build-arg TRACELY_APP_SECRET="${{ secrets.TRACELY_APP_SECRET }}" \ | |
| --build-arg TRACELY_HOST="${{ secrets.TRACELY_HOST }}" \ | |
| --platform linux/amd64,linux/arm64,linux/arm/v7 \ | |
| --cache-from type=gha,scope=full-linux-amd64 \ | |
| --cache-from type=gha,scope=full-linux-arm64 \ | |
| --cache-from type=gha,scope=full-linux-armv7 \ | |
| --cache-from type=registry,ref=${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:latest \ | |
| --cache-from type=registry,ref=${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:dev \ | |
| --cache-to type=inline \ | |
| --push \ | |
| . | |
| # ---------- 逐平台构建精简版并导出 tar ---------- | |
| - name: Build & save lite linux/amd64 | |
| run: | | |
| docker buildx build \ | |
| --build-arg VERSION="${{ needs.prepare.outputs.version }}" \ | |
| --build-arg GIT_COMMIT="${{ steps.meta.outputs.GIT_COMMIT }}" \ | |
| --build-arg BUILD_TIME="${{ steps.meta.outputs.BUILD_TIME }}" \ | |
| --build-arg TRACELY_APP_ID="${{ secrets.TRACELY_APP_ID }}" \ | |
| --build-arg TRACELY_APP_SECRET="${{ secrets.TRACELY_APP_SECRET }}" \ | |
| --build-arg TRACELY_HOST="${{ secrets.TRACELY_HOST }}" \ | |
| --build-arg LITE_BUILD=true \ | |
| --platform linux/amd64 \ | |
| --tag ${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:tmp-lite-amd64 \ | |
| --cache-from type=gha,scope=lite-linux-amd64 \ | |
| --cache-from type=registry,ref=${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:lite \ | |
| --cache-from type=registry,ref=${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:dev-lite \ | |
| --cache-to type=gha,scope=lite-linux-amd64,mode=max \ | |
| --load \ | |
| . | |
| docker save ${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:tmp-lite-amd64 \ | |
| -o docker-tars/songloft-docker-linux-amd64-lite.tar | |
| docker rmi ${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:tmp-lite-amd64 2>/dev/null || true | |
| - name: Build & save lite linux/arm64 | |
| run: | | |
| docker buildx build \ | |
| --build-arg VERSION="${{ needs.prepare.outputs.version }}" \ | |
| --build-arg GIT_COMMIT="${{ steps.meta.outputs.GIT_COMMIT }}" \ | |
| --build-arg BUILD_TIME="${{ steps.meta.outputs.BUILD_TIME }}" \ | |
| --build-arg TRACELY_APP_ID="${{ secrets.TRACELY_APP_ID }}" \ | |
| --build-arg TRACELY_APP_SECRET="${{ secrets.TRACELY_APP_SECRET }}" \ | |
| --build-arg TRACELY_HOST="${{ secrets.TRACELY_HOST }}" \ | |
| --build-arg LITE_BUILD=true \ | |
| --platform linux/arm64 \ | |
| --tag ${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:tmp-lite-arm64 \ | |
| --cache-from type=gha,scope=lite-linux-arm64 \ | |
| --cache-from type=registry,ref=${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:lite \ | |
| --cache-from type=registry,ref=${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:dev-lite \ | |
| --cache-to type=gha,scope=lite-linux-arm64,mode=max \ | |
| --load \ | |
| . | |
| docker save ${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:tmp-lite-arm64 \ | |
| -o docker-tars/songloft-docker-linux-arm64-lite.tar | |
| docker rmi ${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:tmp-lite-arm64 2>/dev/null || true | |
| - name: Build & save lite linux/arm/v7 | |
| run: | | |
| docker buildx build \ | |
| --build-arg VERSION="${{ needs.prepare.outputs.version }}" \ | |
| --build-arg GIT_COMMIT="${{ steps.meta.outputs.GIT_COMMIT }}" \ | |
| --build-arg BUILD_TIME="${{ steps.meta.outputs.BUILD_TIME }}" \ | |
| --build-arg TRACELY_APP_ID="${{ secrets.TRACELY_APP_ID }}" \ | |
| --build-arg TRACELY_APP_SECRET="${{ secrets.TRACELY_APP_SECRET }}" \ | |
| --build-arg TRACELY_HOST="${{ secrets.TRACELY_HOST }}" \ | |
| --build-arg LITE_BUILD=true \ | |
| --platform linux/arm/v7 \ | |
| --tag ${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:tmp-lite-armv7 \ | |
| --cache-from type=gha,scope=lite-linux-armv7 \ | |
| --cache-from type=registry,ref=${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:lite \ | |
| --cache-from type=registry,ref=${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:dev-lite \ | |
| --cache-to type=gha,scope=lite-linux-armv7,mode=max \ | |
| --load \ | |
| . | |
| docker save ${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:tmp-lite-armv7 \ | |
| -o docker-tars/songloft-docker-linux-arm-v7-lite.tar | |
| docker rmi ${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:tmp-lite-armv7 2>/dev/null || true | |
| # ---------- 推送精简版多架构 manifest(命中上面的 GHA cache)---------- | |
| - name: Push lite multi-arch manifest | |
| run: | | |
| IFS=',' read -ra TAGS <<< "${{ steps.meta.outputs.LITE_TAGS }}" | |
| TAG_ARGS="" | |
| for tag in "${TAGS[@]}"; do | |
| TAG_ARGS="${TAG_ARGS} -t ${tag}" | |
| done | |
| docker buildx build ${TAG_ARGS} \ | |
| --build-arg VERSION="${{ needs.prepare.outputs.version }}" \ | |
| --build-arg GIT_COMMIT="${{ steps.meta.outputs.GIT_COMMIT }}" \ | |
| --build-arg BUILD_TIME="${{ steps.meta.outputs.BUILD_TIME }}" \ | |
| --build-arg TRACELY_APP_ID="${{ secrets.TRACELY_APP_ID }}" \ | |
| --build-arg TRACELY_APP_SECRET="${{ secrets.TRACELY_APP_SECRET }}" \ | |
| --build-arg TRACELY_HOST="${{ secrets.TRACELY_HOST }}" \ | |
| --build-arg LITE_BUILD=true \ | |
| --platform linux/amd64,linux/arm64,linux/arm/v7 \ | |
| --cache-from type=gha,scope=lite-linux-amd64 \ | |
| --cache-from type=gha,scope=lite-linux-arm64 \ | |
| --cache-from type=gha,scope=lite-linux-armv7 \ | |
| --cache-from type=registry,ref=${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:lite \ | |
| --cache-from type=registry,ref=${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:dev-lite \ | |
| --cache-to type=inline \ | |
| --push \ | |
| . | |
| - name: List docker tars | |
| run: ls -lh docker-tars/ | |
| - name: Upload Docker tar artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: docker-tars | |
| path: docker-tars/ | |
| retention-days: 1 | |
| # ============================================================ | |
| # Job 5-8: 含后端的客户端构建(bundled client) | |
| # 将 Go 后端嵌入 Flutter 客户端,用户无需单独部署服务端 | |
| # ============================================================ | |
| build-bundled-android: | |
| name: Bundled Android | |
| needs: [prepare] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: false | |
| - name: Init submodules | |
| run: | | |
| git submodule update --init --recursive -- songloft-player | |
| git submodule update --init --recursive -- pkg/tag | |
| - name: Setup Go | |
| uses: actions/setup-go@v5 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| cache: true | |
| - name: Setup Java | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: '17' | |
| - name: Install gomobile | |
| run: | | |
| go install golang.org/x/mobile/cmd/gomobile@latest | |
| gomobile init | |
| - name: Build Go backend as Android .aar | |
| env: | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| TRACELY_APP_ID: ${{ secrets.TRACELY_APP_ID }} | |
| TRACELY_APP_SECRET: ${{ secrets.TRACELY_APP_SECRET }} | |
| TRACELY_HOST: ${{ secrets.TRACELY_HOST }} | |
| run: make build-go-mobile-android | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| flutter-version: ${{ env.FLUTTER_VERSION }} | |
| channel: stable | |
| cache: true | |
| - name: Flutter pub get | |
| run: | | |
| cd songloft-player | |
| flutter pub get | |
| # 原生契约哈希:pub get 后(GeneratedPluginRegistrant/.flutter-plugins-dependencies | |
| # 已生成)、build 前计算并写 asset,同一组值稍后写进 manifest(同源同值)。 | |
| - name: Compute native contract hash (bake asset) | |
| run: | | |
| cd songloft-player | |
| HASHES=$(./scripts/compute_native_contract.sh \ | |
| --export-surface ../mobile/export_surface.txt \ | |
| --write-asset android/app/src/main/assets/native_contract.json) | |
| echo "native_contract.json = $HASHES" | |
| DART_CONTRACT_HASH=$(printf '%s' "$HASHES" | sed -n 's/.*"dart":"\([a-f0-9]*\)".*/\1/p') | |
| GO_CONTRACT_HASH=$(printf '%s' "$HASHES" | sed -n 's/.*"go":"\([a-f0-9]*\)".*/\1/p') | |
| echo "DART_CONTRACT_HASH=$DART_CONTRACT_HASH" >> "$GITHUB_ENV" | |
| echo "GO_CONTRACT_HASH=$GO_CONTRACT_HASH" >> "$GITHUB_ENV" | |
| - name: Setup Android signing | |
| env: | |
| ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} | |
| run: | | |
| if [ -n "$ANDROID_KEYSTORE_BASE64" ]; then | |
| echo "$ANDROID_KEYSTORE_BASE64" | base64 --decode > $RUNNER_TEMP/release.jks | |
| echo "ANDROID_KEYSTORE_PATH=$RUNNER_TEMP/release.jks" >> $GITHUB_ENV | |
| fi | |
| - name: Build APK with embedded backend | |
| env: | |
| ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} | |
| ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} | |
| ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} | |
| run: | | |
| cd songloft-player | |
| flutter build apk --release --split-per-abi \ | |
| --dart-define=HAS_BACKEND=true \ | |
| --dart-define=FRONTEND_VERSION=${{ needs.prepare.outputs.version_tag }} \ | |
| --dart-define=FRONTEND_BUILD_TIME=${{ needs.prepare.outputs.build_time }} \ | |
| --dart-define=FRONTEND_GIT_COMMIT=${{ needs.prepare.outputs.git_commit }} \ | |
| --dart-define=FLUTTER_BINDING=${{ env.FLUTTER_VERSION }} | |
| - name: Collect APKs | |
| run: | | |
| cd songloft-player | |
| cp build/app/outputs/flutter-apk/app-arm64-v8a-release.apk "$GITHUB_WORKSPACE/songloft-bundled-android-arm64-v8a.apk" | |
| cp build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk "$GITHUB_WORKSPACE/songloft-bundled-android-armeabi-v7a.apk" | |
| cp build/app/outputs/flutter-apk/app-x86_64-release.apk "$GITHUB_WORKSPACE/songloft-bundled-android-x86_64.apk" | |
| # 无基线热更:每次发版自动产出本渠道最新的前端补丁(libapp.so)+ 后端补丁 | |
| # (libgojni.so),随 release 上传;客户端查本渠道最新即更新。覆盖 arm64-v8a/ | |
| # armeabi-v7a/x86_64(gomobile bind 含 android/amd64 目标,x86_64 供模拟器验证)。 | |
| # 详见 songloft-player/docs/cn/backend_hotupdate.md。 | |
| - name: Guard mobile export surface (frozen) | |
| run: | | |
| go doc ./mobile | grep '^func ' | sort > /tmp/current_surface.txt | |
| sort mobile/export_surface.txt > /tmp/expected_surface.txt | |
| if ! diff -u /tmp/expected_surface.txt /tmp/current_surface.txt; then | |
| echo "::error::mobile 包导出面已变更!只换 libgojni.so 不安全(须连 DEX/整包一起发)。若确为有意变更,请同步更新 mobile/export_surface.txt。" | |
| exit 1 | |
| fi | |
| echo "导出面守卫通过。" | |
| - name: Produce hot-update patch assets (frontend zip + backend so) | |
| env: | |
| REPO: ${{ env.RELEASE_REPO }} | |
| TAG: ${{ needs.prepare.outputs.version }} | |
| SEMVER: ${{ needs.prepare.outputs.version_tag }} | |
| GIT_COMMIT: ${{ needs.prepare.outputs.git_commit }} | |
| BUILD_TIME: ${{ needs.prepare.outputs.build_time }} | |
| FLUTTER_BINDING: ${{ env.FLUTTER_VERSION }} | |
| run: | | |
| set -e | |
| cd songloft-player | |
| # --split-per-abi 时 gradle 会把各 ABI APK 的 versionCode 改写为 | |
| # 「ABI偏移×1000+pubspec基础值」(如 arm64-v8a=2001),flutter_patcher 按 | |
| # versionCode 严格绑定,manifest/补丁必须写 APK 的**真实值**(用 aapt 读), | |
| # 写 pubspec 裸值会让客户端 versionCode 闸恒不通过。 | |
| BUILD_TOOLS_DIR=$(ls -d "$ANDROID_HOME"/build-tools/* | sort -V | tail -1) | |
| for abi in arm64-v8a armeabi-v7a x86_64; do | |
| apk="build/app/outputs/flutter-apk/app-${abi}-release.apk" | |
| [ -f "$apk" ] || { echo "跳过 $abi(无 APK)"; continue; } | |
| ABI_VC=$("$BUILD_TOOLS_DIR/aapt" dump badging "$apk" | sed -n "s/.*versionCode='\([0-9]*\)'.*/\1/p" | head -1) | |
| [ -n "$ABI_VC" ] || { echo "::error::无法从 $apk 读取 versionCode"; exit 1; } | |
| LABEL="${SEMVER}-${GIT_COMMIT}" | |
| # 前端补丁:libapp.so 打成 patch.zip + 扩展 manifest | |
| rm -rf dist | |
| dart run flutter_patcher:pack --apk "$apk" --version "$LABEL" \ | |
| --target-version-code "$ABI_VC" --abi "$abi" | |
| cp dist/patch.zip "$GITHUB_WORKSPACE/patch-${abi}.zip" | |
| ZIP_MD5=$(md5sum "$GITHUB_WORKSPACE/patch-${abi}.zip" | cut -d' ' -f1) | |
| cat > "$GITHUB_WORKSPACE/manifest-${abi}.json" <<EOF | |
| { | |
| "hasUpdate": true, | |
| "patch": { | |
| "version": "${LABEL}", | |
| "semanticVersion": "${SEMVER}", | |
| "gitCommit": "${GIT_COMMIT}", | |
| "flutterBinding": "${FLUTTER_BINDING}", | |
| "contractHash": "${DART_CONTRACT_HASH}", | |
| "targetVersionCode": ${ABI_VC}, | |
| "patchUrl": "https://github.com/${REPO}/releases/download/${TAG}/patch-${abi}.zip", | |
| "md5": "${ZIP_MD5}" | |
| } | |
| } | |
| EOF | |
| # 后端补丁:从 APK 抽 libgojni.so + 校验 SONAME + manifest | |
| unzip -p "$apk" "lib/${abi}/libgojni.so" > "$GITHUB_WORKSPACE/libgojni-${abi}.so" | |
| # gomobile 产物**无 DT_SONAME**(正常),bionic 回退用文件 basename;客户端落地 | |
| # 文件名固定 libgojni.so,故去重仍生效。仅当存在**非空且非 libgojni.so** 的 | |
| # soname 时才会破坏去重 → 才失败。 | |
| SONAME=$(readelf -d "$GITHUB_WORKSPACE/libgojni-${abi}.so" | sed -n 's/.*SONAME.*\[\(.*\)\].*/\1/p') | |
| if [ -n "$SONAME" ] && [ "$SONAME" != "libgojni.so" ]; then | |
| echo "::error::libgojni-${abi}.so 的 DT_SONAME=[$SONAME](非空且非 libgojni.so),预加载去重将失效" | |
| exit 1 | |
| fi | |
| echo "soname 校验通过(soname=[$SONAME],空则走 basename 回退)" | |
| SO_MD5=$(md5sum "$GITHUB_WORKSPACE/libgojni-${abi}.so" | cut -d' ' -f1) | |
| SO_SIZE=$(stat -c%s "$GITHUB_WORKSPACE/libgojni-${abi}.so") | |
| cat > "$GITHUB_WORKSPACE/backend-manifest-${abi}.json" <<EOF | |
| { | |
| "hasUpdate": true, | |
| "backend": { | |
| "abi": "${abi}", | |
| "version": "${TAG}", | |
| "gitCommit": "${GIT_COMMIT}", | |
| "buildTime": "${BUILD_TIME}", | |
| "soUrl": "https://github.com/${REPO}/releases/download/${TAG}/libgojni-${abi}.so", | |
| "md5": "${SO_MD5}", | |
| "contractHash": "${GO_CONTRACT_HASH}", | |
| "size": ${SO_SIZE} | |
| } | |
| } | |
| EOF | |
| echo "补丁 $abi: soname=$SONAME so_md5=$SO_MD5 zip_md5=$ZIP_MD5" | |
| done | |
| - name: Upload artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: bundled-android | |
| path: | | |
| songloft-bundled-android-*.apk | |
| manifest-*.json | |
| patch-*.zip | |
| backend-manifest-*.json | |
| libgojni-*.so | |
| retention-days: 1 | |
| build-bundled-linux: | |
| name: Bundled Linux | |
| needs: [prepare] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: false | |
| - name: Init submodules | |
| run: | | |
| git submodule update --init --recursive -- songloft-player | |
| git submodule update --init --recursive -- pkg/tag | |
| - name: Setup Go | |
| uses: actions/setup-go@v5 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| cache: true | |
| - name: Build Go backend for Linux | |
| env: | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| TRACELY_APP_ID: ${{ secrets.TRACELY_APP_ID }} | |
| TRACELY_APP_SECRET: ${{ secrets.TRACELY_APP_SECRET }} | |
| TRACELY_HOST: ${{ secrets.TRACELY_HOST }} | |
| run: make build-go-desktop-linux | |
| - name: Install Linux dependencies | |
| run: | | |
| sudo apt-get update -y | |
| sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev libglu1-mesa libstdc++-12-dev libsecret-1-dev libayatana-appindicator3-dev | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| flutter-version: ${{ env.FLUTTER_VERSION }} | |
| channel: stable | |
| cache: true | |
| - name: Flutter pub get | |
| run: | | |
| cd songloft-player | |
| flutter pub get | |
| - name: Build Flutter Linux with embedded backend | |
| run: | | |
| cd songloft-player | |
| flutter build linux --release \ | |
| --dart-define=HAS_BACKEND=true \ | |
| --dart-define=FRONTEND_VERSION=${{ needs.prepare.outputs.version_tag }} \ | |
| --dart-define=FRONTEND_BUILD_TIME=${{ needs.prepare.outputs.build_time }} \ | |
| --dart-define=FRONTEND_GIT_COMMIT=${{ needs.prepare.outputs.git_commit }} | |
| - name: Package tar.gz | |
| run: | | |
| cd songloft-player/build/linux/x64/release/bundle | |
| tar czf "$GITHUB_WORKSPACE/songloft-bundled-linux-x64.tar.gz" . | |
| - name: Upload artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: bundled-linux | |
| path: songloft-bundled-linux-x64.tar.gz | |
| retention-days: 1 | |
| build-bundled-apple: | |
| name: Bundled macOS + iOS | |
| needs: [prepare] | |
| runs-on: macos-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: false | |
| - name: Init submodules | |
| run: | | |
| git submodule update --init --recursive -- songloft-player | |
| git submodule update --init --recursive -- pkg/tag | |
| - name: Setup Go | |
| uses: actions/setup-go@v5 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| cache: true | |
| - name: Build Go backend for macOS (arm64) | |
| env: | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| TRACELY_APP_ID: ${{ secrets.TRACELY_APP_ID }} | |
| TRACELY_APP_SECRET: ${{ secrets.TRACELY_APP_SECRET }} | |
| TRACELY_HOST: ${{ secrets.TRACELY_HOST }} | |
| run: make build-go-desktop-macos-arm64 | |
| - name: Install gomobile (for iOS) | |
| run: | | |
| go install golang.org/x/mobile/cmd/gomobile@latest | |
| gomobile init | |
| - name: Build Go backend as iOS .xcframework | |
| env: | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| TRACELY_APP_ID: ${{ secrets.TRACELY_APP_ID }} | |
| TRACELY_APP_SECRET: ${{ secrets.TRACELY_APP_SECRET }} | |
| TRACELY_HOST: ${{ secrets.TRACELY_HOST }} | |
| run: make build-go-mobile-ios | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| flutter-version: ${{ env.FLUTTER_VERSION }} | |
| channel: stable | |
| cache: true | |
| - name: Flutter pub get | |
| run: | | |
| cd songloft-player | |
| flutter pub get | |
| - name: Build Flutter macOS with embedded backend | |
| run: | | |
| cd songloft-player | |
| flutter build macos --release \ | |
| --dart-define=HAS_BACKEND=true \ | |
| --dart-define=FRONTEND_VERSION=${{ needs.prepare.outputs.version_tag }} \ | |
| --dart-define=FRONTEND_BUILD_TIME=${{ needs.prepare.outputs.build_time }} \ | |
| --dart-define=FRONTEND_GIT_COMMIT=${{ needs.prepare.outputs.git_commit }} | |
| - name: Finalize macOS app bundle | |
| run: | | |
| cd songloft-player | |
| sign_identity="${MACOS_CODESIGN_IDENTITY:--}" | |
| for app_dir in build/macos/Build/Products/Release/*.app; do | |
| if [ ! -x "$app_dir/Contents/MacOS/songloft-server" ]; then | |
| cp macos/Runner/songloft-server "$app_dir/Contents/MacOS/songloft-server" | |
| chmod +x "$app_dir/Contents/MacOS/songloft-server" | |
| # 先深度签名整个 app,再单独给子进程签 inherit entitlements; | |
| # 顺序不能反:--deep 重签会覆盖子进程 entitlements,导致沙盒下无 | |
| # network.server 权限,songloft-server 无法 bind,本地模式报错。 | |
| codesign --force --deep --sign "$sign_identity" --entitlements macos/Runner/Release.entitlements "$app_dir" | |
| codesign --force --sign "$sign_identity" --entitlements macos/Runner/GoBackend.entitlements "$app_dir/Contents/MacOS/songloft-server" | |
| fi | |
| codesign --verify --deep --strict --verbose=2 "$app_dir" | |
| done | |
| - name: Package macOS .app as ZIP | |
| run: | | |
| cd songloft-player/build/macos/Build/Products/Release | |
| app_dir=$(find . -maxdepth 1 -name "*.app" -type d -print -quit) | |
| test -n "$app_dir" | |
| ditto -c -k --sequesterRsrc --keepParent "${app_dir#./}" "$GITHUB_WORKSPACE/songloft-bundled-macos.zip" | |
| - name: Build Flutter iOS with embedded backend (no codesign) | |
| run: | | |
| cd songloft-player | |
| flutter build ios --release --no-codesign \ | |
| --dart-define=HAS_BACKEND=true \ | |
| --dart-define=FRONTEND_VERSION=${{ needs.prepare.outputs.version_tag }} \ | |
| --dart-define=FRONTEND_BUILD_TIME=${{ needs.prepare.outputs.build_time }} \ | |
| --dart-define=FRONTEND_GIT_COMMIT=${{ needs.prepare.outputs.git_commit }} | |
| - name: Package iOS as IPA | |
| run: | | |
| cd songloft-player | |
| test ! -e build/ios/iphoneos/Runner.app/Frameworks/Songloft.framework | |
| nm build/ios/iphoneos/Runner.app/Runner | grep -q 'MobileStart' | |
| mkdir -p Payload | |
| cp -r build/ios/iphoneos/Runner.app Payload/ | |
| zip -r -y "$GITHUB_WORKSPACE/songloft-bundled-ios-nosign.ipa" Payload | |
| rm -rf Payload | |
| - name: Upload artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: bundled-apple | |
| path: | | |
| songloft-bundled-macos.zip | |
| songloft-bundled-ios-nosign.ipa | |
| retention-days: 1 | |
| build-bundled-windows: | |
| name: Bundled Windows | |
| needs: [prepare] | |
| runs-on: windows-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: false | |
| - name: Init submodules | |
| run: | | |
| git submodule update --init --recursive -- songloft-player | |
| git submodule update --init --recursive -- pkg/tag | |
| - name: Setup Go | |
| uses: actions/setup-go@v5 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| cache: true | |
| - name: Build Go backend for Windows | |
| env: | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| TRACELY_APP_ID: ${{ secrets.TRACELY_APP_ID }} | |
| TRACELY_APP_SECRET: ${{ secrets.TRACELY_APP_SECRET }} | |
| TRACELY_HOST: ${{ secrets.TRACELY_HOST }} | |
| shell: bash | |
| run: | | |
| GIT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown") | |
| BUILD_TIME=$(date -u '+%Y-%m-%d_%H:%M:%S') | |
| CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -v -tags lite \ | |
| -ldflags="-s -w -X songloft/internal/version.Version=${VERSION} -X songloft/internal/version.GitCommit=${GIT_COMMIT} -X songloft/internal/version.BuildTime=${BUILD_TIME} -X songloft/internal/version.BuildType=lite" \ | |
| -o songloft-player/windows/runner/songloft-server.exe . | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| flutter-version: ${{ env.FLUTTER_VERSION }} | |
| channel: stable | |
| cache: true | |
| - name: Setup NuGet | |
| uses: nuget/setup-nuget@v2 | |
| - name: Flutter pub get | |
| run: | | |
| cd songloft-player | |
| flutter pub get | |
| - name: Build Flutter Windows with embedded backend | |
| run: | | |
| cd songloft-player | |
| flutter build windows --release --dart-define=HAS_BACKEND=true --dart-define=FRONTEND_VERSION=${{ needs.prepare.outputs.version_tag }} --dart-define=FRONTEND_BUILD_TIME=${{ needs.prepare.outputs.build_time }} --dart-define=FRONTEND_GIT_COMMIT=${{ needs.prepare.outputs.git_commit }} | |
| - name: Package ZIP | |
| shell: pwsh | |
| run: | | |
| Compress-Archive -Path songloft-player\build\windows\x64\runner\Release\* -DestinationPath songloft-bundled-windows-x64.zip | |
| - name: Upload artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: bundled-windows | |
| path: songloft-bundled-windows-x64.zip | |
| retention-days: 1 | |
| # ============================================================ | |
| # Job 9: 生成 Changelog 并发布 Release | |
| # ============================================================ | |
| create-release: | |
| name: Create Release | |
| needs: [prepare, build-binaries, docker-build-push, build-bundled-android, build-bundled-linux, build-bundled-apple, build-bundled-windows] | |
| # 即使 bundled 构建失败,只要核心 job 成功就发布 release | |
| if: | | |
| always() && | |
| needs.prepare.result == 'success' && | |
| needs.build-binaries.result == 'success' && | |
| needs.docker-build-push.result == 'success' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout main | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # 获取完整历史与所有 tag 用于 changelog 生成 | |
| ref: main # 直接在 main 上工作,便于 changelog-action 写入 CHANGELOG.md 后 commit/push | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Download all binary artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: binary-* | |
| path: release-files | |
| merge-multiple: true | |
| - name: Download Docker tar artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: docker-tars | |
| path: release-files | |
| - name: Download bundled Android artifacts | |
| if: needs.build-bundled-android.result == 'success' | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: bundled-android | |
| path: release-files | |
| - name: Download bundled Linux artifacts | |
| if: needs.build-bundled-linux.result == 'success' | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: bundled-linux | |
| path: release-files | |
| - name: Download bundled Apple artifacts | |
| if: needs.build-bundled-apple.result == 'success' | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: bundled-apple | |
| path: release-files | |
| - name: Download bundled Windows artifacts | |
| if: needs.build-bundled-windows.result == 'success' | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: bundled-windows | |
| path: release-files | |
| - name: Generate checksums | |
| run: | | |
| cd release-files | |
| # 对所有文件(二进制 + Docker tar 包)生成 sha256 校验和 | |
| sha256sum $(ls | grep -v checksums.txt) > checksums.txt | |
| echo "=== checksums.txt ===" | |
| cat checksums.txt | |
| - name: List all release files | |
| run: | | |
| echo "=== Release files ===" | |
| ls -lh release-files/ | |
| - name: Generate Changelog from Conventional Commits | |
| id: changelog | |
| continue-on-error: true | |
| uses: requarks/changelog-action@v1 | |
| with: | |
| token: ${{ github.token }} | |
| fromTag: ${{ startsWith(github.ref, 'refs/tags/') && needs.prepare.outputs.version || github.sha }} | |
| toTag: ${{ needs.prepare.outputs.previous_tag }} | |
| writeToFile: ${{ needs.prepare.outputs.is_prerelease == 'false' }} | |
| changelogFilePath: CHANGELOG.md | |
| useGitmojis: true | |
| includeInvalidCommits: false | |
| restrictToTypes: feat,fix,docs,style,refactor,perf,test,build,ci,chore,revert | |
| - name: Create GitHub Release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| RELEASE_TITLE: ${{ needs.prepare.outputs.release_title }} | |
| IS_PRERELEASE: ${{ needs.prepare.outputs.is_prerelease }} | |
| run: | | |
| cd release-files | |
| # 收集所有文件 | |
| FILES=($(ls)) | |
| echo "Uploading ${#FILES[@]} files..." | |
| # 检查 release 是否已存在,存在则删除 | |
| if gh release view "${VERSION}" --repo "${{ env.RELEASE_REPO }}" > /dev/null 2>&1; then | |
| echo "Release ${VERSION} already exists, deleting..." | |
| gh release delete "${VERSION}" --repo "${{ env.RELEASE_REPO }}" --yes | |
| fi | |
| # 构建 Release Notes(用 heredoc 避免 changelog 内容被 shell 展开执行) | |
| RELEASE_NOTES=$(cat <<'NOTES_EOF' | |
| ${{ steps.changelog.outputs.changes }} | |
| NOTES_EOF | |
| ) | |
| RELEASE_NOTES=$(echo "$RELEASE_NOTES" | sed '/^[[:space:]]*$/d') | |
| # 如果 changelog 为空,使用默认内容 | |
| if [ -z "$RELEASE_NOTES" ]; then | |
| if [[ "${IS_PRERELEASE}" == "true" ]]; then | |
| RELEASE_NOTES="Development build from branch: ${{ github.ref_name }}" | |
| else | |
| RELEASE_NOTES="Release ${VERSION}" | |
| fi | |
| fi | |
| # 创建 Release | |
| if [[ "${IS_PRERELEASE}" == "true" ]]; then | |
| gh release create "${VERSION}" \ | |
| --repo "${{ env.RELEASE_REPO }}" \ | |
| --title "${RELEASE_TITLE}" \ | |
| --notes "${RELEASE_NOTES}" \ | |
| --prerelease \ | |
| "${FILES[@]}" | |
| else | |
| gh release create "${VERSION}" \ | |
| --repo "${{ env.RELEASE_REPO }}" \ | |
| --title "${RELEASE_TITLE}" \ | |
| --notes "${RELEASE_NOTES}" \ | |
| --latest \ | |
| "${FILES[@]}" | |
| fi | |
| echo "Release created: https://github.com/${{ env.RELEASE_REPO }}/releases/tag/${VERSION}" | |
| # ============================================================ | |
| # 仅 stable:把 changelog-action 写入的 CHANGELOG.md commit 回 main | |
| # (checkout 阶段已经直接在 main 分支上工作,无需切分支) | |
| # ============================================================ | |
| - name: Commit CHANGELOG.md to main | |
| if: needs.prepare.outputs.is_prerelease == 'false' | |
| env: | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| if git diff --quiet -- CHANGELOG.md; then | |
| echo "CHANGELOG.md 无变更,跳过 commit" | |
| exit 0 | |
| fi | |
| git add CHANGELOG.md | |
| git commit -m "docs: update CHANGELOG for ${VERSION}" | |
| git push origin main |