From 4ac1f5671e408b162caae9f1fee4387404b30397 Mon Sep 17 00:00:00 2001 From: leedongmyung Date: Fri, 23 May 2025 15:42:58 +0900 Subject: [PATCH] =?UTF-8?q?testflight=20status=20check,=20distribute=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/oto/application.dart | 4 +- lib/oto/commands/build/build_ios.dart | 103 ++++++++++++++++----- lib/oto/commands/build/testflight_ios.dart | 67 ++++++++++++-- lib/oto/commands/command.dart | 11 ++- lib/oto/commands/util/timer.dart | 35 +++++++ lib/oto/data/command_data.dart | 63 ++++++++++--- lib/oto/data/command_data.g.dart | 88 ++++++++++++++++-- 7 files changed, 316 insertions(+), 55 deletions(-) create mode 100644 lib/oto/commands/util/timer.dart diff --git a/lib/oto/application.dart b/lib/oto/application.dart index 54127fb..3c0b877 100644 --- a/lib/oto/application.dart +++ b/lib/oto/application.dart @@ -174,9 +174,9 @@ class Application { Future printBuildStep(String name, Color? color) async { var message = - '''$enter************************************************************************************ + '''$enter********************************************************************************************* * $name -************************************************************************************'''; +*********************************************************************************************'''; await CLI.printString(message, color: color ?? Color.green); return simpleFuture; } diff --git a/lib/oto/commands/build/build_ios.dart b/lib/oto/commands/build/build_ios.dart index 15405bb..e10d314 100644 --- a/lib/oto/commands/build/build_ios.dart +++ b/lib/oto/commands/build/build_ios.dart @@ -5,7 +5,6 @@ import 'dart:io'; import 'package:dart_framework/utils/string_util.dart'; import 'package:dart_framework/utils/system_util.dart'; import 'package:oto_cli/oto/commands/build/fastlane_template.dart'; -import 'package:oto_cli/oto/commands/file/file.dart'; import 'package:xml/xml.dart'; import 'package:oto_cli/oto/application.dart'; import 'package:oto_cli/oto/commands/build/build_flutter.dart'; @@ -32,7 +31,10 @@ class BuildiOS extends Command { String? buildNumber = data.buildNumber; var xcodeproj = data.xcodeProjectFilePath ?? await getMainXcodeprojPath(data.xcworkspaceFilePath!); - if (data.autoBuildNumber ?? false) { + + /************ Auto find build number ************/ + var autoBuildNumber = (data.autoBuildNumber ?? false) ? !((version != null && version.isNotEmpty) || (buildNumber != null && buildNumber.isNotEmpty)) : false; + if (autoBuildNumber) { var appID = await getAppIdentifier( data.workspace, xcodeproj, data.scheme, data.setAppID); if (appID == null) { @@ -42,27 +44,12 @@ class BuildiOS extends Command { throw Exception( 'If autoVersionNumber = true then must set apiKeyPath.'); } - var json = jsonDecode(File(data.apiKeyPath!).readAsStringSync()); - var fastlaneContent = fastlaneTemplete; - fastlaneContent = fastlaneContent - .replaceFirst('[APP_IDENTIFIER]', appID) - .replaceFirst('[API_KEY_ID]', json['key_id']) - .replaceFirst('[API_ISSUER_ID]', json['issuer_id']) - .replaceFirst('[AUTH_KEY_CONTENT]', - json['key'].toString().replaceAll('\n', '\\n')); - - var shell = StringBuffer(); - shell.write('cd ${data.workspace}'); - shell.write(' && rm -rf "${data.workspace}/fastlane"'); - shell.write(' && yes | fastlane init'); - await ProcessExecutor.run(shell); - - var fastFile = File('${data.workspace}/fastlane/Fastfile'); - fastFile.writeAsStringSync(fastlaneContent, flush: true); + var fastFile = await initializeFastlane(data.workspace!, data.apiKeyPath!, appID); + var fastlaneContent = fastFile.readAsStringSync(); //Find PREPARE_FOR_SUBMISSION state Version String? targetVersion; - shell = StringBuffer(); + var shell = StringBuffer(); shell.write('cd ${data.workspace}/fastlane'); shell.write(' && fastlane appstore_versions'); var process = await ProcessExecutor.run(shell); @@ -98,7 +85,6 @@ class BuildiOS extends Command { shell.write(' && fastlane latest_build_number'); process = await ProcessExecutor.run(shell); arr = process.stdout.toString().split('\n'); - for (var item in arr) { if (item.contains(result)) { var json = @@ -113,15 +99,29 @@ class BuildiOS extends Command { buildNumber = incrementVersion(targetBuildNumber); } - if (version != null) { + String? target = data.target; + //version이 있을경우 세팅, 없을경우 기존 버전값을 메모리에 가지고 있음 + if (version != null && version.isNotEmpty) { await updateMarketingVersion( '${data.workspace}/$xcodeproj/project.pbxproj', version); + } else { + target = target ?? await getTargetXcodeproj(data.workspace!, xcodeproj!); + version = await getSettingXcodeproj(data.workspace!, xcodeproj!, target!, 'MARKETING_VERSION'); } - if (buildNumber != null) { + //buildNumber가 있을경우 세팅, 없을경우 기존 버전값을 메모리에 가지고 있음 + if (buildNumber != null && buildNumber.isNotEmpty) { await updateBuildNumber( '${data.workspace}/$xcodeproj/project.pbxproj', buildNumber); + } else { + target = target ?? await getTargetXcodeproj(data.workspace!, xcodeproj!); + buildNumber = await getSettingXcodeproj(data.workspace!, xcodeproj!, target!, 'CURRENT_PROJECT_VERSION'); } + var setVersion = data.setVersion != null && data.setVersion!.isNotEmpty ? data.setVersion! : '<@property.version>'; + setProperty(setVersion, version); + var setBuildNumber = data.setBuildNumber != null && data.setBuildNumber!.isNotEmpty ? data.setBuildNumber! : '<@property.buildNumber>'; + setProperty(setBuildNumber, buildNumber); + var os = 'iOS'; if (data.os != null) { if (data.os == BuildPlatform.Macos) { @@ -193,6 +193,27 @@ class BuildiOS extends Command { } } + static Future initializeFastlane(String workspace, String apiKeyPath, String appID) async { + var json = jsonDecode(File(apiKeyPath).readAsStringSync()); + var fastlaneContent = fastlaneTemplete; + fastlaneContent = fastlaneContent + .replaceFirst('[APP_IDENTIFIER]', appID) + .replaceFirst('[API_KEY_ID]', json['key_id']) + .replaceFirst('[API_ISSUER_ID]', json['issuer_id']) + .replaceFirst('[AUTH_KEY_CONTENT]', + json['key'].toString().replaceAll('\n', '\\n')); + + var shell = StringBuffer(); + shell.write('cd $workspace'); + shell.write(' && rm -rf "$workspace/fastlane"'); + shell.write(' && yes | fastlane init'); + await ProcessExecutor.run(shell); + + var fastFile = File('$workspace/fastlane/Fastfile'); + fastFile.writeAsStringSync(fastlaneContent, flush: true); + return dataFutrue(fastFile); + } + Future getAppIdentifier(String? workspace, String? xcodeproj, String scheme, String? setAppID) async { String? identifier; @@ -302,6 +323,42 @@ class BuildiOS extends Command { await file.writeAsString(updatedLines.join('\n')); } + + Future getSettingXcodeproj(String workspace, String xcodeproj, String target, String marker) async { + String? version; + var shell = StringBuffer(); + shell.write('cd $workspace'); + shell.write(' && xcodebuild -project $xcodeproj -target $target -showBuildSettings | grep $marker'); + + var mark = '$marker = '; + var process = await ProcessExecutor.run(shell); + var arr = process.stdout.toString().split('\n'); + for(var item in arr) { + if (item.contains(mark)) { + version = item.replaceAll(mark, '').trim(); + break; + } + } + return dataFutrue(version); + } + + Future getTargetXcodeproj(String workspace, String xcoodeproj) async { + String? target; + var shell = StringBuffer(); + shell.write('cd $workspace'); + shell.write(" && xcodebuild -project $xcoodeproj -list | awk '/Targets:/ {getline; print; exit}'"); + + var process = await ProcessExecutor.run(shell); + var arr = process.stdout.toString().split('\n'); + for(var item in arr) { + item = item.trim(); + if (item.isNotEmpty) { + target = item.trim(); + break; + } + } + return dataFutrue(target); + } } class CodeSign extends Command { diff --git a/lib/oto/commands/build/testflight_ios.dart b/lib/oto/commands/build/testflight_ios.dart index 8632c30..cfc3d14 100644 --- a/lib/oto/commands/build/testflight_ios.dart +++ b/lib/oto/commands/build/testflight_ios.dart @@ -1,5 +1,8 @@ +import 'dart:convert'; + import 'package:dart_framework/platform/process.dart'; import 'package:oto_cli/oto/application.dart'; +import 'package:oto_cli/oto/commands/build/build_ios.dart'; import 'package:oto_cli/oto/commands/command.dart'; import 'package:oto_cli/oto/data/command_data.dart'; @@ -25,19 +28,71 @@ class TestflightUpload extends Command { } } -class IOSVersion extends Command { +class TestflightStatusCheck extends Command { Function? get error => null; @override Future execute(DataCommand command) async { - var data = DataiOSVersion.fromJson(getParam(command)); + var data = DataTestflightStatusCheck.fromJson(getParam(command)); + var fastFile = await BuildiOS.initializeFastlane(data.workspace!, data.apiKeyPath, data.appID); + var fastlaneContent = fastFile.readAsStringSync(); + + fastlaneContent = fastlaneContent.replaceFirst('[TARGET_VERSION]', data.version) + .replaceFirst('[TARGET_BUILD_NUMBER]', data.buildNumber); + fastFile.writeAsStringSync(fastlaneContent, flush: true); + + var messageInReview = 'Review in progress.'; + var messageComplete = 'Your review has been completed 🎉'; + const result = '[RESULT]: '; + var isProcessing = true; + while(isProcessing) { + var shell = StringBuffer(); + shell.write('cd ${data.workspace}/fastlane'); + shell.write(' && fastlane testflight_status'); + + var process = await ProcessExecutor.run(shell); + var arr = process.stdout.toString().split('\n'); + for (var item in arr) { + if (item.contains(result)) { + var json = + jsonDecode(item.substring(item.indexOf(result) + result.length)); + if(json['state'] == 'VALID') { + isProcessing = false; + } + break; + } + } + + if(isProcessing) { + print(messageInReview); + messageInReview += '.'; + } else { + print(messageComplete); + } + } + return await complete(0, command); + } +} + +class TestflightDistribute extends Command { + Function? get error => null; + + @override + Future execute(DataCommand command) async { + var data = DataTestflightDistribute.fromJson(getParam(command)); + + var testers = data.testers != null && data.testers!.isNotEmpty ? ' --groups "${data.testers}"' : ''; + var message = data.message != null && data.message!.isNotEmpty ? ' --changelog "${data.message}"' : ''; + var platform = data.platform ?? 'ios'; + + print('Distribute is processing...'); var shell = StringBuffer(); - shell.write("echo 'test'"); + shell.write('cd ${data.workspace}/fastlane'); + shell.write( + ' && fastlane pilot distribute --api_key_path ${data.apiKeyPath} --app_identifier ${data.appID}$testers$message --app-platform $platform'); - var process = await ProcessExecutor.start(shell, - workspace: workspace, - printStderr: false, + var process = await ProcessExecutor.start(shell, logHandler: Application.logWithType); return await completeProcess(process, command, passExitCodes: [1]); diff --git a/lib/oto/commands/command.dart b/lib/oto/commands/command.dart index fb301ba..3404fcb 100644 --- a/lib/oto/commands/command.dart +++ b/lib/oto/commands/command.dart @@ -31,6 +31,7 @@ import 'package:oto_cli/oto/commands/util/print.dart'; import 'package:oto_cli/oto/commands/util/set_value.dart'; import 'package:oto_cli/oto/commands/util/smb_authorize.dart'; import 'package:oto_cli/oto/commands/util/string_util.dart'; +import 'package:oto_cli/oto/commands/util/timer.dart'; import 'package:oto_cli/oto/commands/web/web.dart'; import 'package:oto_cli/oto/data/command_data.dart'; import 'package:oto_cli/oto/data/jenkins_data.dart'; @@ -55,7 +56,8 @@ enum CommandType { BuildDotNet, BuildiOS, TestflightUpload, - IOSVersion, + TestflightStatusCheck, + TestflightDistribute, BuildMSBuild, CodeSign, CodeSignVerify, @@ -111,7 +113,8 @@ enum CommandType { ProcessPortUse, ProcessRun, ProcessKill, - SMBAuth + SMBAuth, + Delay } abstract class Command { @@ -126,7 +129,8 @@ abstract class Command { CommandType.BuildDotNet: () => BuildDotNet(), CommandType.BuildiOS: () => BuildiOS(), CommandType.TestflightUpload: () => TestflightUpload(), - CommandType.IOSVersion: () => IOSVersion(), + CommandType.TestflightStatusCheck: () => TestflightStatusCheck(), + CommandType.TestflightDistribute: () => TestflightDistribute(), CommandType.BuildMSBuild: () => BuildMSBuild(), CommandType.CodeSign: () => CodeSign(), CommandType.CodeSignVerify: () => CodeSignVerify(), @@ -183,6 +187,7 @@ abstract class Command { CommandType.ProcessRun: () => ProcessRun(), CommandType.ProcessKill: () => ProcessKill(), CommandType.SMBAuth: () => SMBAuth(), + CommandType.Delay: () => Delay(), }; factory Command.byType(CommandType type) { diff --git a/lib/oto/commands/util/timer.dart b/lib/oto/commands/util/timer.dart new file mode 100644 index 0000000..0d786de --- /dev/null +++ b/lib/oto/commands/util/timer.dart @@ -0,0 +1,35 @@ + + +import 'package:oto_cli/oto/commands/command.dart'; +import 'package:oto_cli/oto/data/command_data.dart'; + +class Delay extends Command { + Function? get error => null; + + @override + Future execute(DataCommand command) async { + var data = DataDelay.fromJson(getParam(command)); + + if(data.millisecond != null) { + print('Delay: ${data.millisecond} millisecond'); + await Future.delayed(Duration(milliseconds: data.millisecond!)); + } + + if(data.second != null) { + print('Delay: ${data.second} second'); + await Future.delayed(Duration(seconds: data.second!)); + } + + if(data.minute != null) { + print('Delay: ${data.minute} minute'); + await Future.delayed(Duration(minutes: data.minute!)); + } + + if(data.hour != null) { + print('Delay: ${data.hour} hour'); + await Future.delayed(Duration(hours: data.hour!)); + } + + return await complete(0, command); + } +} \ No newline at end of file diff --git a/lib/oto/data/command_data.dart b/lib/oto/data/command_data.dart index 117e4b3..cca6e04 100644 --- a/lib/oto/data/command_data.dart +++ b/lib/oto/data/command_data.dart @@ -222,10 +222,15 @@ class DataBuildiOS extends DataParam { late Map? settings; //Version Setting - late String? version; - late String? buildNumber; + late String? target; //없을시 xcodeproj 자동 검색하여 최상단 target을 가져옴 + late String? version; //존재시 autoBuildNumber = false + late String? buildNumber; //존재시 autoBuildNumber = false late bool? autoBuildNumber = false; late String? apiKeyPath; //autoVersionNumber 사용시 필수 + + //set + late String? setVersion; + late String? setBuildNumber; late String? setAppID; factory DataBuildiOS.fromJson(Map json) => @@ -343,19 +348,38 @@ class DataTestflightUpload extends DataParam { Map toJson() => _$DataTestflightUploadToJson(this); } -//iOSVersion +//TestflightReviewCheck @JsonSerializable() -class DataiOSVersion extends DataParam { - DataiOSVersion(); +class DataTestflightStatusCheck extends DataParam { + DataTestflightStatusCheck(); - late String scheme; - late String xcworkspacePath; - late String? setAppID; + late String appID; + late String apiKeyPath; + late String version; + late String buildNumber; - factory DataiOSVersion.fromJson(Map json) => - _$DataiOSVersionFromJson(json); + factory DataTestflightStatusCheck.fromJson(Map json) => + _$DataTestflightStatusCheckFromJson(json); @override - Map toJson() => _$DataiOSVersionToJson(this); + Map toJson() => _$DataTestflightStatusCheckToJson(this); +} + + +//TestflightUpload +@JsonSerializable() +class DataTestflightDistribute extends DataParam { + DataTestflightDistribute(); + + late String apiKeyPath; + late String appID; + late String? message; + late String? testers; + late String? platform; + + factory DataTestflightDistribute.fromJson(Map json) => + _$DataTestflightDistributeFromJson(json); + @override + Map toJson() => _$DataTestflightDistributeToJson(this); } //PublishiOS @@ -1144,3 +1168,20 @@ class DataSMBAuthorize extends DataParam { @override Map toJson() => _$DataSMBAuthorizeToJson(this); } + +//Smb Authorize +@JsonSerializable() +class DataDelay extends DataParam { + DataDelay(); + + late int? millisecond; + late int? second; + late int? minute; + late int? hour; + + factory DataDelay.fromJson(Map json) => + _$DataDelayFromJson(json); + @override + Map toJson() => _$DataDelayToJson(this); +} + diff --git a/lib/oto/data/command_data.g.dart b/lib/oto/data/command_data.g.dart index 19d4e11..c389dcd 100644 --- a/lib/oto/data/command_data.g.dart +++ b/lib/oto/data/command_data.g.dart @@ -63,7 +63,8 @@ const _$CommandTypeEnumMap = { CommandType.BuildDotNet: 'BuildDotNet', CommandType.BuildiOS: 'BuildiOS', CommandType.TestflightUpload: 'TestflightUpload', - CommandType.IOSVersion: 'IOSVersion', + CommandType.TestflightStatusCheck: 'TestflightStatusCheck', + CommandType.TestflightDistribute: 'TestflightDistribute', CommandType.BuildMSBuild: 'BuildMSBuild', CommandType.CodeSign: 'CodeSign', CommandType.CodeSignVerify: 'CodeSignVerify', @@ -120,6 +121,7 @@ const _$CommandTypeEnumMap = { CommandType.ProcessRun: 'ProcessRun', CommandType.ProcessKill: 'ProcessKill', CommandType.SMBAuth: 'SMBAuth', + CommandType.Delay: 'Delay', }; DataParam _$DataParamFromJson(Map json) => DataParam() @@ -394,10 +396,13 @@ DataBuildiOS _$DataBuildiOSFromJson(Map json) => DataBuildiOS() ..settings = (json['settings'] as Map?)?.map( (k, e) => MapEntry(k, e as String), ) + ..target = json['target'] as String? ..version = json['version'] as String? ..buildNumber = json['buildNumber'] as String? ..autoBuildNumber = json['autoBuildNumber'] as bool? ..apiKeyPath = json['apiKeyPath'] as String? + ..setVersion = json['setVersion'] as String? + ..setBuildNumber = json['setBuildNumber'] as String? ..setAppID = json['setAppID'] as String?; Map _$DataBuildiOSToJson(DataBuildiOS instance) => @@ -416,10 +421,13 @@ Map _$DataBuildiOSToJson(DataBuildiOS instance) => 'cleanPod': instance.cleanPod, 'configuration': instance.configuration, 'settings': instance.settings, + 'target': instance.target, 'version': instance.version, 'buildNumber': instance.buildNumber, 'autoBuildNumber': instance.autoBuildNumber, 'apiKeyPath': instance.apiKeyPath, + 'setVersion': instance.setVersion, + 'setBuildNumber': instance.setBuildNumber, 'setAppID': instance.setAppID, }; @@ -616,8 +624,9 @@ Map _$DataTestflightUploadToJson( 'ipaPath': instance.ipaPath, }; -DataiOSVersion _$DataiOSVersionFromJson(Map json) => - DataiOSVersion() +DataTestflightStatusCheck _$DataTestflightStatusCheckFromJson( + Map json) => + DataTestflightStatusCheck() ..workspace = json['workspace'] as String? ..passExitCodes = (json['passExitCodes'] as List?) ?.map((e) => (e as num).toInt()) @@ -625,20 +634,54 @@ DataiOSVersion _$DataiOSVersionFromJson(Map json) => ..setResult = json['setResult'] as String? ..setExitCode = json['setExitCode'] as String? ..showPrint = json['showPrint'] as bool? - ..scheme = json['scheme'] as String - ..xcworkspacePath = json['xcworkspacePath'] as String - ..setAppID = json['setAppID'] as String?; + ..appID = json['appID'] as String + ..apiKeyPath = json['apiKeyPath'] as String + ..version = json['version'] as String + ..buildNumber = json['buildNumber'] as String; -Map _$DataiOSVersionToJson(DataiOSVersion instance) => +Map _$DataTestflightStatusCheckToJson( + DataTestflightStatusCheck instance) => { 'workspace': instance.workspace, 'passExitCodes': instance.passExitCodes, 'setResult': instance.setResult, 'setExitCode': instance.setExitCode, 'showPrint': instance.showPrint, - 'scheme': instance.scheme, - 'xcworkspacePath': instance.xcworkspacePath, - 'setAppID': instance.setAppID, + 'appID': instance.appID, + 'apiKeyPath': instance.apiKeyPath, + 'version': instance.version, + 'buildNumber': instance.buildNumber, + }; + +DataTestflightDistribute _$DataTestflightDistributeFromJson( + Map json) => + DataTestflightDistribute() + ..workspace = json['workspace'] as String? + ..passExitCodes = (json['passExitCodes'] as List?) + ?.map((e) => (e as num).toInt()) + .toList() + ..setResult = json['setResult'] as String? + ..setExitCode = json['setExitCode'] as String? + ..showPrint = json['showPrint'] as bool? + ..apiKeyPath = json['apiKeyPath'] as String + ..appID = json['appID'] as String + ..message = json['message'] as String? + ..testers = json['testers'] as String? + ..platform = json['platform'] as String?; + +Map _$DataTestflightDistributeToJson( + DataTestflightDistribute instance) => + { + 'workspace': instance.workspace, + 'passExitCodes': instance.passExitCodes, + 'setResult': instance.setResult, + 'setExitCode': instance.setExitCode, + 'showPrint': instance.showPrint, + 'apiKeyPath': instance.apiKeyPath, + 'appID': instance.appID, + 'message': instance.message, + 'testers': instance.testers, + 'platform': instance.platform, }; DataPublishiOS _$DataPublishiOSFromJson(Map json) => @@ -1955,3 +1998,28 @@ Map _$DataSMBAuthorizeToJson(DataSMBAuthorize instance) => 'id': instance.id, 'password': instance.password, }; + +DataDelay _$DataDelayFromJson(Map json) => DataDelay() + ..workspace = json['workspace'] as String? + ..passExitCodes = (json['passExitCodes'] as List?) + ?.map((e) => (e as num).toInt()) + .toList() + ..setResult = json['setResult'] as String? + ..setExitCode = json['setExitCode'] as String? + ..showPrint = json['showPrint'] as bool? + ..millisecond = (json['millisecond'] as num?)?.toInt() + ..second = (json['second'] as num?)?.toInt() + ..minute = (json['minute'] as num?)?.toInt() + ..hour = (json['hour'] as num?)?.toInt(); + +Map _$DataDelayToJson(DataDelay instance) => { + 'workspace': instance.workspace, + 'passExitCodes': instance.passExitCodes, + 'setResult': instance.setResult, + 'setExitCode': instance.setExitCode, + 'showPrint': instance.showPrint, + 'millisecond': instance.millisecond, + 'second': instance.second, + 'minute': instance.minute, + 'hour': instance.hour, + };