Skip to content

perf(jsplugin): 插件商店拉取结果缓存 5 分钟,翻页搜索不再重拉注册表 #562

perf(jsplugin): 插件商店拉取结果缓存 5 分钟,翻页搜索不再重拉注册表

perf(jsplugin): 插件商店拉取结果缓存 5 分钟,翻页搜索不再重拉注册表 #562

Workflow file for this run

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