After spending a day last week struggling to get a working integration of Xcode Server and Slack (the popular messaging platform) I finally have arrived upon a reliable solution. What started out as a long bash script (and sed, and cut, and tr) that eventually called a Python program, is now just a Python program run by the Xcode ‘Trigger Script’ functionality. Read on for how I did it.
Working with a client on an iOS app we wanted to keep everyone aware of the project’s progress and the current state of all tests, both server and iOS. So, our team added the client as a Slack single-channel guest and got to work integrating our server-side Strider Continuous Integration server and Xcode Server.
Not knowing any better I dove into the output of the Xcode Build located in /Library/Developer/XcodeServer/IntegrationAssets and poked around. I found the file buildService.log which contained build output detailing everything I needed to know about the build, looking like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
[BuildService] Build results summary: { analyzerWarningChange = 0; analyzerWarningCount = 0; errorChange = 2; errorCount = 2; improvedPerfTestCount = 0; regressedPerfTestCount = 0; testFailureChange = 0; testFailureCount = 0; testsChange = 3; testsCount = 3; warningChange = 0; warningCount = 0; } |
Which, I immediately thought, “I know, I’ll tackle it with bash and sed!”. So I got to work writing a script to parse these entries and ultimately distribute them out to our Slack channel so everyone can see progress as we go along. Here’s the bash script I wrote:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
#!/bin/bash # Build the basic file paths and variables to be used LINES_IN_SUMMARY=12 XCODE_BUILD_DIR="/Library/Developer/XcodeServer/IntegrationAssets" BUILD_SERVICE_LOG=buildService.log # Currently we're only using 1 bot. If this changes the following will need to change BOT_DIR="$XCODE_BUILD_DIR"/$(ls "$XCODE_BUILD_DIR" | tail -1) LAST_BUILD_DIR="$BOT_DIR"/$(ls -Art "$BOT_DIR" | tail -n1) RESULTS_FILE="$LAST_BUILD_DIR"/"$BUILD_SERVICE_LOG" echo "Evaluating file $RESULTS_FILE" # Get the line number of the beginning of the summary block LINE_NUMBER=$(grep -n "Build results summary" "$RESULTS_FILE" | cut -f1 -d:) # Now, get the line number of the end of the block END_OF_LOG=$((LINE_NUMBER + LINES_IN_SUMMARY)) # This prints from the found regexp to the end of the block summary BUILD_RESULT_VARIABLES=$(sed -n -e "/Build results summary/,$END_OF_LOG p" "$RESULTS_FILE") # Loop through the results list which looks like: # testsCount = 2; # errorCount = 0; # We split each line out looking for specific text then store it in a variable while read -r line; do echo "$line" if [[ "$line" == *"testsCount"* ]] then TESTS_COUNT=$(cut -d "=" -f2 <<< "$line" | tr -d ' ' | tr -d ';') fi if [[ "$line" == *"errorCount"* ]] then ERROR_COUNT=$(cut -d "=" -f2 <<< "$line" | tr -d ' ' | tr -d ';') fi if [[ "$line" == *"warningCount"* ]] then WARNING_COUNT=$(cut -d "=" -f2 <<< "$line" | tr -d ' ' | tr -d ';') fi done <<< "$BUILD_RESULT_VARIABLES" python -c "import argparse; import slack; import slack.chat; slack.api_token = 'slack_token'; slack.chat.post_message('#slack_channel', 'Xcode test completed ' + \"$TESTS_COUNT\" + ' tests with ' + \"$ERROR_COUNT\" + ' errors, and ' + \"$WARNING_COUNT\" + ' warnings.', username='XcodeBot')" echo "Tests count: $TESTS_COUNT" echo "Error count: $ERROR_COUNT" echo "Warning count: $WARNING_COUNT" |
This seemed to work! But, however, proved problematic as I discovered (only after I’d written the script!) that there lies a chicken-and-egg problem with when Xcode will execute a script versus when it’s all done packaging up the buildService.log files. The pattern that Xcode follows is: run the project tests, run the post-process script, then build the buildService.log files. So, the script above was running on a previous build, not the most current one.
I poked around a bit more but ultimately couldn’t come up with a solution of how to solve this. So, I searched around on Twitter and found @mjmoriarity, a wonderfully helpful Apple engineer who pointed me towards my ultimate solution of using Xcode’s set environment variables. Using these, my script was now reduced to a handful of lines and is wonderfully reliable:
1 2 3 4 5 6 7 8 |
env # The Xcode build system sets environment variables which we’re using in our Python script echo “Total: $XCS_TESTS_COUNT” echo “Errors: $XCS_ERROR_COUNT” echo “Warnings: $XCS_WARNING_COUNT” python -c "import slack; import slack.chat; slack.api_token = 'slack_token'; slack.chat.post_message('#slack_channel', \"$XCS_BOT_NAME\" + ' test #' + \"$XCS_INTEGRATION_NUMBER\" + ' completed with the result: ' + \"$XCS_INTEGRATION_RESULT\" + '. There were ' + \"$XCS_TESTS_COUNT\" + ' tests with ' + \"$XCS_ERROR_COUNT\" + ' errors, and ' + \"$XCS_WARNING_COUNT\" + ' warnings.', username='XcodeBot')" |