testflight status check, distribute 추가

This commit is contained in:
leedongmyung 2025-05-23 15:42:58 +09:00
parent 2c203c4719
commit 4ac1f5671e
7 changed files with 316 additions and 55 deletions

View file

@ -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;
}

View file

@ -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<File> 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<String?> 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<String?> 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<String?> 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 {

View file

@ -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]);

View file

@ -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) {

View file

@ -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);
}
}

View file

@ -222,10 +222,15 @@ class DataBuildiOS extends DataParam {
late Map<String, String>? 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<String, dynamic> json) =>
@ -343,19 +348,38 @@ class DataTestflightUpload extends DataParam {
Map<String, dynamic> 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<String, dynamic> json) =>
_$DataiOSVersionFromJson(json);
factory DataTestflightStatusCheck.fromJson(Map<String, dynamic> json) =>
_$DataTestflightStatusCheckFromJson(json);
@override
Map<String, dynamic> toJson() => _$DataiOSVersionToJson(this);
Map<String, dynamic> 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<String, dynamic> json) =>
_$DataTestflightDistributeFromJson(json);
@override
Map<String, dynamic> toJson() => _$DataTestflightDistributeToJson(this);
}
//PublishiOS
@ -1144,3 +1168,20 @@ class DataSMBAuthorize extends DataParam {
@override
Map<String, dynamic> 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<String, dynamic> json) =>
_$DataDelayFromJson(json);
@override
Map<String, dynamic> toJson() => _$DataDelayToJson(this);
}

View file

@ -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<String, dynamic> json) => DataParam()
@ -394,10 +396,13 @@ DataBuildiOS _$DataBuildiOSFromJson(Map<String, dynamic> json) => DataBuildiOS()
..settings = (json['settings'] as Map<String, dynamic>?)?.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<String, dynamic> _$DataBuildiOSToJson(DataBuildiOS instance) =>
@ -416,10 +421,13 @@ Map<String, dynamic> _$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<String, dynamic> _$DataTestflightUploadToJson(
'ipaPath': instance.ipaPath,
};
DataiOSVersion _$DataiOSVersionFromJson(Map<String, dynamic> json) =>
DataiOSVersion()
DataTestflightStatusCheck _$DataTestflightStatusCheckFromJson(
Map<String, dynamic> json) =>
DataTestflightStatusCheck()
..workspace = json['workspace'] as String?
..passExitCodes = (json['passExitCodes'] as List<dynamic>?)
?.map((e) => (e as num).toInt())
@ -625,20 +634,54 @@ DataiOSVersion _$DataiOSVersionFromJson(Map<String, dynamic> 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<String, dynamic> _$DataiOSVersionToJson(DataiOSVersion instance) =>
Map<String, dynamic> _$DataTestflightStatusCheckToJson(
DataTestflightStatusCheck instance) =>
<String, dynamic>{
'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<String, dynamic> json) =>
DataTestflightDistribute()
..workspace = json['workspace'] as String?
..passExitCodes = (json['passExitCodes'] as List<dynamic>?)
?.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<String, dynamic> _$DataTestflightDistributeToJson(
DataTestflightDistribute instance) =>
<String, dynamic>{
'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<String, dynamic> json) =>
@ -1955,3 +1998,28 @@ Map<String, dynamic> _$DataSMBAuthorizeToJson(DataSMBAuthorize instance) =>
'id': instance.id,
'password': instance.password,
};
DataDelay _$DataDelayFromJson(Map<String, dynamic> json) => DataDelay()
..workspace = json['workspace'] as String?
..passExitCodes = (json['passExitCodes'] as List<dynamic>?)
?.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<String, dynamic> _$DataDelayToJson(DataDelay instance) => <String, dynamic>{
'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,
};