ci: gate TestFlight behind opt-in toggle + Xcode 27 build fixes

TestFlight is now opt-in (default OFF). A workflow_dispatch boolean
`enable_testflight` (or the `ENABLE_TESTFLIGHT` repo variable) drives a
`prepare.outputs.testflight_enabled` flag that gates the macOS
app-store-pkg build leg and both testflight_ios/testflight_macos upload
legs. Missing Apple signing secrets no longer fail the normal DMG/IPA
release path (package-macos-app-store-pkg.sh hard-exits without them).

Xcode 27 build compatibility:
- Align Apple deployment targets so no pod sits below the app minimum
  (Xcode 27 rejects this): macOS pods + RunnerTests -> 15.6, iOS pods
  -> 15.5 to match the Runner targets.
- Add a `lipo` shim (scripts/xcode-tools/lipo) wired onto PATH in the
  iOS/macOS build phases; Xcode 27 only accepts one `-verify_arch`
  architecture per call while Flutter passes them all at once.
- macOS project hygiene: correct PrivacyInfo.xcprivacy path, set app
  display name + LSApplicationCategoryType.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Haitao Pan 2026-06-30 06:48:30 +08:00
parent ca0d8a49a8
commit fe4c0ebe24
8 changed files with 77 additions and 20 deletions

View File

@ -30,6 +30,11 @@ on:
- ".github/actions/setup-flutter-sdk/action.yml"
- ".github/workflows/build-and-release.yml"
workflow_dispatch:
inputs:
enable_testflight:
description: "Build & upload TestFlight (macOS/iOS App Store) artifacts"
type: boolean
default: false
permissions:
contents: read
@ -49,6 +54,7 @@ jobs:
contents: write
outputs:
should_release: ${{ steps.flags.outputs.should_release }}
testflight_enabled: ${{ steps.flags.outputs.testflight_enabled }}
release_tag: ${{ steps.meta.outputs.release_tag }}
release_title: ${{ steps.meta.outputs.release_title }}
release_notes: ${{ steps.meta.outputs.release_notes }}
@ -61,6 +67,9 @@ jobs:
- name: Determine release mode
id: flags
shell: bash
env:
ENABLE_TESTFLIGHT_INPUT: ${{ github.event.inputs.enable_testflight }}
ENABLE_TESTFLIGHT_VAR: ${{ vars.ENABLE_TESTFLIGHT }}
run: |
if [[ "${GITHUB_REF:-}" == refs/tags/v* || "${GITHUB_EVENT_NAME:-}" == "workflow_dispatch" || "${GITHUB_REF:-}" == "refs/heads/main" ]]; then
echo "should_release=true" >> "$GITHUB_OUTPUT"
@ -68,6 +77,16 @@ jobs:
echo "should_release=false" >> "$GITHUB_OUTPUT"
fi
# TestFlight is opt-in (default OFF). Enabled only when explicitly
# requested via the workflow_dispatch input or the ENABLE_TESTFLIGHT
# repo/org variable. Keeps missing Apple signing secrets from failing
# the normal DMG/IPA release path.
if [[ "${ENABLE_TESTFLIGHT_INPUT:-}" == "true" || "${ENABLE_TESTFLIGHT_VAR:-}" == "true" ]]; then
echo "testflight_enabled=true" >> "$GITHUB_OUTPUT"
else
echo "testflight_enabled=false" >> "$GITHUB_OUTPUT"
fi
- name: Compute release metadata
id: meta
shell: bash
@ -257,12 +276,12 @@ jobs:
go-version: "1.24.1"
- name: Build platform artifacts
if: ${{ steps.preflight.outputs.should_build_platform == 'true' && (matrix.release_only != 'true' || env.SHOULD_RELEASE == 'true') }}
if: ${{ steps.preflight.outputs.should_build_platform == 'true' && (matrix.release_only != 'true' || env.SHOULD_RELEASE == 'true') && (matrix.package != 'app-store-pkg' || needs.prepare.outputs.testflight_enabled == 'true') }}
shell: bash
run: bash ./scripts/ci/build_matrix_artifacts.sh "$PLATFORM" "$ARCH" "${{ matrix.package }}" "$SHOULD_RELEASE"
- name: Upload build artifacts
if: ${{ steps.preflight.outputs.should_build_platform == 'true' && (matrix.release_only != 'true' || env.SHOULD_RELEASE == 'true') }}
if: ${{ steps.preflight.outputs.should_build_platform == 'true' && (matrix.release_only != 'true' || env.SHOULD_RELEASE == 'true') && (matrix.package != 'app-store-pkg' || needs.prepare.outputs.testflight_enabled == 'true') }}
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.artifact_name }}
@ -338,7 +357,7 @@ jobs:
- name: Load App Store Connect secrets
id: vault
if: ${{ matrix.target != 'github_release' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
if: ${{ matrix.target != 'github_release' && needs.prepare.outputs.testflight_enabled == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
uses: hashicorp/vault-action@v4
with:
url: ${{ env.VAULT_ADDR }}
@ -356,7 +375,7 @@ jobs:
kv/data/github-actions/xworkmate-app APP_STORE_CONNECT_API_KEY_P8_BASE64 | APP_STORE_CONNECT_API_KEY_P8_BASE64
- name: Export App Store Connect secrets
if: ${{ matrix.target != 'github_release' }}
if: ${{ matrix.target != 'github_release' && needs.prepare.outputs.testflight_enabled == 'true' }}
run: |
{
echo "APPLE_CERT_P12_BASE64=${{ steps.vault.outputs.APPLE_CERT_P12_BASE64 }}"
@ -385,13 +404,13 @@ jobs:
RELEASE_NOTES: ${{ needs.prepare.outputs.release_notes }}
- name: Download TestFlight artifact
if: ${{ matrix.target != 'github_release' }}
if: ${{ matrix.target != 'github_release' && needs.prepare.outputs.testflight_enabled == 'true' }}
uses: actions/download-artifact@v8
with:
name: ${{ matrix.artifact_name }}
path: ${{ matrix.artifact_path }}
- name: Upload to TestFlight
if: ${{ matrix.target != 'github_release' }}
if: ${{ matrix.target != 'github_release' && needs.prepare.outputs.testflight_enabled == 'true' }}
shell: bash
run: bash ./scripts/ci/testflight_upload.sh "${{ matrix.testflight_platform }}" "${{ matrix.artifact_path }}"

View File

@ -39,6 +39,11 @@ post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
# Xcode 27 rejects dependency targets below the app's iOS 15.5 minimum.
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.5'
end
next unless ['Pods-Runner', 'Pods-RunnerTests'].include?(target.name)
target.build_configurations.each do |config|

View File

@ -67,6 +67,6 @@ SPEC CHECKSUMS:
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db
PODFILE CHECKSUM: 5ab2a375a52a76f419425b2b219d2743259d6f1f
PODFILE CHECKSUM: ca16f6ef66890e172b6528d5f0eb390e0410291e
COCOAPODS: 1.16.2

View File

@ -291,7 +291,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
shellScript = "export PATH=\"$PROJECT_DIR/../scripts/xcode-tools:$PATH\"\n/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
@ -306,7 +306,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
shellScript = "export PATH=\"$PROJECT_DIR/../scripts/xcode-tools:$PATH\"\n/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
BA47ED2B244B5E2B99043424 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;

View File

@ -1,4 +1,4 @@
platform :osx, '14.0'
platform :osx, '15.6'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
@ -97,7 +97,8 @@ post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_macos_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '14.0'
# Xcode 27 rejects dependency targets below the app's 15.6 minimum.
config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '15.6'
next unless ['Pods-Runner', 'Pods-RunnerTests', 'WebRTC-SDK', 'flutter_webrtc'].include?(target.name)

View File

@ -61,6 +61,6 @@ SPEC CHECKSUMS:
super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189
WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db
PODFILE CHECKSUM: 1eb7d5d1472c632b8f775dd34562291c20ae818a
PODFILE CHECKSUM: 7804cba3ecbc9953edc70dee53b2ce2b4aeaa013
COCOAPODS: 1.16.2

View File

@ -371,7 +371,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
shellScript = "export PATH=\"$PROJECT_DIR/../scripts/xcode-tools:$PATH\"\necho \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
};
33CC111E2044C6BF0003C045 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
@ -392,7 +392,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "if [[ \"${WORKSPACE_DIR:-}\" == *.xcodeproj ]]; then\n echo \"error: XWorkmate macOS builds with CocoaPods plugins must be launched from macos/Runner.xcworkspace, not Runner.xcodeproj.\" >&2\n echo \"error: Close this project, open macos/Runner.xcworkspace in Xcode, and build the shared Runner scheme for My Mac.\" >&2\n echo \"error: Pods targets appearing in the workspace are expected. Only configure signing on the Runner target.\" >&2\n echo \"error: For release packaging, run 'flutter build macos' from the repository root.\" >&2\n exit 1\nfi\n\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
shellScript = "export PATH=\"$PROJECT_DIR/../scripts/xcode-tools:$PATH\"\nif [[ \"${WORKSPACE_DIR:-}\" == *.xcodeproj ]]; then\n echo \"error: XWorkmate macOS builds with CocoaPods plugins must be launched from macos/Runner.xcworkspace, not Runner.xcodeproj.\" >&2\n echo \"error: Close this project, open macos/Runner.xcworkspace in Xcode, and build the shared Runner scheme for My Mac.\" >&2\n echo \"error: Pods targets appearing in the workspace are expected. Only configure signing on the Runner target.\" >&2\n echo \"error: For release packaging, run 'flutter build macos' from the repository root.\" >&2\n exit 1\nfi\n\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
};
8E8C2A3EBAA3461603096C04 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
@ -511,7 +511,7 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 15.6;
MARKETING_VERSION = 1.0;
OTHER_CFLAGS = (
"$(inherited)",
@ -539,7 +539,7 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 15.6;
MARKETING_VERSION = 1.0;
OTHER_CFLAGS = (
"$(inherited)",
@ -567,7 +567,7 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 15.6;
MARKETING_VERSION = 1.0;
OTHER_CFLAGS = (
"$(inherited)",
@ -661,11 +661,13 @@
ENABLE_RESOURCE_ACCESS_USB = NO;
ENABLE_USER_SELECTED_FILES = readonly;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Xworkmate;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 15.6;
OTHER_CFLAGS = (
"$(inherited)",
"-Wno-ignored-attributes",
@ -823,11 +825,13 @@
ENABLE_RESOURCE_ACCESS_USB = NO;
ENABLE_USER_SELECTED_FILES = readonly;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Xworkmate;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 15.6;
OTHER_CFLAGS = (
"$(inherited)",
"-Wno-ignored-attributes",
@ -863,11 +867,13 @@
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_USER_SELECTED_FILES = readonly;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Xworkmate;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 15.6;
OTHER_CFLAGS = (
"$(inherited)",
"-Wno-ignored-attributes",

26
scripts/xcode-tools/lipo Executable file
View File

@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
real_lipo="$(xcrun --find lipo)"
args=("$@")
verify_index=-1
for ((index = 0; index < ${#args[@]}; index++)); do
if [[ "${args[index]}" == "-verify_arch" ]]; then
verify_index=$index
break
fi
done
# Xcode 27 accepts one architecture per -verify_arch invocation. Flutter
# passes all requested architectures at once, so verify each one separately.
if ((verify_index >= 0 && ${#args[@]} - verify_index > 2)); then
command_prefix=("${args[@]:0:verify_index}")
architectures=("${args[@]:verify_index+1}")
for architecture in "${architectures[@]}"; do
"$real_lipo" "${command_prefix[@]}" -verify_arch "$architecture"
done
exit 0
fi
exec "$real_lipo" "$@"