diff --git a/build-release b/Tools/build-release similarity index 100% rename from build-release rename to Tools/build-release diff --git a/Tools/xcode-coverage-report b/Tools/xcode-coverage-report new file mode 100755 index 0000000..ea398d1 --- /dev/null +++ b/Tools/xcode-coverage-report @@ -0,0 +1,50 @@ +#!/bin/bash +function printHelp() { +cat < ] + +Reads Xcode coverage data and outputs it in a format suitable for phabit ingestion. +HELP +} + +build_dir="" +while (( $# > 0 )) +do + if [[ $1 == -h ]] + then + printHelp + exit 0 + fi + if (( ${#build_dir} != 0 )) + then + echo ">>> Error: Build directory already set: '$build_dir'. Scanning '$1'." 1>&2 + exit 1 + fi + build_dir="$1" + shift +done + +if (( ${#build_dir} == 0 )) +then + if [ -e "./Build" ] + then + build_dir="./Build" + else + build_dir=".build" + fi +fi + +test_run=$(echo ./Build/Logs/Test/**/**/*.xccovarchive) +if (( $(echo "$test_run" | wc -l) != 1 )) +then + echo ">>> Error: can't find xccovarchive file: '$test_run'." 1>&2 + exit 1 +fi + +while IFS= read -r file +do + echo "File: \"$file\"" + xcrun xccov view \ + --file "$file" \ + "$test_run" +done < <(xcrun xccov view --file-list "$test_run") diff --git a/XcodeGitHub/XGCommand.m b/XcodeGitHub/XGCommand.m index 3afeabd..86a2357 100644 --- a/XcodeGitHub/XGCommand.m +++ b/XcodeGitHub/XGCommand.m @@ -153,10 +153,12 @@ message:message statusURL:nil]; if (error) return error; + + // Add a completion message to the PR: if ([botStatus.currentStep isEqualToString:@"completed"]) { - error = [pr addComment:[botStatus.formattedDetailString renderMarkDown]]; + error = [pr addComment:[[botStatus formatDetailString:options.successMessage :options.failureMessage :options.perfectMessage] renderMarkDown]]; if (error) return error; } diff --git a/XcodeGitHub/XGCommandOptions.h b/XcodeGitHub/XGCommandOptions.h index 5e047d2..4dc6628 100644 --- a/XcodeGitHub/XGCommandOptions.h +++ b/XcodeGitHub/XGCommandOptions.h @@ -18,6 +18,9 @@ NS_ASSUME_NONNULL_BEGIN @property (copy) NSString*_Nullable xcodeServerPassword; // Optional @property (copy) NSString*_Nullable templateBotName; @property (copy) NSString*_Nullable githubAuthToken; +@property (copy) NSString*_Nullable successMessage; +@property (copy) NSString*_Nullable failureMessage; +@property (copy) NSString*_Nullable perfectMessage; @property (assign) int verbosity; @property (assign) BOOL dryRun; @property (assign) BOOL showStatusOnly; diff --git a/XcodeGitHub/XGCommandOptions.m b/XcodeGitHub/XGCommandOptions.m index b7ad1f9..b7c2b50 100644 --- a/XcodeGitHub/XGCommandOptions.m +++ b/XcodeGitHub/XGCommandOptions.m @@ -29,6 +29,9 @@ - (instancetype _Nonnull) initWithArgc:(int)argc argv:(char*const _Nullable[_Nul {"verbose", no_argument, NULL, 'v'}, {"version", no_argument, NULL, 'V'}, {"xcodeserver", required_argument, NULL, 'x'}, + {"successmessage", no_argument, NULL, 'S'}, + {"failuremessage", no_argument, NULL, 'F'}, + {"perfectmessage", no_argument, NULL, 'P'}, {0, 0, 0, 0} }; @@ -54,6 +57,9 @@ - (instancetype _Nonnull) initWithArgc:(int)argc argv:(char*const _Nullable[_Nul case 'v': self.verbosity++; break; case 'V': self.showVersion = YES; break; case 'x': self.xcodeServerName = [self.class stringFromParameter]; break; + case 'S': self.successMessage = [self.class stringFromParameter]; break; + case 'F': self.failureMessage = [self.class stringFromParameter]; break; + case 'P': self.perfectMessage = [self.class stringFromParameter]; break; default: self.badOptionsError = YES; break; } } while (c != -1 && !self.badOptionsError); @@ -107,6 +113,14 @@ + (NSString*) helpString { "\n" " -x, --xcodeserver \n" " The network name of the xcode server.\n" + " -S, --successmessage \n" + " Optional custom integration success message.\n" + "\n" + " -F, --failuremessage \n" + " Optional custom integration failure message.\n" + "\n" + " -P, --perfectmessage \n" + " Optional custom perfect integration message.\n" "\n" "The tool returns 0 on success, otherwise a non-zero value.\n" "\n" diff --git a/XcodeGitHub/XGXcodeBot.h b/XcodeGitHub/XGXcodeBot.h index 8388fca..be77df7 100644 --- a/XcodeGitHub/XGXcodeBot.h +++ b/XcodeGitHub/XGXcodeBot.h @@ -80,10 +80,11 @@ NS_ASSUME_NONNULL_BEGIN @property (strong, readonly) NSArray*_Nullable tags; @property (strong, readonly) NSString* summaryString; -@property (strong, readonly) APFormattedString* formattedDetailString; @property (strong, readonly) NSURL* integrationLogURL; +- (APFormattedString*) formatDetailString:(NSString*_Nullable) successMessage : (NSString*_Nullable) failureMessage : (NSString*_Nullable) perfectMessage; + - (instancetype) initWithServerName:(NSString*_Nullable)serverName dictionary:(NSDictionary*_Nullable)dictionary NS_DESIGNATED_INITIALIZER; diff --git a/XcodeGitHub/XGXcodeBot.m b/XcodeGitHub/XGXcodeBot.m index abf3632..a3faaf0 100644 --- a/XcodeGitHub/XGXcodeBot.m +++ b/XcodeGitHub/XGXcodeBot.m @@ -132,112 +132,125 @@ - (NSString*) summaryString { _Test Coverage_: 65% (193 tests). */ -- (APFormattedString*) formattedDetailString { +- (APFormattedString*) formatDetailString:(NSString*_Nullable)successMessage :(NSString*_Nullable) failureMessage :(NSString*_Nullable)perfectMessage { NSTimeInterval duration = [self.endedDate timeIntervalSinceDate:self.startedDate]; - NSString*durationString = XGDurationStringFromTimeInterval(duration); - - APFormattedString *apstring = - [[[[APFormattedString - plainText:@"Result of Integration %@", self.integrationNumber] - line] - italicText:@"Duration"] - plainText:@": %@\n", durationString]; - - if ([self.result isEqualToString:@"canceled"]) { - [[[apstring - plainText:@"Build was "] - boldText:@"**manually canceled**"] - plainText:@"."]; - return apstring; - } + NSString*durationString = XGDurationStringFromTimeInterval(duration); + + APFormattedString *apstring = + [[[[APFormattedString + plainText:@"Result of Integration %@", self.integrationNumber] + line] + italicText:@"Duration"] + plainText:@": %@\n", durationString]; + + if ([self.result isEqualToString:@"canceled"]) { + [[[apstring + plainText:@"Build was "] + boldText:@"**manually canceled**"] + plainText:@"."]; + return apstring; + } + + [[apstring + italicText:@"Result"] + plainText:@": "]; + + if ([self.errorCount integerValue] > 0) { + [[[apstring + boldText:@"%@ errors, failing state: %@", self.errorCount, self.summaryString] + plainText:@"\n%@", failureMessage] + line]; + return apstring; + } + + if ([self.testFailureCount integerValue] > 0) { + [[[[apstring + plainText:@"%@", failureMessage] + line] + boldText:@"Build failed %@ tests", self.testFailureCount] + plainText:@" out of %@", self.testsCount]; + return apstring; + } + + if ([self.testsCount integerValue] > 0 && + [self.warningCount integerValue] > 0 && + [self.analyzerWarningCount integerValue] > 0) { + [[[[[apstring + plainText:@"All %@ tests passed, but please ", self.testsCount] + boldText:@"fix %@ warnings", self.warningCount] + plainText:@" and "] + boldText:@"%@ analyzer warnings", self.analyzerWarningCount] + plainText:@"."]; + if ([self.codeCoveragePercentage doubleValue] > 0) { + [[apstring + italicText:@"\nTest Coverage"] + plainText:@": %@%%", self.codeCoveragePercentage]; + } + [[apstring plainText:@"\n%@", successMessage] line]; + return apstring; + } + + if ([self.testsCount integerValue] > 0 && + [self.warningCount integerValue] > 0) { + [[apstring + plainText:@"All %@ tests passed, but please ", self.testsCount] + boldText:@"fix %@ warnings.", self.warningCount]; + if ([self.codeCoveragePercentage doubleValue] > 0) { + [[apstring + italicText:@"\nTest Coverage"] + plainText:@": %@%%", self.codeCoveragePercentage]; + } + [[apstring plainText:@"\n%@", successMessage] line]; + return apstring; + } + + if ([self.testsCount integerValue] > 0 && + [self.analyzerWarningCount integerValue] > 0) { + [[apstring + plainText:@"All %@ tests passed, but please ", self.testsCount] + boldText:@"fix %@ analyzer warnings.", self.analyzerWarningCount]; + if ([self.codeCoveragePercentage doubleValue] > 0) { + [[apstring + italicText:@"\nTest Coverage"] + plainText:@": %@%%", self.codeCoveragePercentage]; + } + return apstring; + } - [[apstring - italicText:@"Result"] - plainText:@": "]; - - if ([self.errorCount integerValue] > 0) { - [apstring boldText:@"%@ errors, failing state: %@", self.errorCount, self.summaryString]; - return apstring; - } - - if ([self.testFailureCount integerValue] > 0) { - [[apstring boldText:@"Build failed %@ tests", self.testFailureCount] - plainText:@" out of %@", self.testsCount]; - return apstring; - } - - if ([self.testsCount integerValue] > 0 && - [self.warningCount integerValue] > 0 && - [self.analyzerWarningCount integerValue] > 0) { - [[[[[apstring - plainText:@"All %@ tests passed, but please ", self.testsCount] - boldText:@"fix %@ warnings", self.warningCount] - plainText:@" and "] - boldText:@"%@ analyzer warnings", self.analyzerWarningCount] - plainText:@"."]; - if ([self.codeCoveragePercentage doubleValue] > 0) { - [[apstring - italicText:@"\nTest Coverage"] - plainText:@": %@%%", self.codeCoveragePercentage]; - } - return apstring; - } - - if ([self.testsCount integerValue] > 0 && - [self.warningCount integerValue] > 0) { - [[apstring - plainText:@"All %@ tests passed, but please ", self.testsCount] - boldText:@"fix %@ warnings.", self.warningCount]; - if ([self.codeCoveragePercentage doubleValue] > 0) { - [[apstring - italicText:@"\nTest Coverage"] - plainText:@": %@%%", self.codeCoveragePercentage]; - } - return apstring; - } - - if ([self.testsCount integerValue] > 0 && - [self.analyzerWarningCount integerValue] > 0) { - [[apstring - plainText:@"All %@ tests passed, but please ", self.testsCount] - boldText:@"fix %@ analyzer warnings.", self.analyzerWarningCount]; - if ([self.codeCoveragePercentage doubleValue] > 0) { - [[apstring - italicText:@"\nTest Coverage"] - plainText:@": %@%%", self.codeCoveragePercentage]; - } - return apstring; - } - - if ([self.errorCount integerValue] == 0 && [self.result isEqualToString:@"succeeded"]) { - if (self.testsCount.integerValue == 0) { - [apstring boldText:@"Perfect build! 👍"]; - } else { - if (self.testFailureCount.integerValue == 0) { - [apstring boldText:@"Perfect build!"]; - [apstring plainText:@" All %ld tests passed. 👍\n", (long) self.testsCount.integerValue]; - if (self.codeCoveragePercentage.doubleValue > 0.0) { - [[apstring - italicText:@"Test Coverage"] - plainText:@": %@%%", self.codeCoveragePercentage]; - } - } else { - [apstring boldText:@"Perfect build!"]; - [apstring plainText:@" But please fix %ld failing tests.\n", (long) self.testFailureCount.integerValue]; - [[apstring - italicText:@"Test Coverage"] - plainText:@": %@%% (%@ tests).", self.codeCoveragePercentage, self.testsCount]; - } - } - return apstring; - } - - [apstring boldText:@"Failing state: %@.", self.summaryString]; - if ([self.tags containsObject:@"xcs-upgrade"]) { - [apstring italicText:@"\nThe current configuration may not be supported by the Xcode upgrade."]; - } - - return apstring; + if ([self.testsCount integerValue] == 0) { + [apstring boldText:@"Build Succeeded"]; + [apstring plainText:@"\nNo tests ran on this integration"]; + [apstring plainText:@"\n%@", successMessage]; + return apstring; + } + + if ([self.errorCount integerValue] == 0 && [self.result isEqualToString:@"succeeded"]) { + if (self.testFailureCount.integerValue == 0) { + [apstring boldText:@"Perfect build!"]; + [apstring plainText:@" All %ld tests passed. 👍\n", (long) self.testsCount.integerValue]; + if (self.codeCoveragePercentage.doubleValue > 0.0) { + [[apstring + italicText:@"Test Coverage"] + plainText:@": %@%%", self.codeCoveragePercentage]; + } + } else { + [apstring boldText:@"Perfect build!"]; + [apstring plainText:@" But please fix %ld failing tests.\n", (long) self.testFailureCount.integerValue]; + [[apstring + italicText:@"Test Coverage"] + plainText:@": %@%% (%@ tests).", self.codeCoveragePercentage, self.testsCount]; + } + [apstring plainText:@"\n%@", perfectMessage]; + return apstring; + } + + [apstring boldText:@"Failing state: %@.", self.summaryString]; + [apstring plainText:@"\n%@", failureMessage]; + if ([self.tags containsObject:@"xcs-upgrade"]) { + [apstring italicText:@"\nThe current configuration may not be supported by the Xcode upgrade."]; + } + + return apstring; } - (NSURL*) integrationLogURL { diff --git a/xcode-github-app.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/xcode-github-app.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings index 0c67376..f9b0d7c 100644 --- a/xcode-github-app.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ b/xcode-github-app.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -1,5 +1,8 @@ - + + PreviewsEnabled + + diff --git a/xcode-github-app/XGAPreferencesViewController.xib b/xcode-github-app/XGAPreferencesViewController.xib index 31ba7fb..dc85f74 100644 --- a/xcode-github-app/XGAPreferencesViewController.xib +++ b/xcode-github-app/XGAPreferencesViewController.xib @@ -1,8 +1,8 @@ - + - + @@ -22,15 +22,15 @@ - + - - + + - + - + - + - + @@ -88,9 +88,11 @@ - + - + + + @@ -105,6 +107,11 @@ + + + + + @@ -136,9 +143,11 @@ - + - + + + @@ -153,6 +162,11 @@ + + + + + @@ -178,7 +192,7 @@ - + @@ -221,32 +235,18 @@ - + - - + + - - + @@ -272,7 +272,7 @@ - + @@ -293,10 +293,8 @@ - - @@ -304,15 +302,178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + @@ -325,7 +486,7 @@ - + diff --git a/xcode-github-app/XGASettings.h b/xcode-github-app/XGASettings.h index 73ef0cd..8fcc8d8 100644 --- a/xcode-github-app/XGASettings.h +++ b/xcode-github-app/XGASettings.h @@ -37,6 +37,9 @@ FOUNDATION_EXPORT NSString*_Nonnull XGACleanString(NSString*_Nullable string); @property (assign) BOOL showDebugMessages; @property (assign) NSTimeInterval refreshSeconds; @property (copy) NSString*gitHubToken; +@property (copy) NSString*failedBuildMessage; +@property (copy) NSString*perfectBuildMessage; +@property (copy) NSString*successfulBuildMessage; @property (strong, null_resettable) NSMutableDictionary*servers; @property (strong, null_resettable) NSMutableArray*gitHubSyncTasks; @end diff --git a/xcode-github-app/XGASettings.m b/xcode-github-app/XGASettings.m index 64fed7a..d2fb8ae 100644 --- a/xcode-github-app/XGASettings.m +++ b/xcode-github-app/XGASettings.m @@ -135,6 +135,75 @@ - (void) setGitHubToken:(NSString *)token { } } +- (NSString*) failedBuildMessage { + @synchronized (self) { + NSError*error = nil; + NSString*message = + [BNCKeyChain retrieveValueForService:kXGAServiceName + key:@"FailedBuildMessage" + error:&error]; + if (error) BNCLog(@"Can't retrieve Failed Build Message: %@.", error); + return message; + } +} + +- (void) setFailedBuildMessage:(NSString *)message { + @synchronized (self) { + NSError *error = + [BNCKeyChain storeValue:message + forService:kXGAServiceName + key:@"FailedBuildMessage" + cloudAccessGroup:nil]; + if (error) BNCLog(@"Can't retrieve Failed Build Message: %@.", error); + } +} + +- (NSString*) successfulBuildMessage { + @synchronized (self) { + NSError*error = nil; + NSString*message = + [BNCKeyChain retrieveValueForService:kXGAServiceName + key:@"SuccessfulBuildMessage" + error:&error]; + if (error) BNCLog(@"Can't retrieve successful Build Message: %@.", error); + return message; + } +} + +- (void) setSuccessfulBuildMessage:(NSString *)message { + @synchronized (self) { + NSError *error = + [BNCKeyChain storeValue:message + forService:kXGAServiceName + key:@"SuccessfulBuildMessage" + cloudAccessGroup:nil]; + if (error) BNCLog(@"Can't retrieve successful Build Message: %@.", error); + } +} + +- (NSString*) perfectBuildMessage { + @synchronized (self) { + NSError*error = nil; + NSString*message = + [BNCKeyChain retrieveValueForService:kXGAServiceName + key:@"PerfectBuildMessage" + error:&error]; + if (error) BNCLog(@"Can't retrieve Perfect Build Message: %@.", error); + return message; + } +} + +- (void) setPerfectBuildMessage:(NSString *)message { + @synchronized (self) { + NSError *error = + [BNCKeyChain storeValue:message + forService:kXGAServiceName + key:@"PerfectBuildMessage" + cloudAccessGroup:nil]; + if (error) BNCLog(@"Can't retrieve Perfect Build Message: %@.", error); + } +} + - (NSMutableDictionary*) servers { @synchronized (self) { if (_servers == nil) _servers = [NSMutableDictionary new]; diff --git a/xcode-github-app/XGAStatusViewController.m b/xcode-github-app/XGAStatusViewController.m index e2cc42c..9e7f7dd 100644 --- a/xcode-github-app/XGAStatusViewController.m +++ b/xcode-github-app/XGAStatusViewController.m @@ -433,6 +433,9 @@ - (void) updateSyncBots:(XGAGitHubSyncTask*)syncTask { options.xcodeServerPassword = server.password; options.templateBotName = syncTask.botNameForTemplate; options.githubAuthToken = XGASettings.shared.gitHubToken; + options.successMessage = XGASettings.shared.successfulBuildMessage; + options.perfectMessage = XGASettings.shared.perfectBuildMessage; + options.failureMessage = XGASettings.shared.failedBuildMessage; options.dryRun = XGASettings.shared.dryRun; error = XGUpdateXcodeBotsWithGitHub(options); if (error) { diff --git a/xcode-github-app/XGAStatusViewItem.m b/xcode-github-app/XGAStatusViewItem.m index d02f483..0a1d405 100644 --- a/xcode-github-app/XGAStatusViewItem.m +++ b/xcode-github-app/XGAStatusViewItem.m @@ -25,7 +25,7 @@ + (instancetype) newItemWithBot:(XGXcodeBot*)bot status:(XGXcodeBotStatus*)botSt status.botName = botStatus.botName; status.statusSummary = [APFormattedString boldText:@"%@", botStatus.summaryString]; - status.statusDetail = botStatus.formattedDetailString; + status.statusDetail = [botStatus formatDetailString:XGASettings.shared.successfulBuildMessage :XGASettings.shared.failedBuildMessage :XGASettings.shared.perfectBuildMessage]; status.repository = [NSString stringWithFormat:@"%@/%@", bot.repoOwner, bot.repoName];