diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 038f69e7..61c08a39 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -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 }}" diff --git a/ios/Podfile b/ios/Podfile index 233398f4..e834f9a9 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -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| diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 9d9f0d3f..38ed7aa1 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -67,6 +67,6 @@ SPEC CHECKSUMS: super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4 WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db -PODFILE CHECKSUM: 5ab2a375a52a76f419425b2b219d2743259d6f1f +PODFILE CHECKSUM: ca16f6ef66890e172b6528d5f0eb390e0410291e COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index a9971d07..4b7f4634 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -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; diff --git a/macos/Podfile b/macos/Podfile index 571f554e..4dcf00b5 100644 --- a/macos/Podfile +++ b/macos/Podfile @@ -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) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 54cd6ad2..3ed091b0 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -61,6 +61,6 @@ SPEC CHECKSUMS: super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db -PODFILE CHECKSUM: 1eb7d5d1472c632b8f775dd34562291c20ae818a +PODFILE CHECKSUM: 7804cba3ecbc9953edc70dee53b2ce2b4aeaa013 COCOAPODS: 1.16.2 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 0d884d44..2003cdfa 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -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", diff --git a/scripts/xcode-tools/lipo b/scripts/xcode-tools/lipo new file mode 100755 index 00000000..8c156f59 --- /dev/null +++ b/scripts/xcode-tools/lipo @@ -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" "$@" diff --git a/test/runtime/assistant_execution_target_test.dart b/test/runtime/assistant_execution_target_test.dart index 9276a09a..08331617 100644 --- a/test/runtime/assistant_execution_target_test.dart +++ b/test/runtime/assistant_execution_target_test.dart @@ -322,9 +322,7 @@ void main() { 'xworkmate-no-runtime-main-home-', ); addTearDown(() async { - if (await localHome.exists()) { - await localHome.delete(recursive: true); - } + await _resilientDelete(localHome); }); final controller = _sandboxController( environmentOverride: const {}, @@ -358,9 +356,7 @@ void main() { 'xworkmate-refresh-no-session-one-', ); addTearDown(() async { - if (await localHome.exists()) { - await localHome.delete(recursive: true); - } + await _resilientDelete(localHome); }); final controller = _sandboxController( environmentOverride: const {}, @@ -439,9 +435,7 @@ void main() { 'xworkmate-stable-task-selection-home-', ); addTearDown(() async { - if (await localHome.exists()) { - await localHome.delete(recursive: true); - } + await _resilientDelete(localHome); }); final controller = _sandboxController( environmentOverride: const {}, @@ -1716,9 +1710,7 @@ void main() { 'xworkmate-acp-interrupt-artifacts-', ); addTearDown(() async { - if (await localWorkspace.exists()) { - await localWorkspace.delete(recursive: true); - } + await _resilientDelete(localWorkspace); }); final fakeGoTaskService = _RecordingGoTaskServiceClient() ..onExecuteTask = ((request) async { @@ -2017,9 +2009,7 @@ void main() { 'xworkmate-acp-handshake-interrupt-artifacts-', ); addTearDown(() async { - if (await localWorkspace.exists()) { - await localWorkspace.delete(recursive: true); - } + await _resilientDelete(localWorkspace); }); final fakeGoTaskService = _RecordingGoTaskServiceClient() ..updatesBeforeNextOutcome.add( @@ -2383,9 +2373,7 @@ void main() { 'xworkmate-background-completion-home-', ); addTearDown(() async { - if (await localHome.exists()) { - await localHome.delete(recursive: true); - } + await _resilientDelete(localHome); }); final fakeGoTaskService = _BlockingGoTaskServiceClient(); final controller = _connectedController( @@ -2508,9 +2496,7 @@ void main() { 'xworkmate-same-prompt-home-', ); addTearDown(() async { - if (await localHome.exists()) { - await localHome.delete(recursive: true); - } + await _resilientDelete(localHome); }); final fakeGoTaskService = _BlockingGoTaskServiceClient(); final controller = _connectedController( @@ -2683,9 +2669,7 @@ void main() { 'xworkmate-same-prompt-empty-home-', ); addTearDown(() async { - if (await localHome.exists()) { - await localHome.delete(recursive: true); - } + await _resilientDelete(localHome); }); final fakeGoTaskService = _BlockingGoTaskServiceClient(); final controller = _connectedController( @@ -2707,9 +2691,7 @@ void main() { continue; } final directory = Directory(workspace); - if (await directory.exists()) { - await directory.delete(recursive: true); - } + await _resilientDelete(directory); } }); @@ -2845,9 +2827,7 @@ void main() { 'xworkmate-terminal-failure-home-', ); addTearDown(() async { - if (await localHome.exists()) { - await localHome.delete(recursive: true); - } + await _resilientDelete(localHome); }); final fakeGoTaskService = _BlockingGoTaskServiceClient(); final controller = _connectedController( @@ -2925,9 +2905,7 @@ void main() { 'xworkmate-empty-output-home-', ); addTearDown(() async { - if (await localHome.exists()) { - await localHome.delete(recursive: true); - } + await _resilientDelete(localHome); }); final fakeGoTaskService = _BlockingGoTaskServiceClient(); final controller = _connectedController( @@ -3297,9 +3275,7 @@ void main() { addTearDown(() async { fakeGoTaskService.completeAll(); controller.dispose(); - if (await localHome.exists()) { - await localHome.delete(recursive: true); - } + await _resilientDelete(localHome); }); for ( @@ -4902,19 +4878,28 @@ UiFeatureManifest _defaultDesktopManifest() { } Future _resilientDelete(Directory dir) async { - if (!await dir.exists()) { - return; - } for (var attempt = 0; attempt < 8; attempt++) { + if (!await dir.exists()) { + return; + } try { await dir.delete(recursive: true); return; } catch (error) { + // A background flush (e.g. controller dispose still persisting state) + // may keep writing into the temp dir, so a recursive delete can race + // and fail with "Directory not empty". Retry a few times. debugPrint('Temporary directory delete retry: $error'); await Future.delayed(const Duration(milliseconds: 50)); } } - await dir.delete(recursive: true); + // Best-effort cleanup: never fail a test over leftover temp files; the OS + // reclaims the temp directory regardless. + try { + await dir.delete(recursive: true); + } catch (error) { + debugPrint('Giving up on temporary directory cleanup: $error'); + } } AppController _sandboxController({