fix(player): 播放时保持屏幕常亮,修复 TV 屏保/息屏 (#37) #50
Workflow file for this run
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*" | |
| - "[0-9]*" | |
| workflow_dispatch: | |
| inputs: | |
| tag_name: | |
| description: "Release tag (手动运行可自定义,留空则使用 manual-<short_sha>)" | |
| required: false | |
| type: string | |
| release_name: | |
| description: "Release 名称(可选,默认与 tag 相同)" | |
| required: false | |
| type: string | |
| prerelease: | |
| description: "是否标记为 Pre-release(手动运行默认 true)" | |
| required: false | |
| default: false | |
| type: boolean | |
| create_release: | |
| description: "是否创建 GitHub Release(手动运行可设为 false,仅构建与产出 Actions Artifacts)" | |
| required: false | |
| default: true | |
| type: boolean | |
| permissions: | |
| contents: write | |
| env: | |
| FLUTTER_VERSION: "3.41.9" | |
| jobs: | |
| android: | |
| name: Android (APK + AAB) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Prepare release metadata | |
| id: meta | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| IS_TAG="false" | |
| TAG_NAME="" | |
| if [[ "${GITHUB_REF:-}" == refs/tags/* ]]; then | |
| IS_TAG="true" | |
| TAG_NAME="${GITHUB_REF#refs/tags/}" | |
| fi | |
| if [[ -z "$TAG_NAME" ]]; then | |
| INPUT_TAG="${{ github.event.inputs.tag_name || '' }}" | |
| if [[ -n "$INPUT_TAG" ]]; then | |
| TAG_NAME="$INPUT_TAG" | |
| else | |
| SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7) | |
| TAG_NAME="manual-${SHORT_SHA}" | |
| fi | |
| fi | |
| echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT | |
| - name: Setup Java 17 | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: "zulu" | |
| java-version: "17" | |
| - name: Setup Android SDK | |
| uses: android-actions/setup-android@v3 | |
| - name: Install Android build-tools and platforms | |
| run: | | |
| sdkmanager --install "platform-tools" "platforms;android-35" "build-tools;35.0.0" | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| flutter-version: ${{ env.FLUTTER_VERSION }} | |
| channel: "stable" | |
| cache: true | |
| - name: Update pubspec.yaml version | |
| run: | | |
| VERSION='${{ steps.meta.outputs.tag_name }}' | |
| echo "Updating pubspec.yaml version to: $VERSION" | |
| # 去掉 v 前缀 | |
| CLEAN_VERSION=${VERSION#v} | |
| # 使用 GitHub run number 作为构建号 | |
| BUILD_NUMBER=${{ github.run_number }} | |
| # 更新 pubspec.yaml | |
| sed -i "s/^version: .*/version: ${CLEAN_VERSION}+${BUILD_NUMBER}/" pubspec.yaml | |
| echo "Updated version in pubspec.yaml:" | |
| grep "^version:" pubspec.yaml | |
| - name: Flutter pub get | |
| run: flutter pub get | |
| - name: Setup Android signing from secrets (optional) | |
| env: | |
| ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} | |
| ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} | |
| ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} | |
| ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} | |
| run: | | |
| set -euo pipefail | |
| if [ -n "${ANDROID_KEYSTORE_BASE64:-}" ]; then | |
| echo "Writing Android keystore from secret..." | |
| echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > android/app/ci-release.keystore | |
| { | |
| echo "storeFile=ci-release.keystore" | |
| echo "storePassword=${ANDROID_KEYSTORE_PASSWORD}" | |
| echo "keyAlias=${ANDROID_KEY_ALIAS:-xplayer}" | |
| echo "keyPassword=${ANDROID_KEY_PASSWORD}" | |
| } > android/key.properties | |
| echo "key.properties created (release signing enabled)." | |
| else | |
| echo "No ANDROID_KEYSTORE_BASE64 secret; release will fall back to debug signing." | |
| fi | |
| - name: Build split APKs (release) | |
| run: flutter build apk --release -PabiSplit | |
| - name: Build App Bundle (AAB) | |
| run: flutter build appbundle --release | |
| - name: Rename Android artifacts | |
| run: | | |
| set -euo pipefail | |
| TAG='${{ steps.meta.outputs.tag_name }}' | |
| VERSION="${TAG#v}" | |
| APKDIR=build/app/outputs/flutter-apk | |
| OUT=dist-android | |
| mkdir -p "$OUT" | |
| # 按 ABI 拆分的瘦包:arm64-v8a 作主包(不带后缀),其余带后缀 | |
| for abi in arm64-v8a armeabi-v7a x86_64; do | |
| f="$APKDIR/app-${abi}-release.apk" | |
| [ -f "$f" ] || continue | |
| if [ "$abi" = "arm64-v8a" ]; then | |
| cp "$f" "$OUT/xplayer-${VERSION}.apk" | |
| else | |
| cp "$f" "$OUT/xplayer-${VERSION}-${abi}.apk" | |
| fi | |
| done | |
| # 通用兜底包:universalApk 经 Flutter 输出为 flutter-apk/app-release.apk(含全部 ABI) | |
| [ -f "$APKDIR/app-release.apk" ] && cp "$APKDIR/app-release.apk" "$OUT/xplayer-${VERSION}-universal.apk" | |
| AAB=build/app/outputs/bundle/release/app-release.aab | |
| [ -f "$AAB" ] && cp "$AAB" "$OUT/xplayer-${VERSION}.aab" | |
| echo "== renamed artifacts ==" && ls -la "$OUT" | |
| - name: Upload Android artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: android | |
| if-no-files-found: error | |
| path: dist-android/* | |
| windows: | |
| name: Windows (installer + portable) | |
| # 钉到 windows-2022:windows-latest 已滚到更新镜像,Flutter 3.27.3 在其上会误选 | |
| # VS2019 生成器导致构建失败(2025-10 旧镜像上还是好的)。保留 continue-on-error 兜底。 | |
| runs-on: windows-2022 | |
| continue-on-error: true | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Prepare release metadata | |
| id: meta | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| IS_TAG="false" | |
| TAG_NAME="" | |
| if [[ "${GITHUB_REF:-}" == refs/tags/* ]]; then | |
| IS_TAG="true" | |
| TAG_NAME="${GITHUB_REF#refs/tags/}" | |
| fi | |
| if [[ -z "$TAG_NAME" ]]; then | |
| INPUT_TAG="${{ github.event.inputs.tag_name || '' }}" | |
| if [[ -n "$INPUT_TAG" ]]; then | |
| TAG_NAME="$INPUT_TAG" | |
| else | |
| SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7) | |
| TAG_NAME="manual-${SHORT_SHA}" | |
| fi | |
| fi | |
| echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| flutter-version: ${{ env.FLUTTER_VERSION }} | |
| channel: "stable" | |
| cache: true | |
| - name: Enable Windows desktop | |
| run: flutter config --enable-windows-desktop | |
| - name: Update pubspec.yaml version | |
| shell: bash | |
| run: | | |
| VERSION='${{ steps.meta.outputs.tag_name }}' | |
| echo "Updating pubspec.yaml version to: $VERSION" | |
| CLEAN_VERSION=${VERSION#v} | |
| BUILD_NUMBER=${{ github.run_number }} | |
| sed -i "s/^version: .*/version: ${CLEAN_VERSION}+${BUILD_NUMBER}/" pubspec.yaml | |
| echo "Updated version in pubspec.yaml:" | |
| grep "^version:" pubspec.yaml | |
| - name: Flutter pub get | |
| run: flutter pub get | |
| - name: Build Windows (release) | |
| run: flutter build windows --release | |
| - name: Resolve installer version | |
| id: winver | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| V='${{ steps.meta.outputs.tag_name }}' | |
| V=${V#v} | |
| # Inno Setup AppVersion / file name need a clean x.y.z; fall back for manual-* tags. | |
| if [[ ! "$V" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then V="1.0.0"; fi | |
| echo "version=$V" >> $GITHUB_OUTPUT | |
| - name: Package portable zip (with app-local VC++ runtime) | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| $rel = "build/windows/x64/runner/Release" | |
| if (!(Test-Path $rel)) { Get-ChildItem -Recurse build/windows | Write-Output; exit 1 } | |
| # App-local deployment of the MSVC runtime so the portable build runs | |
| # on clean machines without the VC++ Redistributable installed. | |
| foreach ($d in @("vcruntime140.dll","vcruntime140_1.dll","msvcp140.dll")) { | |
| $s = Join-Path $env:SystemRoot "System32\$d" | |
| if (Test-Path $s) { Copy-Item $s (Join-Path $rel $d) -Force } | |
| } | |
| $zip = "xplayer-${{ steps.winver.outputs.version }}-windows-x64-portable.zip" | |
| if (Test-Path $zip) { Remove-Item $zip } | |
| Compress-Archive -Path "$rel/*" -DestinationPath $zip | |
| - name: Download VC++ redistributable (for installer) | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| Invoke-WebRequest -Uri "https://aka.ms/vs/17/release/vc_redist.x64.exe" ` | |
| -OutFile "windows/installer/vc_redist.x64.exe" | |
| - name: Build installer (Inno Setup) | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| $iscc = "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" | |
| if (!(Test-Path $iscc)) { | |
| choco install innosetup -y --no-progress | Out-Null | |
| } | |
| if (!(Test-Path $iscc)) { $iscc = "ISCC.exe" } | |
| & $iscc ` | |
| "/DAppVersion=${{ steps.winver.outputs.version }}" ` | |
| "/DSourceDir=$((Resolve-Path 'build/windows/x64/runner/Release').Path)" ` | |
| "/DOutputDir=$((Resolve-Path '.').Path)" ` | |
| "windows/installer/xplayer.iss" | |
| - name: Upload Windows artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: windows | |
| if-no-files-found: error | |
| path: | | |
| xplayer-*-windows-x64-setup.exe | |
| xplayer-*-windows-x64-portable.zip | |
| macos: | |
| name: macOS (app.zip) | |
| runs-on: macos-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Prepare release metadata | |
| id: meta | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| IS_TAG="false" | |
| TAG_NAME="" | |
| if [[ "${GITHUB_REF:-}" == refs/tags/* ]]; then | |
| IS_TAG="true" | |
| TAG_NAME="${GITHUB_REF#refs/tags/}" | |
| fi | |
| if [[ -z "$TAG_NAME" ]]; then | |
| INPUT_TAG="${{ github.event.inputs.tag_name || '' }}" | |
| if [[ -n "$INPUT_TAG" ]]; then | |
| TAG_NAME="$INPUT_TAG" | |
| else | |
| SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7) | |
| TAG_NAME="manual-${SHORT_SHA}" | |
| fi | |
| fi | |
| echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| flutter-version: ${{ env.FLUTTER_VERSION }} | |
| channel: "stable" | |
| cache: true | |
| - name: Enable macOS desktop | |
| run: flutter config --enable-macos-desktop | |
| - name: Update pubspec.yaml version | |
| run: | | |
| VERSION='${{ steps.meta.outputs.tag_name }}' | |
| echo "Updating pubspec.yaml version to: $VERSION" | |
| CLEAN_VERSION=${VERSION#v} | |
| BUILD_NUMBER=${{ github.run_number }} | |
| sed -i "" "s/^version: .*/version: ${CLEAN_VERSION}+${BUILD_NUMBER}/" pubspec.yaml | |
| echo "Updated version in pubspec.yaml:" | |
| grep "^version:" pubspec.yaml | |
| - name: Flutter pub get | |
| run: flutter pub get | |
| - name: Build macOS (release) | |
| run: flutter build macos --release | |
| - name: Package macOS .app to zip (unsigned) | |
| run: | | |
| set -euo pipefail | |
| APP_PATH=$(ls -d build/macos/Build/Products/Release/*.app | head -n 1) | |
| echo "Found app: $APP_PATH" | |
| ditto -c -k --sequesterRsrc --keepParent "$APP_PATH" xplayer-macos.zip | |
| - name: Package macOS .app to DMG (unsigned) | |
| run: | | |
| set -euo pipefail | |
| APP_PATH=$(ls -d build/macos/Build/Products/Release/*.app | head -n 1) | |
| VOLUME_NAME="xplayer" | |
| DMG_PATH="xplayer-macos.dmg" | |
| [ -f "$DMG_PATH" ] && rm -f "$DMG_PATH" | |
| # 创建临时目录以生成 DMG 内容 | |
| TMPDIR=$(mktemp -d) | |
| cp -R "$APP_PATH" "$TMPDIR/" | |
| hdiutil create -volname "$VOLUME_NAME" -srcfolder "$TMPDIR" -ov -format UDZO "$DMG_PATH" | |
| rm -rf "$TMPDIR" | |
| - name: Upload macOS artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: macos | |
| if-no-files-found: error | |
| path: | | |
| xplayer-macos.zip | |
| xplayer-macos.dmg | |
| # macOS App Store(签名 .pkg → TestFlight/ASC)。和 iOS 的区别: | |
| # - 两张证书:App 签名(Apple Distribution)+ 安装包(Mac Installer Distribution) | |
| # - 产物是 productbuild 出的签名 .pkg(不是 .ipa) | |
| # - 需要单独的 macOS 描述文件,嵌进 .app 再签 | |
| # 无 MACOS_* secrets 时整段跳过,不阻塞其它平台。参考 BeeTiny。 | |
| macos_appstore: | |
| name: macOS → App Store | |
| runs-on: macos-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Prepare release metadata | |
| id: meta | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| TAG_NAME="" | |
| if [[ "${GITHUB_REF:-}" == refs/tags/* ]]; then | |
| TAG_NAME="${GITHUB_REF#refs/tags/}" | |
| fi | |
| if [[ -z "$TAG_NAME" ]]; then | |
| INPUT_TAG="${{ github.event.inputs.tag_name || '' }}" | |
| if [[ -n "$INPUT_TAG" ]]; then TAG_NAME="$INPUT_TAG"; else TAG_NAME="manual-$(echo "${GITHUB_SHA}" | cut -c1-7)"; fi | |
| fi | |
| echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT | |
| - name: Select latest Xcode | |
| run: | | |
| LATEST_XCODE=$(ls -d /Applications/Xcode*.app 2>/dev/null | sort -V | tail -1) | |
| if [ -n "$LATEST_XCODE" ]; then sudo xcode-select -s "$LATEST_XCODE"; fi | |
| xcodebuild -version | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| flutter-version: ${{ env.FLUTTER_VERSION }} | |
| channel: "stable" | |
| cache: true | |
| - name: Flutter pub get | |
| run: flutter pub get | |
| - name: Update pubspec.yaml version | |
| run: | | |
| VERSION='${{ steps.meta.outputs.tag_name }}' | |
| CLEAN_VERSION=${VERSION#v} | |
| sed -i "" "s/^version: .*/version: ${CLEAN_VERSION}+${{ github.run_number }}/" pubspec.yaml | |
| grep "^version:" pubspec.yaml | |
| - name: Import certificates + provisioning profile | |
| env: | |
| MACOS_CERTIFICATE_P12: ${{ secrets.MACOS_CERTIFICATE_P12 }} | |
| MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} | |
| MACOS_INSTALLER_CERTIFICATE_P12: ${{ secrets.MACOS_INSTALLER_CERTIFICATE_P12 }} | |
| MACOS_INSTALLER_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_INSTALLER_CERTIFICATE_PASSWORD }} | |
| MACOS_KEYCHAIN_PASSWORD: ${{ secrets.MACOS_KEYCHAIN_PASSWORD }} | |
| MACOS_PROVISIONING_PROFILE: ${{ secrets.MACOS_PROVISIONING_PROFILE }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "${MACOS_CERTIFICATE_P12:-}" ]; then | |
| echo "No MACOS_* signing secrets — skipping macOS App Store upload" | |
| echo "MACOS_SIGNING_ENABLED=false" >> $GITHUB_ENV | |
| exit 0 | |
| fi | |
| KEYCHAIN_PW="${MACOS_KEYCHAIN_PASSWORD:-$(openssl rand -base64 32)}" | |
| CERT=$RUNNER_TEMP/cert.p12 | |
| echo -n "$MACOS_CERTIFICATE_P12" | base64 --decode -o $CERT | |
| PP=$RUNNER_TEMP/xplayer.provisionprofile | |
| echo -n "$MACOS_PROVISIONING_PROFILE" | base64 --decode -o $PP | |
| KC=$RUNNER_TEMP/app-signing.keychain-db | |
| security create-keychain -p "$KEYCHAIN_PW" $KC | |
| security set-keychain-settings -lut 21600 $KC | |
| security unlock-keychain -p "$KEYCHAIN_PW" $KC | |
| security import $CERT -P "$MACOS_CERTIFICATE_PASSWORD" -A -f pkcs12 -k $KC | |
| if [ -n "${MACOS_INSTALLER_CERTIFICATE_P12:-}" ]; then | |
| ICERT=$RUNNER_TEMP/installer.p12 | |
| IPW="${MACOS_INSTALLER_CERTIFICATE_PASSWORD:-$MACOS_CERTIFICATE_PASSWORD}" | |
| echo -n "$MACOS_INSTALLER_CERTIFICATE_P12" | base64 --decode -o $ICERT | |
| security import $ICERT -P "$IPW" -A -f pkcs12 -k $KC | |
| fi | |
| security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PW" $KC | |
| security list-keychain -d user -s $KC | |
| mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles | |
| cp "$PP" "$HOME/Library/MobileDevice/Provisioning Profiles/xplayer.provisionprofile" | |
| echo "MACOS_SIGNING_ENABLED=true" >> $GITHUB_ENV | |
| - name: Build macOS (STORE build = 纯播放器无内置源) | |
| if: env.MACOS_SIGNING_ENABLED == 'true' | |
| run: flutter build macos --release --dart-define=STORE_BUILD=true | |
| - name: Create signed .pkg for App Store | |
| if: env.MACOS_SIGNING_ENABLED == 'true' | |
| run: | | |
| set -euo pipefail | |
| APP_PATH=$(ls -d build/macos/Build/Products/Release/*.app | head -n 1) | |
| if [ ! -d "$APP_PATH" ]; then echo "app not found"; exit 1; fi | |
| SIGNING_IDENTITY=$(security find-identity -v | grep -Eo '"Apple Distribution[^"]*"|"3rd Party Mac Developer Application[^"]*"' | head -1 | tr -d '"') | |
| INSTALLER_IDENTITY=$(security find-identity -v | grep -Eo '"Mac Installer Distribution[^"]*"|"3rd Party Mac Developer Installer[^"]*"' | head -1 | tr -d '"') | |
| echo "Signing: $SIGNING_IDENTITY | Installer: $INSTALLER_IDENTITY" | |
| if [ -z "$SIGNING_IDENTITY" ] || [ -z "$INSTALLER_IDENTITY" ]; then | |
| echo "Missing signing/installer identity"; exit 1 | |
| fi | |
| PP_PATH="$HOME/Library/MobileDevice/Provisioning Profiles/xplayer.provisionprofile" | |
| cp "$PP_PATH" "$APP_PATH/Contents/embedded.provisionprofile" | |
| PLIST=$RUNNER_TEMP/pp.plist | |
| security cms -D -i "$PP_PATH" > "$PLIST" | |
| APP_ID_KEY="com.apple.application-identifier" | |
| APP_IDENTIFIER=$(/usr/libexec/PlistBuddy -c "Print :Entitlements:com.apple.application-identifier" "$PLIST" 2>/dev/null || true) | |
| if [ -z "$APP_IDENTIFIER" ]; then | |
| APP_ID_KEY="application-identifier" | |
| APP_IDENTIFIER=$(/usr/libexec/PlistBuddy -c "Print :Entitlements:application-identifier" "$PLIST" 2>/dev/null || true) | |
| fi | |
| TEAM_IDENTIFIER=$(/usr/libexec/PlistBuddy -c "Print :Entitlements:com.apple.developer.team-identifier" "$PLIST" 2>/dev/null || /usr/libexec/PlistBuddy -c "Print :TeamIdentifier:0" "$PLIST" 2>/dev/null || true) | |
| ENT=$RUNNER_TEMP/sign.entitlements | |
| cp macos/Runner/Release.entitlements "$ENT" | |
| /usr/libexec/PlistBuddy -c "Add :$APP_ID_KEY string $APP_IDENTIFIER" "$ENT" 2>/dev/null || true | |
| /usr/libexec/PlistBuddy -c "Add :com.apple.developer.team-identifier string $TEAM_IDENTIFIER" "$ENT" 2>/dev/null || true | |
| if [ -d "$APP_PATH/Contents/Frameworks" ]; then | |
| find "$APP_PATH/Contents/Frameworks" -depth \( -type d \( -name "*.framework" -o -name "*.xpc" -o -name "*.app" \) -o -type f -name "*.dylib" \) -print0 | while IFS= read -r -d '' N; do | |
| codesign --force --verify --sign "$SIGNING_IDENTITY" "$N" | |
| done | |
| fi | |
| codesign --force --verify --sign "$SIGNING_IDENTITY" --entitlements "$ENT" "$APP_PATH" | |
| codesign --verify --deep --strict "$APP_PATH" | |
| productbuild --component "$APP_PATH" /Applications --sign "$INSTALLER_IDENTITY" xplayer.pkg | |
| ls -lh xplayer.pkg | |
| - name: Upload to App Store Connect | |
| if: env.MACOS_SIGNING_ENABLED == 'true' | |
| # 不要 continue-on-error:上传失败必须让本 job 变红(此 job 不在 release 的 needs 里, | |
| # 所以不会阻塞 GitHub Release 发布)。之前的 continue-on-error 会把失败步骤的 conclusion | |
| # 标成 success,导致 90242 上传失败时整个 run 仍显示绿色。 | |
| env: | |
| APPLE_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY_CONTENT }} | |
| APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} | |
| APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }} | |
| run: | | |
| set -euo pipefail | |
| mkdir -p ~/.appstoreconnect/private_keys | |
| P8="$HOME/.appstoreconnect/private_keys/AuthKey_${APPLE_API_KEY_ID}.p8" | |
| printf '%s' "$APPLE_API_KEY_CONTENT" > "$P8"; chmod 600 "$P8" | |
| xcrun altool --upload-app --type macos --file xplayer.pkg \ | |
| --apiKey "$APPLE_API_KEY_ID" --apiIssuer "$APPLE_API_ISSUER_ID" --show-progress | |
| rm -f "$P8" | |
| - name: Cleanup keychain | |
| if: always() && env.MACOS_SIGNING_ENABLED == 'true' | |
| run: security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true | |
| ios: | |
| name: iOS (build signed IPA + unsigned) | |
| runs-on: macos-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Prepare release metadata | |
| id: meta | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| TAG_NAME="" | |
| if [[ "${GITHUB_REF:-}" == refs/tags/* ]]; then | |
| TAG_NAME="${GITHUB_REF#refs/tags/}" | |
| fi | |
| if [[ -z "$TAG_NAME" ]]; then | |
| INPUT_TAG="${{ github.event.inputs.tag_name || '' }}" | |
| if [[ -n "$INPUT_TAG" ]]; then | |
| TAG_NAME="$INPUT_TAG" | |
| else | |
| SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7) | |
| TAG_NAME="manual-${SHORT_SHA}" | |
| fi | |
| fi | |
| echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT | |
| - name: Select latest Xcode | |
| run: | | |
| LATEST_XCODE=$(ls -d /Applications/Xcode*.app 2>/dev/null | sort -V | tail -1) | |
| if [ -n "$LATEST_XCODE" ]; then sudo xcode-select -s "$LATEST_XCODE"; fi | |
| xcodebuild -version | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| flutter-version: ${{ env.FLUTTER_VERSION }} | |
| channel: "stable" | |
| cache: true | |
| - name: Flutter pub get | |
| run: flutter pub get | |
| - name: Update pubspec.yaml version | |
| run: | | |
| VERSION='${{ steps.meta.outputs.tag_name }}' | |
| CLEAN_VERSION=${VERSION#v} | |
| BUILD_NUMBER=${{ github.run_number }} | |
| sed -i "" "s/^version: .*/version: ${CLEAN_VERSION}+${BUILD_NUMBER}/" pubspec.yaml | |
| grep "^version:" pubspec.yaml | |
| - name: Setup iOS signing from secrets | |
| env: | |
| IOS_P12_BASE64: ${{ secrets.APPLE_CERTIFICATE_P12 }} | |
| IOS_P12_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | |
| IOS_PROVISION_PROFILE_BASE64: ${{ secrets.APPLE_PROVISIONING_PROFILE }} | |
| IOS_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| run: | | |
| set -euo pipefail | |
| if [ -n "${IOS_P12_BASE64:-}" ]; then | |
| echo "Setting up iOS signing..." | |
| KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db | |
| KEYCHAIN_PASSWORD=$(openssl rand -base64 32) | |
| security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH | |
| security set-keychain-settings -lut 21600 $KEYCHAIN_PATH | |
| security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH | |
| CERT_PATH=$RUNNER_TEMP/cert.p12 | |
| echo "$IOS_P12_BASE64" | base64 -d > $CERT_PATH | |
| security import $CERT_PATH -P "$IOS_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH | |
| security list-keychain -d user -s $KEYCHAIN_PATH | |
| PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision | |
| echo "$IOS_PROVISION_PROFILE_BASE64" | base64 -d > $PP_PATH | |
| PP_UUID=$(security cms -D -i $PP_PATH | plutil -extract UUID raw -) | |
| mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles | |
| cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles/${PP_UUID}.mobileprovision | |
| security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH | |
| security find-identity -v -p codesigning $KEYCHAIN_PATH | |
| printf '%s\n' \ | |
| '<?xml version="1.0" encoding="UTF-8"?>' \ | |
| '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">' \ | |
| '<plist version="1.0">' \ | |
| '<dict>' \ | |
| ' <key>method</key>' \ | |
| ' <string>app-store</string>' \ | |
| ' <key>teamID</key>' \ | |
| " <string>${IOS_TEAM_ID}</string>" \ | |
| ' <key>uploadBitcode</key>' \ | |
| ' <false/>' \ | |
| ' <key>uploadSymbols</key>' \ | |
| ' <true/>' \ | |
| ' <key>signingStyle</key>' \ | |
| ' <string>manual</string>' \ | |
| ' <key>signingCertificate</key>' \ | |
| ' <string>Apple Distribution</string>' \ | |
| ' <key>provisioningProfiles</key>' \ | |
| ' <dict>' \ | |
| ' <key>com.tntlikely.xplayer</key>' \ | |
| ' <string>xplayer_AppStore</string>' \ | |
| ' </dict>' \ | |
| '</dict>' \ | |
| '</plist>' \ | |
| > ios/ExportOptions.plist | |
| echo "IOS_SIGNING_ENABLED=true" >> $GITHUB_ENV | |
| echo "iOS signing OK" | |
| else | |
| echo "No iOS signing secrets — unsigned only" | |
| echo "IOS_SIGNING_ENABLED=false" >> $GITHUB_ENV | |
| fi | |
| - name: Build iOS (no codesign, STORE build = 纯播放器无内置源) | |
| run: flutter build ios --release --no-codesign --dart-define=STORE_BUILD=true | |
| - name: Configure Xcode for manual signing | |
| if: env.IOS_SIGNING_ENABLED == 'true' | |
| env: | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| run: | | |
| PROJECT_FILE="ios/Runner.xcodeproj/project.pbxproj" | |
| sed -i '' 's/"CODE_SIGN_IDENTITY\[sdk=iphoneos\*\]" = "iPhone Developer";/"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Distribution";/g' "$PROJECT_FILE" | |
| sed -i '' '/CODE_SIGN_STYLE = /d' "$PROJECT_FILE" | |
| sed -i '' '/DEVELOPMENT_TEAM = /d' "$PROJECT_FILE" | |
| sed -i '' '/PROVISIONING_PROFILE_SPECIFIER = /d' "$PROJECT_FILE" | |
| perl -i -pe "s/(buildSettings = \{)/\$1\n\t\t\t\tCODE_SIGN_STYLE = Manual;\n\t\t\t\tDEVELOPMENT_TEAM = ${APPLE_TEAM_ID};/g" "$PROJECT_FILE" | |
| perl -i -pe ' | |
| if (/^\s+PRODUCT_BUNDLE_IDENTIFIER = com\.tntlikely\.xplayer;\s*$/) { | |
| print "\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"xplayer_AppStore\";\n"; | |
| } | |
| ' "$PROJECT_FILE" | |
| echo "Xcode signing configured" | |
| - name: Archive iOS | |
| if: env.IOS_SIGNING_ENABLED == 'true' | |
| run: | | |
| xcodebuild archive \ | |
| -workspace ios/Runner.xcworkspace \ | |
| -scheme Runner -configuration Release \ | |
| -archivePath build/ios/Runner.xcarchive \ | |
| -quiet 2>&1 | grep -E "error:|failed|succeeded" || true | |
| if [ ! -d build/ios/Runner.xcarchive ]; then echo "Archive failed"; exit 1; fi | |
| - name: Export IPA | |
| if: env.IOS_SIGNING_ENABLED == 'true' | |
| run: | | |
| xcodebuild -exportArchive \ | |
| -archivePath build/ios/Runner.xcarchive \ | |
| -exportPath build/ios/ipa \ | |
| -exportOptionsPlist ios/ExportOptions.plist \ | |
| -quiet 2>&1 | grep -E "error:|Exported|succeeded" || true | |
| ls -lh build/ios/ipa/ || (echo "Export failed"; exit 1) | |
| - name: Copy signed IPA | |
| if: env.IOS_SIGNING_ENABLED == 'true' | |
| run: | | |
| VERSION="${{ steps.meta.outputs.tag_name }}" | |
| if ls build/ios/ipa/*.ipa >/dev/null 2>&1; then | |
| cp build/ios/ipa/*.ipa "xplayer-${VERSION}-signed.ipa" | |
| ls -lh "xplayer-${VERSION}-signed.ipa" | |
| fi | |
| - name: Cleanup keychain | |
| if: always() && env.IOS_SIGNING_ENABLED == 'true' | |
| run: security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true | |
| - name: Package unsigned IPA fallback | |
| run: | | |
| VERSION="${{ steps.meta.outputs.tag_name }}" | |
| APP_PATH="build/ios/iphoneos/Runner.app" | |
| if [ -d "$APP_PATH" ]; then | |
| ditto -c -k --sequesterRsrc --keepParent "$APP_PATH" "xplayer-${VERSION}-iphoneos.app.zip" | |
| mkdir -p Payload && cp -R "$APP_PATH" Payload/ | |
| /usr/bin/zip -qry "xplayer-${VERSION}-unsigned.ipa" Payload | |
| rm -rf Payload | |
| fi | |
| ls -la xplayer-${VERSION}-* || true | |
| - name: Upload signed IPA artifact (consumed by ios_appstore job) | |
| if: env.IOS_SIGNING_ENABLED == 'true' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ios-signed | |
| path: xplayer-*-signed.ipa | |
| if-no-files-found: error | |
| - name: Upload iOS artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ios | |
| path: | | |
| xplayer-*-iphoneos.app.zip | |
| xplayer-*-unsigned.ipa | |
| # TestFlight 上传独立成 job:上传失败时整个 run 会变红(无 continue-on-error), | |
| # 但它不在 release 的 needs 里,所以不会阻塞 GitHub Release 发布。复用 ios job 产出的签名 ipa。 | |
| ios_appstore: | |
| name: iOS → TestFlight | |
| needs: [ios] | |
| runs-on: macos-latest | |
| steps: | |
| - name: Download signed IPA | |
| # 无签名 secrets 时 ios job 不会产出 ios-signed,这里下载失败属正常; | |
| # 真正的上传失败由下一步(无 continue-on-error)暴露成红色。 | |
| continue-on-error: true | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: ios-signed | |
| path: signed | |
| - name: Upload to TestFlight | |
| env: | |
| APPLE_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY_CONTENT }} | |
| APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} | |
| APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }} | |
| run: | | |
| set -euo pipefail | |
| IPA_FILE=$(ls signed/*.ipa 2>/dev/null | head -1 || true) | |
| if [ -z "$IPA_FILE" ]; then | |
| echo "No signed IPA artifact (iOS signing disabled) — nothing to upload" | |
| exit 0 | |
| fi | |
| if [ -z "${APPLE_API_KEY_CONTENT:-}" ] || [ -z "${APPLE_API_KEY_ID:-}" ] || [ -z "${APPLE_API_ISSUER_ID:-}" ]; then | |
| echo "No App Store Connect API key secrets — skipped TestFlight upload" | |
| exit 0 | |
| fi | |
| echo "Uploading $IPA_FILE to TestFlight..." | |
| mkdir -p ~/.appstoreconnect/private_keys | |
| P8="$HOME/.appstoreconnect/private_keys/AuthKey_${APPLE_API_KEY_ID}.p8" | |
| printf '%s' "$APPLE_API_KEY_CONTENT" > "$P8"; chmod 600 "$P8" | |
| xcrun altool --upload-app --type ios --file "$IPA_FILE" \ | |
| --apiKey "$APPLE_API_KEY_ID" --apiIssuer "$APPLE_API_ISSUER_ID" --verbose | |
| rm -f "$P8" | |
| echo "Uploaded to TestFlight" | |
| linux: | |
| name: Linux (tar.gz) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Prepare release metadata | |
| id: meta | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| IS_TAG="false" | |
| TAG_NAME="" | |
| if [[ "${GITHUB_REF:-}" == refs/tags/* ]]; then | |
| IS_TAG="true" | |
| TAG_NAME="${GITHUB_REF#refs/tags/}" | |
| fi | |
| if [[ -z "$TAG_NAME" ]]; then | |
| INPUT_TAG="${{ github.event.inputs.tag_name || '' }}" | |
| if [[ -n "$INPUT_TAG" ]]; then | |
| TAG_NAME="$INPUT_TAG" | |
| else | |
| SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7) | |
| TAG_NAME="manual-${SHORT_SHA}" | |
| fi | |
| fi | |
| echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT | |
| - name: Install Linux build dependencies | |
| run: sudo apt-get update && sudo apt-get install -y libgtk-3-dev | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| flutter-version: ${{ env.FLUTTER_VERSION }} | |
| channel: "stable" | |
| cache: true | |
| - name: Enable Linux desktop | |
| run: flutter config --enable-linux-desktop | |
| - name: Update pubspec.yaml version | |
| run: | | |
| VERSION='${{ steps.meta.outputs.tag_name }}' | |
| echo "Updating pubspec.yaml version to: $VERSION" | |
| CLEAN_VERSION=${VERSION#v} | |
| BUILD_NUMBER=${{ github.run_number }} | |
| sed -i "s/^version: .*/version: ${CLEAN_VERSION}+${BUILD_NUMBER}/" pubspec.yaml | |
| echo "Updated version in pubspec.yaml:" | |
| grep "^version:" pubspec.yaml | |
| - name: Flutter pub get | |
| run: flutter pub get | |
| - name: Build Linux (release) | |
| run: flutter build linux --release | |
| - name: Package Linux tar.gz | |
| run: | | |
| set -euo pipefail | |
| cd build/linux/x64/release/bundle | |
| tar -czf ../../../xplayer-linux-x64.tar.gz * | |
| cd - | |
| - name: Upload Linux artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: linux | |
| if-no-files-found: error | |
| path: build/linux/xplayer-linux-x64.tar.gz | |
| release: | |
| name: Create GitHub Release | |
| runs-on: ubuntu-latest | |
| needs: [android, windows, macos, ios, linux] | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Prepare release metadata | |
| id: meta | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| IS_TAG="false" | |
| TAG_NAME="" | |
| if [[ "${GITHUB_REF:-}" == refs/tags/* ]]; then | |
| IS_TAG="true" | |
| TAG_NAME="${GITHUB_REF#refs/tags/}" | |
| fi | |
| if [[ -z "$TAG_NAME" ]]; then | |
| INPUT_TAG="${{ github.event.inputs.tag_name || '' }}" | |
| if [[ -n "$INPUT_TAG" ]]; then | |
| TAG_NAME="$INPUT_TAG" | |
| else | |
| SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7) | |
| TAG_NAME="manual-${SHORT_SHA}" | |
| fi | |
| fi | |
| # 手动触发默认 prerelease=true;tag 触发默认 false | |
| INPUT_PRERELEASE="${{ github.event.inputs.prerelease || '' }}" | |
| if [[ "$IS_TAG" == "true" ]]; then | |
| PRERELEASE="false" | |
| else | |
| PRERELEASE=${INPUT_PRERELEASE:-true} | |
| fi | |
| # 是否创建 Release | |
| INPUT_CREATE="${{ github.event.inputs.create_release || '' }}" | |
| if [[ "$IS_TAG" == "true" ]]; then | |
| CREATE_RELEASE="true" | |
| else | |
| CREATE_RELEASE=${INPUT_CREATE:-false} | |
| fi | |
| RELEASE_NAME_INPUT="${{ github.event.inputs.release_name || '' }}" | |
| if [[ -n "$RELEASE_NAME_INPUT" ]]; then | |
| RELEASE_NAME="$RELEASE_NAME_INPUT" | |
| else | |
| RELEASE_NAME="$TAG_NAME" | |
| fi | |
| echo "is_tag=$IS_TAG" >> $GITHUB_OUTPUT | |
| echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT | |
| echo "prerelease=$PRERELEASE" >> $GITHUB_OUTPUT | |
| echo "create_release=$CREATE_RELEASE" >> $GITHUB_OUTPUT | |
| echo "release_name=$RELEASE_NAME" >> $GITHUB_OUTPUT | |
| - name: Download Android artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: android | |
| path: ./dist/android | |
| - name: List downloaded artifacts (debug) | |
| run: | | |
| echo "== dist tree ==" | |
| ls -R ./dist || true | |
| - name: Download Windows artifact | |
| uses: actions/download-artifact@v4 | |
| continue-on-error: true # Windows 失败时无产物,跳过不阻塞发布 | |
| with: | |
| name: windows | |
| path: ./dist/windows | |
| - name: Download macOS artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: macos | |
| path: ./dist/macos | |
| - name: Download iOS artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: ios | |
| path: ./dist/ios | |
| - name: Download Linux artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: linux | |
| path: ./dist/linux | |
| - name: Generate release notes | |
| id: notes | |
| run: | | |
| TAG="${{ steps.meta.outputs.tag_name }}" | |
| # 上一个发布 tag = 历史上最近的祖先「版本」tag(排除 manual-* 等非版本 tag), | |
| # 避免 --sort=-version:refname 把 manual-* 排到最前、导致变更记录跨越多个版本。 | |
| PREV_TAG=$(git describe --tags --abbrev=0 --match '[0-9]*' --match 'v[0-9]*' HEAD^ 2>/dev/null || true) | |
| echo "Current tag: $TAG" | |
| echo "Previous tag: $PREV_TAG" | |
| # 生成 commit 列表(排除 merge commit) | |
| if [ -n "$PREV_TAG" ]; then | |
| echo "Generating commit log from $PREV_TAG to HEAD" | |
| COMMITS=$(git log --no-merges --pretty=format:"- %s ([%h](https://github.com/${GITHUB_REPOSITORY}/commit/%H))" "$PREV_TAG"..HEAD) | |
| else | |
| echo "No previous tag found, showing all commits" | |
| COMMITS=$(git log --no-merges --pretty=format:"- %s ([%h](https://github.com/${GITHUB_REPOSITORY}/commit/%H))") | |
| fi | |
| cat > RELEASE_NOTES.md <<EOF | |
| ## 变更记录 | |
| $COMMITS | |
| EOF | |
| echo "notes_file=RELEASE_NOTES.md" >> $GITHUB_OUTPUT | |
| - name: Create Release | |
| if: ${{ steps.meta.outputs.create_release == 'true' }} | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ steps.meta.outputs.tag_name }} | |
| name: ${{ steps.meta.outputs.release_name }} | |
| prerelease: ${{ steps.meta.outputs.prerelease == 'true' }} | |
| body_path: ${{ steps.notes.outputs.notes_file }} | |
| files: | | |
| dist/android/**/*.apk | |
| dist/android/**/*.aab | |
| dist/windows/* | |
| dist/macos/* | |
| dist/ios/* | |
| dist/linux/* | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |