Hosted CI Blog

Hosted Continuous Integration for iOS and Mac

Configure Jenkins to Include Commit Logs With TestFlight Builds

Motivation

Inspired by the FullContact’s post I’ve decided to implement a feature to include commit messages in TestFlight release notes.

However FullContact’s solution is quite hacky and is Git-only (while we have to support Git, Mercurial and Subversion). So I had to put together my own solution which is described in this post.

Idea

Implementing separate code for each of the version control systems seemed like overkill (especially given the same info is already parsed by Jenkins). I’ve decided to fetch the commit logs using Jenkins REST API, however after further investigation I used a simpler hack.

Jenkins stores information about builds in a relatively straightforward order, which looks something like following (included only stuff relevant for our task):

JENKINS_HOME
 +- jobs
     +- [JOB_NAME]
         +- lastSuccessful (symlink to the last successful build folder)
         +- builds
             +- [BUILD_NUMBER] (for each build, symlink to corresponding build folder)
             +- [BUILD_ID] (for each build, contains all data, named based on build date)
                 +- changelog.xml (this file contains change logs that we need)

Individual changelog.xml files contain data in folowing format:

Changes in branch origin/master, between b34f501dbddae6ccc962797945d44b7f37ce38f6 and d24808f7bd6c3c84ba256913350472e370cbc42e
commit d24808f7bd6c3c84ba256913350472e370cbc42e
tree f4f538a6f8867b4f3ee1ae1d18676c170ebcdc54
parent 8fa86545108ac070c4e840b9e027b056b379b100
author Vladimir Grichina <vgrichina@componentix.com> 1358642236 +0200
committer Vladimir Grichina <vgrichina@componentix.com> 1358642236 +0200

    Added CalendarDemo as dependency for tests

:100644 100644 72f0d09be20b86ec26f19af064dddd96fc186169 be53875b416ba4c968c31b5edfe8287aded833ff M  Calendar.xcodeproj/project.pbxproj

commit 8fa86545108ac070c4e840b9e027b056b379b100
tree 0007a2a3259f6b965a34b43c4f6448f7afba45d9
parent 121a19c952b338af3506e05fac70500075270207
author Vladimir Grichina <vgrichina@componentix.com> 1358642075 +0200
committer Vladimir Grichina <vgrichina@componentix.com> 1358642075 +0200

    Uncommented commented out tests

:100644 100644 1e0c797595d2b09491aa0939d4a86dd2bf5766ca 4cd227fd5b19d2a1ec80e8adb67fecdbe727fdd1 M  CalendarTests/CalendarViewSpec.m

So basically our script has to do following:

  1. Get build folder linked to by lastSuccessful symlink
  2. Find changelog.xml files in all build folders that go after it (in lexicographical order)
  3. Select relevant info from changelog.xml files
  4. Output resulting message for TestFlight upload script

Implementation

Most of Hosted CI code is using Node.js, so I used it for this script too (slightly adapted here to be standalone):

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
function filterChangeLog(changeLog) {
    return changeLog.split("\n").filter(function(it) {
        return it.startsWith("    ");
    }).map(function(it) {
        return it.substring(4);
    }).join("\n");
}

function printLogSinceLastSuccess(jobName) {
    var projectDir = path.join(process.env.HOME, ".jenkins/jobs", jobName);
    var lastSuccesfulPath = path.join(projectDir, "lastSuccessful");
    var lastSuccessfulBuildPath;
    if (fs.existsSync(lastSuccesfulPath)) {
        lastSuccessfulBuildPath = path.join(projectDir, fs.readlinkSync(lastSuccesfulPath));
    }
    var buildPaths = fs.readdirSync(path.join(projectDir, "builds")).sort();
    buildPaths.forEach(function(buildPath) {
        if (buildPath.length < 6) {
            return;
        }

        var fullPath = path.join(projectDir, "builds", buildPath);
        if (!lastSuccessfulBuildPath || fullPath > lastSuccessfulBuildPath) {
            var changelogPath = path.join(fullPath, "changelog.xml");
            if (fs.existsSync(changelogPath)) {
                console.log(filterChangeLog(fs.readFileSync(changelogPath, "utf-8")));
            }
        }
    });
}

var args = process.argv.slice(2);

printLogSinceLastSuccess(args[0]);

Now it is just needed to use it in Jenkins – I use script similar to the following:

1
2
3
4
5
6
COMMIT_LOG="Changes since last build:\n"`/path/to/script.js "$JOB_NAME"`
curl http://testflightapp.com/api/builds.json \
    -F file=@"$WORKSPACE/PROJECT_DIR/build/CONFIGURATION-iphoneos/TARGET_NAME-CONFIGURATION-$BUILD_NUMBER.ipa" \
    -F api_token="API_TOKEN" -F team_token="TEAM_TOKEN" \
    -F notes="This is an autodeploy build $BUILD_ID from http://hosted-ci.com\n\n$COMMIT_LOG" \
    -F notify=True -F distribution_lists="DISTRIBUTION_LIST"

Shameless plug

To avoid all aforementioned pain-in-the-ass you may wish to subscribe to Hosted CI, which provides complete CI solution for iOS / Mac apps without need to waste time tinkering with Jenkins configuration.