publish method

  1. @override
Future<void> publish()
override

Implementation

@override
Future<void> publish() async {
  _logger.info('Install dependencies...');

  final isProduction = flutterPublish.stage == PublishStage.production;

  await ensureInstalled('fastlane');

  // Create tmp keychain to be able to run non interactively,
  // see https://github.com/fastlane/fastlane/blob/df12128496a9a0ad349f8cf8efe6f9288612f2cb/fastlane/lib/fastlane/actions/setup_ci.rb#L37
  final fastlaneKeychainName = 'fastlane_tmp_keychain';
  final resultStr = await runFastlaneProcess(
    [
      'run',
      'is_ci',
    ],
    workingDirectory: _iosDirectory,
  );
  final isCi = bool.parse(resultStr ?? 'false');
  if (isCi) {
    await runProcess(
      'fastlane',
      [
        'run',
        'setup_ci',
      ],
      workingDirectory: _iosDirectory,
    );
  } else {
    _logger.warning(
        'Not on CI: Create keychain "$fastlaneKeychainName" manually.');
    await runProcess(
      'fastlane',
      [
        'run',
        'create_keychain',
        'name:$fastlaneKeychainName',
        'default_keychain:true',
        'unlock:true',
        'timeout:3600',
        'lock_when_sleeps:true',
        'password:',
      ],
      workingDirectory: _iosDirectory,
    );
  }

  // Determine app bundle id
  await installFastlanePlugin('get_product_bundle_id',
      workingDirectory: _iosDirectory);

  String buildConfiguration = _isDevelopment ? 'Debug' : 'Release';
  if (xcodeScheme != 'Runner') {
    buildConfiguration += '-$xcodeScheme';
  }
  final bundleId = await runFastlaneProcess(
    [
      'run',
      'get_product_bundle_id',
      'project_filepath:Runner.xcodeproj',
      'target:Runner',
      'scheme:$xcodeScheme',
      'build_configuration:$buildConfiguration',
    ],
    printCall: true,
    workingDirectory: _iosDirectory,
  );
  if (bundleId == null) throw Exception('Bundle Id not found');

  _logger.info('Use app bundle id: $bundleId');

  final fastlaneAppfile = '''
app_identifier("$bundleId")
apple_id("$appleUsername")
itc_team_id("$contentProviderId")
team_id("$teamId")
  ''';
  await Directory(_fastlaneDirectory).create(recursive: true);
  await File('$_fastlaneDirectory/Appfile').writeAsString(fastlaneAppfile);

  final apiKeyJsonPath = await generateApiKeyJson(
    apiPrivateKeyBase64: apiPrivateKeyBase64,
    apiKeyId: apiKeyId,
    apiIssuerId: apiIssuerId,
    isTeamEnterprise: isTeamEnterprise,
    workingDirectory: _iosDirectory,
  );

  Future<void> installCertificates({bool isDevelopment = false}) async {
    final signingIdentity = isDevelopment ? 'Development' : 'Distribution';

    final codeSigningIdentity =
        'iPhone ${isDevelopment ? 'Developer' : 'Distribution'}';
    // Disable automatic code signing
    await runProcess(
      'fastlane',
      [
        'run',
        'update_code_signing_settings',
        'use_automatic_signing:false',
        'path:Runner.xcodeproj',
        'code_sign_identity:$codeSigningIdentity',
        'sdk:iphoneos*',
      ],
      workingDirectory: _iosDirectory,
    );

    final p12PrivateKeyBytes =
        base64Decode(isDevelopment ? '' : distributionPrivateKeyBase64);
    final distributionPrivateKeyFile =
        File('$_iosDirectory/$signingIdentity.p12');
    await distributionPrivateKeyFile.writeAsBytes(p12PrivateKeyBytes);

    // Import private key
    await runProcess(
      'fastlane',
      [
        'run',
        'import_certificate',
        'certificate_path:$signingIdentity.p12',
        'keychain_name:$fastlaneKeychainName',
      ],
      workingDirectory: _iosDirectory,
    );

    final certBytes =
        base64Decode(isDevelopment ? '' : distributionCertificateBase64);
    final certFile = File('$_iosDirectory/$signingIdentity.cer');
    await certFile.writeAsBytes(certBytes);

    // Import certificate
    await runProcess(
      'fastlane',
      [
        'run',
        'import_certificate',
        'certificate_path:$signingIdentity.cer',
        'keychain_name:$fastlaneKeychainName',
      ],
      workingDirectory: _iosDirectory,
    );

    // Download provisioning profile
    await runProcess(
      'fastlane',
      [
        'sigh',
        'download_all',
        if (isDevelopment) '--development',
        // get_provisioning_profile
        //'filename:$signingIdentity.mobileprovision', // only works for newly created profiles
        '--api_key_path',
        apiKeyJsonPath,
      ],
      workingDirectory: _iosDirectory,
    );

    final iosDir = Directory(_iosDirectory);
    final entities = await iosDir.list().toList();
    Iterable<FileSystemEntity> provisioningProfilePaths =
        entities.where((file) {
      final fileName = file.uri.pathSegments.last;
      return fileName.endsWith('.mobileprovision');
    });

    for (var provisioningProfilePath in provisioningProfilePaths) {
      final filePath = provisioningProfilePath.uri.pathSegments.last;
      final fileName = filePath.replaceAll('.mobileprovision', '');
      final provisionParams = fileName.split('_');
      final provisionIsDevelopment = provisionParams[0] != 'AppStore';
      if (provisionIsDevelopment != isDevelopment) continue;
      final provisionBundleId =
          provisionParams[provisionParams.length > 2 ? 2 : 1];

      // Install provisioning profile
      await runProcess(
        'fastlane',
        [
          'run',
          'install_provisioning_profile',
          'path:$filePath',
        ],
        workingDirectory: _iosDirectory,
      );

      if (!updateProvisioning) continue;

      // Update provisioning profile
      // Need to get the target (product) name of the bundle ids in order to update the provisioning profiles.
      // As there's no easy way to do this in fastlane, a script handles this.
      final getBundleIdFromProductRubyUri = Uri.parse(
          'package:flutter_release/fastlane/get_bundle_id_product.rb');
      final getBundleIdFromProductRubyFile =
          await Isolate.resolvePackageUri(getBundleIdFromProductRubyUri);
      var result = await runProcess(
        'ruby',
        [
          getBundleIdFromProductRubyFile!.path,
          'Runner.xcodeproj',
          provisionBundleId,
          buildConfiguration,
        ],
        workingDirectory: _iosDirectory,
      );
      final target = result.stdout.trim();
      _logger.info('Target "$target" has bundle id "$provisionBundleId"');

      result = await runProcess(
        'fastlane',
        [
          'run',
          'update_project_provisioning',
          'xcodeproj:Runner.xcodeproj',
          // 'build_configuration:${isDevelopment ? '/Debug|Profile/gm' : 'Release'}',
          // 'build_configuration:${isDevelopment ? 'Debug' : 'Release'}',
          'target_filter:${target.replaceAll('.', '\\.')}',
          'profile:$filePath',
          'code_signing_identity:$codeSigningIdentity',
        ],
        workingDirectory: _iosDirectory,
      );
      _logger.info(
          'Updating provisioning profile $filePath ($provisionBundleId)');
      _logger.info(result.stdout);
    }
  }

  // await installCertificates(isDevelopment: true);
  await installCertificates(isDevelopment: _isDevelopment);

  await runProcess(
    'fastlane',
    [
      'run',
      'update_project_team',
      'path:Runner.xcodeproj',
      'teamid:$teamId',
    ],
    workingDirectory: _iosDirectory,
  );

  if (!isProduction) {
    final buildVersion = platformBuild.flutterBuild.buildVersion;
    // Remove semver preRelease suffix
    // See: https://github.com/flutter/flutter/issues/27589
    if (buildVersion.isPreRelease) {
      platformBuild.flutterBuild.buildVersion =
          platformBuild.flutterBuild.buildVersion.copyWith(pre: null);
      _logger.info(
        'Build version was truncated from $buildVersion to '
        '${platformBuild.flutterBuild.buildVersion} as required by app store',
      );
    }
  }

  if (platformBuild.flutterBuild.buildVersion.build.isEmpty) {
    var versionCode = await _getLastVersionCodeFromAppStoreConnect(
      isProduction: isProduction,
      apiKeyJsonPath: apiKeyJsonPath,
    );
    if (versionCode != null) {
      // Increase versionCode by 1, if available:
      versionCode++;
      _logger.info(
        'Use "$versionCode" as next version code (fetched from App Store Connect).',
      );

      platformBuild.flutterBuild.buildVersion =
          platformBuild.flutterBuild.buildVersion.copyWith(
        build: versionCode.toString(),
      );
    }
  }

  _logger.info('Build application...');

  // Build xcarchive only
  final outputPath = await platformBuild.build();
  File? outputFile;

  if (outputPath.isEmpty) {
    _logger.warning('Build via flutter command finished failed silently. '
        'This can happen using manual signing with provisioning profiles.\n'
        'Therefore the app is now build again with fastlane. '
        'See: https://docs.flutter.dev/deployment/cd, '
        'and https://github.com/flutter/flutter/issues/106612');

    // Build signed ipa
    // https://docs.flutter.dev/deployment/cd
    // https://github.com/flutter/flutter/issues/106612

    _logger.info('Using XCode scheme "$xcodeScheme" to build the project.');

    await runAsyncProcess(
      printCall: true,
      'fastlane',
      [
        'run',
        'build_app',
        'scheme:$xcodeScheme',
        'skip_build_archive:true',
        'archive_path:../build/ios/archive/Runner.xcarchive',
      ],
      environment: {'FASTLANE_XCODEBUILD_SETTINGS_RETRIES': '15'},
      workingDirectory: _iosDirectory,
    );
  } else {
    _logger.info('Build artifact path: $outputPath');
    outputFile = File(outputPath);
  }

  if (flutterPublish.isDryRun) {
    _logger.info('Did NOT publish: Remove `--dry-run` flag for publishing.');
  } else {
    _logger.info('Publish...');
    if (!isProduction) {
      await runProcess(
        'fastlane',
        // upload_to_testflight
        [
          'pilot',
          'upload',
          '--skip_waiting_for_build_processing',
          '--api_key_path',
          apiKeyJsonPath,
          if (outputFile != null) ...[
            '--ipa',
            outputFile.absolute.path,
          ],
        ],
        workingDirectory: _iosDirectory,
        printCall: true,
      );
    } else {
      await runProcess(
        'fastlane',
        ['upload_to_app_store', '--api_key_path', apiKeyJsonPath],
        workingDirectory: _iosDirectory,
        printCall: true,
      );
    }
  }
  // Clean up
  await runProcess(
    'fastlane',
    [
      'run',
      'delete_keychain',
      'name:$fastlaneKeychainName',
    ],
    workingDirectory: _iosDirectory,
  );
}