zot24
9/19/2016 - 2:45 PM

My example Jenkins Pipeline setup for Android app project

My example Jenkins Pipeline setup for Android app project

#!/usr/bin/groovy

/*
 * Copyright (c) 2016, Andrey Makeev <amaksoft@gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice unmodified, this list of conditions, and the following
 *    disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and|or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/**
 * Method for pushing build results to git repo via SSH. (SSH Agent Plugin required)
 * To keep things going while we wait for official Git Publish support for pipelines (https://issues.jenkins-ci.org/browse/JENKINS-28335)
 *
 * Example call (Inline values):
 * pushSSH(branch: "master", commitMsg: "Jenkins build #${env.BUILD_NUMBER}", tagName: "build-${env.BUILD_NUMBER}", files: ".", config: true, username: "Jenkins CI", email: "jenkins-ci@example.com");
 *
 * Example call (Environment variables):
 * env.BRANCH_NAME = "mycoolbranch"// BRANCH_NAME is predefined in multibranch pipeline job
 * env.J_GIT_CONFIG = "true"
 * env.J_USERNAME = "Jenkins CI"
 * env.J_EMAIL = "jenkins-ci@example.com"
 * env.J_CREDS_IDS = '02aa92ec-593e-4a90-ac85-3f43a06cfae3' // Use credentials id from Jenkins (Does anyone know a way to reference them by name rather than by id?)
 * ...
 * pushSSH(commitMsg: "Jenkins build #${env.BUILD_NUMBER}", tagName: "build-${env.BUILD_NUMBER}", files: ".");
 *
 * @param args Map with followinf parameters:
 *   commitMsg : (String) commit message
 *   files : (String) list of files to push (space serparated) (Won't push files if not specified)
 *   tagName : (String) tag name (won't push tag if not specified)
 *   branch : (String) git branch (Will use env variable BRANCH_NAME if not specified)
 *   creds_ids : (List<String>) credentials ids (Will use env variable J_CREDS_IDS if not specified) (haven't figured out yet how to resolve credentials name)
 *   configure : (boolean) configure git publisher (username, email). (If not specified will check out env variable J_GIT_CONFIG)
 *   username : (String) committer name (If not specified will check out env variable J_USERNAME)
 *   email : (String) committer email (If not specified will check out env variable J_EMAIL)
 */
 
def pushSSH(Map args) {

    String tagName = args.tagName
    String commitMsg = args.commitMsg
    String files = args.files
    String branch = args.branch != null ? args.branch : env.BRANCH_NAME;
    List<String> creds_ids = args.creds != null ? args.creds : env.J_CREDS_IDS.tokenize(" ");
    boolean config; // Boolean.parseBoolean() is forbidden in this DSL
    if(args.config != null)
        config = args.config
    else if (env.J_GIT_CONFIG.toLowerCase() == "true") {
        config = true
    }else {
        echo "git config = ${config}, J_GIT_CONFIG = ${env.J_GIT_CONFIG}, assuming false"
        config = false;
    }
    String username = args.username != null ? args.username : env.J_USERNAME;
    String email = args.email != null ? args.email : env.J_EMAIL;

    if (tagName == null && files == null) {
        echo "Neither tag nor files to push specified. Ignoring.";
        return;
    }

    if (branch == null)
        error "Error. Invalid value: git branch = ${branch}";

    if(config) {
        if (username == null || email == null || creds_ids == null)
            error "Error. Invalid value set: { username = ${username}, email = ${email}, credentials = ${creds_ids} }";
        sh """ git config push.default simple
               git config user.name \"${username}\"
               git config user.email \"${email}\"
           """
    }

    sshagent(creds_ids) {
        if (files != null) {
            sh """ git add . && git commit -m \"${commitMsg}\" || true
                   git push origin HEAD:refs/heads/${branch} || true
               """
        }
        if (tagName != null) {
            sh """ git tag -fa \"${tagName}\" -m \"${commitMsg}\"
                   git push -f origin refs/tags/${tagName}:refs/tags/${tagName}
               """
        }
    }
}

return this;
#!/usr/bin/groovy

/*
 * Copyright (c) 2016, Andrey Makeev <amaksoft@gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice unmodified, this list of conditions, and the following
 *    disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and|or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

// Env variables for git push
env.J_USERNAME = "Jenkins CI"
env.J_EMAIL = "jenkins-ci@example.com"
env.J_GIT_CONFIG = "true"
// Use credentials id from Jenkins (Does anyone know a way to reference them by name rather than by id?)
env.J_CREDS_IDS = '02aa92ec-593e-4a90-ac85-3f43a06cfae3'

def gitLib

timestampedNode ("AndroidBuilder") {
  stage ("Checkout") {
    checkout scm
    sh "chmod a+x ./gradlew"
    gitLib = load "git_push_ssh.groovy"
  }
  
  stage ("Build") {
    // Check environment (We define ANDROID_HOME in node settings)
    if (env.ANDROID_HOME == null || env.ANDROID_HOME == "") error "ANDROID_HOME not defined"
    if (env.JAVA_HOME == null || env.JAVA_HOME == "") error "JAVA_HOME not defined"

    // Default parameters (In case file is unreadable or missing)
    def d = [versionName: 'unversioned', versionCode: '1']
    // Read properties from file (Right now we only keep versionName and VersionCode there)
    HashMap<String, Object> props = readProperties defaults: d, file: 'gradle.properties'
    // Optional user input to override parameters
    def userInput
    try {
      timeout(time: 60, unit: 'SECONDS') {
        userInput = input( id:'userInput', message: 'Override build parameters?', parameters: [
          string(defaultValue: props.versionName, description: 'App version (without build number)', name: 'versionName'),
          string(defaultValue: props.versionCode, description: 'Version code (for GooglePlay Store)', name: 'versionCode')
        ])
        logOverrides(userInput, props, "manual_override.log")
        props.putAll(userInput)
        echo("Parameters entered : ${userInput.toString()}")
      }
    } catch (Exception e) {
      echo "User input timed out or cancelled, continue with default values"
    }
    // Change build name to current app version
    currentBuild.displayName = "${props.versionName}.${env.BUILD_NUMBER}"
    // Common build arguments
    env.COMMON_BUILD_ARGS = " -PBUILD_NUMBER=${env.BUILD_NUMBER} -PBRANCH_NAME=${env.BRANCH_NAME}" +
      " -PversionName=${props.versionName} -PversionCode=${props.versionCode}"

    // Build the app
    sh "./gradlew clean"
        sh """./gradlew assembleDebug ${env.COMMON_BUILD_ARGS}
              ./gradlew assembleRelease ${env.COMMON_BUILD_ARGS}
           """
    }

    stage('Save artifacts and publish') {
      // Save build results
      step([$class: 'ArtifactArchiver', artifacts: "**/*.apk", excludes: "**/*unaligned.apk", fingerprint: true])
      // Push changes and tag
      gitLib.pushSSH(commitMsg: "Jenkins build #${env.BUILD_NUMBER} from ${env.BRANCH_NAME}", 
        tagName: "build/${env.BRANCH_NAME}/${env.BUILD_NUMBER}", files: ".", config: true);
      sendEmails();
    }

  stage ('Crashlytics register') {
    sh """./gradlew crashlyticsUploadDistributionDebug ${env.COMMON_BUILD_ARGS}
          ./gradlew crashlyticsUploadDistributionRelease ${env.COMMON_BUILD_ARGS}
       """
  }
}

stage ('Release') {
  try {
    input 'Do we release this build?'
    node {
      echo "Push Release tag"
      def date  = sh(returnStdout: true, script: 'date -u +%Y%m%d').trim()// = new Date().format('yyyyMMdd') // apparently we can't use Date here, not a problem
      gitLib.pushSSH(tagName: "release-${date}", commitMsg: "Jenkins promoted");
      // Do your release stuff
    }
  } catch (Exception e) {
    echo "Release cancelled"
  }
}

// To send emails to everyone relevant to this build (Requires Email-ext plugin)
def sendEmails() {
  emailext body: "See ${env.BUILD_URL}",
    recipientProviders: [[$class: 'CulpritsRecipientProvider'], [$class: 'DevelopersRecipientProvider'], [$class: 'RequesterRecipientProvider']],
      subject: "Jenkins Build Successful",
      to: "admin@example.com";
}

// To log manual overrides
@NonCPS
def logOverrides(def ov_map, def orig_map, def filename) {
  def header = "# Build ${env.BUILD_NUMBER}-${env.BRANCH_NAME} manual parameters override: ";
  def headWritten = false;
  ov_map.each{ k, v ->
    if( orig_map[k] != v ) {
      if (!headWritten) {
        sh "echo \"${header}\" >> ${filename}"; // apparently we are not allowed to use File.write() in this DSL
        headWritten = true;
      };
      sh "echo \"${k}=${v}\" >> ${filename}"
    }
  }
}

// Taken from jenkinsci/jenkins project (https://github.com/jenkinsci/jenkins/blob/master/Jenkinsfile)
// to add timestamps to logs
def timestampedNode(String label = "master", Closure body) {
  node(label) {
    wrap([$class: 'TimestamperBuildWrapper']) {
      body.call();
    }
  }
}