Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
My Jenkins for Appcelerator mobile CI

Hardware & OS

Running on Apple Mac mini Dualcore i5 2.8GHz (MGEQ2FN/A) OSX Yosemite: 10.10.1

UPDATE Nov 3rd 2015: Upgraded to El Capitan caused my Jenkins to stop working. I had to apply the file permission fix for the log folder again (see known issues at the end of this gist). I also had to reinstall legacy java support and load the jenkins daemon again with launchctl. I took the time to update all the components mentioned in this gist when I did the upgrade. So the Android sdk/ndk, nodejs (0.10 -> 0.12), Titanium and Java. Beware that there is a new command-line tool for titanium (appc from the 'appcelerator' package).

User account setup

I installed and setup the new Mac with 1 main administrative account. In the scripts below it is assumed you are logged in as this administrative user. Changing to the jenkins user is done using sudo su - jenkins.

Required accounts

You will need a valid Apple ID and developer license to install XCode and manage your provisioning files. You also need a valid Appcelerator account to download and install the SDK.

JAVA

Jenkins itself needs Java to run. The titanium tool chain needs an installed JDK to function. We also need to install the legacy Java 6 runtime for some build tools.

Java 8 JDK

Goto the oracle site: http://www.oracle.com/technetwork/java/javase/downloads/index.html Download the JDK and install it (Java SE 8u31 at the time of writing)

Legacy Java 6 SE

Goto the apple download site: http://support.apple.com/kb/DL1572 Download the JavaForOSX2014-001.dmg and install the pkg after mounting the image

Jenkins

I installed Jenkins using the official Mac installer from: http://jenkins-ci.org/content/thank-you-downloading-os-x-installer It ends up running as a deamon for user 'jenkins'. The jenkins user account has normal user permissions (ie. not an administrator). You won't have to be logged in for Jenkins to be available and it will automatically start when a reboot occurs.

Changing port number

If you want jenkins to run on another port then the default 8080 port you can change it with the following sequence of commands (change the 9999 to the port you want):

sudo launchctl unload /Library/LaunchDaemons/org.jenkins-ci.plist
sudo defaults write /Library/Preferences/org.jenkins-ci httpPort 9999
sudo launchctl load /Library/LaunchDaemons/org.jenkins-ci.plist

SSH setup (for Bitbucket/Github/etc)

Setup my SSH key for the Jenkins user:

sudo su - jenkins
ssh-keygen

The generated public key (found in ~/.ssh/id_rsa.pub) is being used on Bitbucket as a deployment key (read-only access). Same could be done for github. I'm using SCM polling in my project because public access to my Jenkins setup is not available.

NodeJS

Installed nodeJS and NPM using official installer from http://nodejs.org/ Globally installed following modules: npm install -g grunt-cli titanium alloy appcelerator

UPDATE Nov 3rd 2015: The 'appcelerator' module is for a new set of platform tools needed to build latest Titanium SDK based apps.

This is not how I'd like to have NodeJS setup. You want to be able to use different versions of node and globally installed modules per project. There is a NodeJS plugin for jenkins that should do this but the auto-installers don't work on Mac. I tried using manual archives but always ended up without a working npm. In the end I gave up and used a globally installed NodeJS for now.

iOS

Installed XCode using Mac App store. Ran at least once to accept agreement and install command-line tools.

TIP: If you installed a new version of xcode you will need to accept the license again. If you only have shell (SSH) access you can just do something like: sudo xcodebuild. This will ask you to review and accept the license if it is needed. You need to run this with administrative permissions or it wont work.

Keychain setup

First we need to create the folder where the keychain is supposed to reside:

sudo su - jenkins
mkdir ~/Library
mkdir ~/Library/Keychains

Create a keychain with the required credentials (private/public keys and distribution certificates) on your Mac. You will need to use the Apple developer portal to create a distribution certificate, an App ID and a provisioning profile (Ad Hoc or App store depending on your needs). These need to be imported into your keychain and installed on your Mac. Easiest is to open XCode, login with your Apple ID and hit the refresh button on the account details page. Copy (don't move, use the Alt/Option key) the certificate and keys to the new jenkins keychain. Note the provisioning profiles are not in your keychain and we will handle them in the next paragraph of this document. Copy the jenkins keychain file (found in ~/Library/Keychains) to the jenkins server and place it in the Keychains directory. Then set it as the default keychain:

sudo su - jenkins
security default-keychain -d user -s ~/Library/Keychains/jenkins.keychain

We will need to use the security command-line tool in build scripts to unlock the keychain when starting a build. Access to the keychain is timed (it automatically locks) so set the default timeout to (for example) 10 minutes. Adjust according to how long your build may take. You only have to do this once as the setting is stored in the keychain itself.

sudo su - jenkins
security unlock ~/Library/Keychains/jenkins.keychain (asks for password)
security set-keychain-settings -t 600 -l ~/Library/Keychains/jenkins.keychain
security show-keychain-info ~/Library/Keychains/jenkins.keychain (to verify the timeout value)

In your project build scripts you can unlock the keychain without user interaction by specifying the password directly:

security unlock -p $KEYCHAIN_PASS ~/Library/Keychains/jenkins.keychain

Important El Capitan security issue

El Capitan is even more strict when it comes to keychain access and security. I ran into the issue that I wanted to change the default access to the private key for the jenkins keychain. What happened is that I was asked for the keychain password to save the changes but it wouldn't accept. Turns out any input with accessibility tools enabled or when using screen sharing is deemed 'unsecure' and it simply won't accept it. I had to directly use connected keyboard to enter the password before it would accept.

Reference info: https://support.apple.com/en-gb/HT205375

Password masking

The jenkins mask password plugin can be used to set the KEYCHAIN_PASS variable and mask it from the console log output:

https://wiki.jenkins-ci.org/display/JENKINS/Mask+Passwords+Plugin

Once the plugin is installed you need to go into the main Jenkins configuration and store your password there. Use KEYCHAIN_PASS as the name and be sure to select the 'Mask passwords (and enable global passwords)' in the build environment section of your project.

Provisioning profiles

We need to create the folder for the provisioning profiles on the Jenkins server and upload them from your mac. First we create the target folder on the server:

sudo su - jenkins
mkdir ~/Library
mkdir ~/Library/MobileDevice
mkdir ~/Library/MobileDevice/Provisioning\ Profiles

Then copy the needed profiles to the jenkins server using a tool like scp. Note that the jenkins user doesn't have a login so you need to upload it to your administrators users home directory first and then login to copy them to the jenkins users home directory. On your mac:

scp ~/Library/MobileDevice/Provisioning\ Profiles/*.mobileprovision user@your_jenkins_server:~/

On the jenkins server you can then copy the uploaded profiles to the correct location. As the administrator user you can:

cp ~/*.mobileprovision /Users/Shared/Jenkins/Library/MobileDevice/Provisioning\ Profiles/

Once we reach the titanium installation phase we can verify if everything is in the right place with the titanium info command.

Android

Apart from the fact that Java on Mac OSX is a little different Android should be easier to setup. We will use homebrew to make installing the Android SDK's a lot easier.

HomeBrew

Installed HomeBrew (http://brew.sh/) and added PATH environment variable to global Jenkins configuration: $PATH:/usr/local/bin

File permissions are an issue. Always check your setup with brew doctor when in doubt. I changed ownership of /usr/local to the jenkins user. Group ownership I set to admin to allow the administrator user(s) to also use brew if needed. I needed to add group write permissions recursively for this. So in the end I did (as an administrator user):

cd /usr/local
sudo chown -R jenkins:admin *
sudo chmod -R g+w *

Android SDK and NDK

Used brew to install Android SDK and NDK: brew install android-sdk brew install android-ndk

Then update the SDK and install the platform tools using Androids SDK manager (grab a coffee at this point) android update sdk --no-ui

Added environment variables in Jenkins to match where Android SDK/NDK is installed: ANDROID_HOME is /usr/local/opt/android-sdk ANDROID_SDK is /usr/local/opt/android-sdk ANDROID_NDK is /usr/local/opt/android-ndk

We also need to tell titanium where the Android SDK/NDK are:

titanium config android.sdkPath /usr/local/opt/android-sdk/
titanium config android.ndkPath /usr/local/opt/android-ndk/

I did the above titanium config for both our administrator account and the jenkins user just to be sure.

Keystore setup

Use the keytool command as you normally would for an Android app to generate a keystore:

keytool -genkeypair -v -keystore android.keystore -alias your-app-alias -keyalg RSA -sigalg SHA1withRSA -validity 10000

Titanium

We need to install and select the titanium SDK for the jenkins user. You need your Appcelerator account details for this. Note that there is a shorthand ti available for the titanium command. We already installed the titanium tools after installing NodeJS with the npm install -g grunt-cli titanium alloy command.

sudo su - jenkins
mkdir ~/Library
mkdir ~/Library/Application\ Support
mkdir ~/Library/Application\ Support/Titanium
appc ti sdk install

You can verify installed and selected SDK using the command:

titanium sdk

You can verify your whole titanium setup using the info command:

titanium info

Putting it all together with Grunt

In package.json we declared the following script section:

"scripts": {
    "test": "command -v grunt >/dev/null 2>&1 && grunt coverage || { echo >&2 'Grunt is not installed'; }",
    "prepublish": "command -v grunt >/dev/null 2>&1 && grunt || { echo >&2 'Grunt is not installed'; }"
},

In Jenkins we create a free-from project with the following shell command:

security unlock -p $KEYCHAIN_PASS /Users/Shared/Jenkins/Library/Keychains/jenkins.keychain
npm install
npm test
security lock /Users/Shared/Jenkins/Library/Keychains/jenkins.keychain

Don't forget to setup the password masking pluging for the keychain and keystore passwords to be available. In the example below we use KEYCHAIN_PASS and KEYSTORE_PASS for Android and iOS respectively.

Below is an example combination of package.json and a Grunt build script. Note that the below example is setup to make an adhoc for iOS. There is an appstore example targetr as well. Remember that you will need different provisioning profiles for Adhoc and Appstore version of iOS apps. Some assembly is required for you own projects.

An example package.json setup:

{
  "name": "YourApp",
  "version": "0.1.0",
  "description": "An app",
  "main": "Resources/app.js",
  "dependencies": {
    "grunt": "^0.4.5",
    "grunt-alloy": "^0.1.0",
    "grunt-titanium": "^0.2.2"
  },
  "devDependencies": {
    "grunt": "^0.4.5",
    "grunt-alloy": "^0.1.0",
    "grunt-contrib-clean": "^0.6.0",
    "grunt-shell": "^1.1.1",
    "grunt-titanium": "^0.2.2"
  },
  "scripts": {
    "test": "command -v grunt >/dev/null 2>&1 && grunt coverage || { echo >&2 'Grunt is not installed'; }",
    "prepublish": "command -v grunt >/dev/null 2>&1 && grunt || { echo >&2 'Grunt is not installed'; }"
  },
  "repository": {
    "type": "git",
    "url": "https://bitbucket.org/your-repo/your-app.git"
  },
  "author": "You!",
  "license": "GPL/MIT/BSD/Commercial/Whatever"
}

And a GruntFile.js to go along with it:

module.exports = function( grunt )
{
    // Project configuration.
    //
    grunt.initConfig(
    {
        pkg: grunt.file.readJSON( "package.json" ),

        settings:
        {
            ppUuid:             "Your provisioning profile UUID (iOS)",
            distributionName:   "Your distribtion name (iOS)"
        },

        clean:
        {
            dist:
            {
                src: [ "dist" ]
            },

            coverage:
            {
                src: [ "dist/coverage" ]
            }
        },

        titanium:
        {
            build_ios:
            {
                options:
                {
                    command:    "build",
                    projectDir: ".",
                    platform:   "ios",
                    buildOnly:  true,
                }
            },
            ios_adhoc: {
                options:
                {
                    command:    "build",
                    projectDir: ".",
                    platform:   "ios",
                    target:     "dist-adhoc",
                    buildOnly:  true,
                    distributionName: "<%= settings.distributionName %>",
                    ppUuid: "<%= settings.ppUuid %>",
                    outputDir: "./dist/artifacts"
                }
            },
            ios_store: {
                options:
                {
                    command:    "build",
                    projectDir: ".",
                    platform:   "ios",
                    target:     "dist-appstore",
                    buildOnly:  true,
                    distributionName: "<%= settings.distributionName %>",
                    ppUuid: "<%= settings.ppUuid %>",
                    outputDir: "./dist/artifacts"
                }
            },
            dev_ios:
            {
                options:
                {
                    command:    "build",
                    args:       "--shadow",
                    projectDir: ".",
                    platform:   "ios",
                    buildOnly:  false,
                }
            },
            build_android:
            {
                options:
                {
                    command:    "build",
                    projectDir: ".",
                    platform:   "android",
                    buildOnly:  true
                }
            },
            android_apk:
            {
                options:
                {
                    command:    "build",
                    projectDir: ".",
                    platform:   "android",
                    target:     "dist-playstore",
                    buildOnly:  true,
                    keystore:   "android.keystore",
                    alias:      "your-app-alias",
                    password:   process.env.KEYSTORE_PASS,
                    outputDir:  "./dist/artifacts"
                }
            },
            dev_android:
            {
                options:
                {
                    command:    "build",
                    args:       "--shadow",
                    projectDir: ".",
                    platform:   "android",
                    buildOnly:  false
                }
            },
            clean:
            {
                options:
                {
                    command:    "clean",
                    projectDir: "."
                }
            }
        }
    } );

    // Load the required plug-ins
    //
    grunt.loadNpmTasks( "grunt-titanium"        );
    grunt.loadNpmTasks( "grunt-alloy"           );
    grunt.loadNpmTasks( "grunt-contrib-clean"   );
    grunt.loadNpmTasks( "grunt-shell"           );

    // Default task(s)
    //
    grunt.registerTask( "default",      [ "titanium:ios_adhoc", "titaniun:android_apk" ] );
    grunt.registerTask( "coverage",     [] );
    grunt.registerTask( "ios-adhoc",    [ "titanium:ios_adhoc"      ] );
    grunt.registerTask( "ios",          [ "titanium:dev_ios"        ] );
    grunt.registerTask( "android",      [ "titanium:dev_android"    ] );
    grunt.registerTask( "android-apk",  [ "titanium:android_apk"    ] );
};

Note that the coverage task is not setup so npm test doesn't really do anything.

Future maintenance / updates

Due to the nature of mobile development this software setup will likely stop working at some point. XCode will need to be updated when Apple says so for instance. Also the globally installed npm modules, Appcelerator SDK and the Android SDK will need to be updated. When this time comes around you will need to rerun all your mobile projects to verify they still build correctly. Having unit tests and code coverage in place is highly recommended but beyond the scope of this document.

Jenkins

A notice will be shown with a download link in the main Jenkins configuration. You need to download the .war file, stop Jenkins, replace the existing .war file and start Jenkins again. The commands to use are:

sudo launchctl unload /Library/LaunchDaemons/org.jenkins-ci.plist
sudo cp ~/Downloads/jenkins.war /Applications/Jenkins/jenkins.war
sudo launchctl load /Library/LaunchDaemons/org.jenkins-ci.plist

XCode

Update using App store

NodeJS

Download and install latest installation package from nodejs.org

NPM modules

As an administrator run:

sudo npm update -g

Homebrew

brew update && brew upgrade

Titanium

sudo su - jenkins
titanium sdk install

Should auto-select the latest SDK when installed. Otherwise run titanium sdk select and choose the correct one. To see available SDK's simply run: ti sdk. Note that the SDK selected in your tiApp.xml needs to be available on your CI server.

Known issues

There is a bug with jenkins on Mac OSX Yosemite 10.10. When the log file for jenkins gets rotated the jenkins user looses write permission. Result is that jenkins can no longer start. To fix it I did:

sudo chown -R jenkins /var/log/jenkins

And then in /private/etc/newsyslog.d/jenkins.conf you need to replace: /var/log/jenkins/jenkins.log 644 3 * $D0 J with /var/log/jenkins/jenkins.log jenkins:jenkins 644 3 * $D0 J

You need to edit that file with root permissions (I did sudo vi /private/etc/newsyslog.d/jenkins.conf but you may prefer another editor.

See issue for details: https://issues.jenkins-ci.org/browse/JENKINS-23543 Future versions of the Mac installer should fix this. The issue is unresolved at this time.

UPDATE Nov 3rd 2015: Issue linked above has been labeled as a duplicate of https://issues.jenkins-ci.org/browse/JENKINS-26982 which claims to have been resolved. A new related issue seems to have appeared in https://issues.jenkins-ci.org/browse/JENKINS-26983

In short, always check log permissions and defaults :(

Useful Jenkins plugins

The discard old build plugin allows you to define a post-build action to remove older builds based on a number of criteria. Disk space may become an issue with mobile build artifacts. https://wiki.jenkins-ci.org/display/JENKINS/Discard+Old+Build+plugin

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment