diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..1973ad2 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,337 @@ +/************************************************************************************************* + * + * (c) 2022 All Rights Reserved. + * + * This JenkinsFile calls the real build job to build the DPC/SDK code. This file will live in the + * the repo for Jenkins' hooks to pick it up and start builds for PRs, merges, etc. + * + *************************************************************************************************/ + +import groovy.transform.Field +import jenkins.model.CauseOfInterruption +import hudson.model.* +import net.sf.json.JSONArray +import net.sf.json.JSONObject +import groovy.json.JsonSlurperClassic + +@Field def branchToReleaseTypeMap = [ + 'develop' : 'alpha', + 'staging' : 'beta', + 'master' : 'prod', + 'RelCandidate' : 'Experimental', + 'SHN-15702-ci': 'test' +] + +@Field def branchToFileName = [ + 'develop': 'develop', + 'RelCandidate' : 'Experimental', + 'staging' : 'staging', + 'master' : 'master', + 'SHN-15702-ci': 'test' +] + +@Field def slackMessageTitle = 'EsperSDK Sample Build Notification' +@Field def slackMessageChannel = '#kservcicdjobtest' +@Field def shoonyDpcPublisherCredId = 'shoonya_jenkins_cloud_ui_build_user' +@Field def shoonyDpcPublisherAwsRegion = 'ap-south-1' +@Field def shoonyDpcPublisherS3Bucket = 'shoonya-dpc' +@Field def jenkinsCloudApiAccessCreds = 'jenkins_cloud_api_access_creds' +@Field def releaseBranch = 'master' +@Field def releaseChannel = '' +@Field def buildNumber = '' + +def sendSlackMessage(titleText, messageText, messageColor, channelName) { + JSONArray attachments = new JSONArray() + JSONObject attachment = new JSONObject() + + attachment.put('title', titleText.toString()) + attachment.put('text', messageText.toString()) + attachment.put('color', messageColor) + + attachments.add(attachment) + slackSend(botUser: true, channel: channelName, attachments: attachments.toString()) +} + +def injectCreds() { + // Copy the secret file locally for inclusion into the container. It will be cleaned up automatically + // during the cleanup() stage + withCredentials([file(credentialsId: 'shoonya-dpc-key-jks-file', variable: 'SECRET_FILE')]) + { + sh "cp ${SECRET_FILE} app/secret.file" + } + + // Copy the keystore.properties file for inclusion into the container. It already points to the secret file as + // "storeFile=/application/secret.file" + withCredentials([file(credentialsId: 'keystore.properties', variable: 'KS_PROPS_FILE')]) + { + sh "cp ${KS_PROPS_FILE} app/keystore.properties" + } + + withCredentials([[$class: 'UsernamePasswordMultiBinding', + credentialsId: 'jfrog-artifactorycreds', + usernameVariable: 'USERNAME', + passwordVariable: 'PASSWORD']]) + { + arifactoryUsername = "${USERNAME}" + arifactoryPassword = "${PASSWORD}" + } + + echo 'Secrets in place, listing files.' +} + +def runBuildInDocker(String dpcBuildNumber, String releaseChannel) { + def latestCommit = "${env.GIT_COMMIT}" + //SDK Publish is separated into different job + def publishEsperDeviceSDK = false + // ecr vars to use for pull docker image + def dockerEcrUrl = '973484954817.dkr.ecr.us-west-2.amazonaws.com' + def dockerEcrParms = 'ecr:us-west-2:c1981b34-fa8a-4d88-b3ce-a7adb5c7f71c' + + // login into ECR and get ready to pull image + docker.withRegistry("https://${dockerEcrUrl}", dockerEcrParms) + { + // pull image (only if it's not already on the box) + def currImage = docker.image('shoonya-android-builder-image:latest') + currImage.pull() + + // get the current user's id -- we will pass this to the container for gosu to kick it in. Note the trim() + def currUserId = sh(returnStdout: true, script: 'id -u $USER').trim() + echo "Current runner's UID = ${currUserId}" + + // make the requested script executable, before mapping it into the container + sh 'chmod a+x jenkins/build_app.sh' + + // set the docker image & tag for use for the pull/run + def dockerImageNameAndTag = "${dockerEcrUrl}/shoonya-android-builder-image:latest" + echo "dockerImageNameAndTag= ${dockerImageNameAndTag}" + + // massage the build numbers + def buildNumberAsInt = dpcBuildNumber as Integer + def fourDigitBuildNumber = String.format('%04d', buildNumberAsInt) + + // first get cached-files from S3, if any, to prevent unecessary dependency downloads during the build + // note that we are making the cli only compare the size and not the timestamp to prevent unnecessary + // downloads from S3. The size is ~275MB. + dir('/buildspace/gradle_dependency_cache') + { + sh 'aws s3 sync s3://shoonya-devops-bucket/GradleDependencyCache/ . --size-only' + } + + // + // + // Start the main build in the downloaded docker image + // -v /buildspace/gradle_dependency_cache:/gradle_dependency_cache \ + // + + sh "docker run --rm -e BUILD_NUMBER=${buildNumberAsInt} \ + -e FOURDIGIT_BUILD_NUMBER=${fourDigitBuildNumber} \ + -e PUBLISH_ESPER_DEVICE_SDK=${publishEsperDeviceSDK} \ + -e ARTIFACTORY_USERNAME=${arifactoryUsername} \ + -e ARTIFACTORY_PASSWORD=${arifactoryPassword} \ + -e RELEASE_CHANNEL=${releaseChannel} \ + -e LOCAL_USER_ID=${currUserId} \ + -v /buildspace/gradle_dependency_cache:/gradle_dependency_cache \ + -v ${pwd()}/jenkins/build_app.sh:/build_app.sh \ + -v ${pwd()}/app:/application '${dockerImageNameAndTag}' \ + sh -c /build_app.sh" + } +} + +def archiveFolders(String buildFolderName, String zipFileSuffix) { + def archiveFoldersList = ['outputs', 'reports', 'test-results'] + def prefix = 'esper-sdk-sample' + + // archive the build-status file--this is at the root of the downloaded code + archiveArtifacts artifacts: 'app/build_status.log', fingerprint: true, allowEmptyArchive: true + + dir("app/app/${buildFolderName}") { + archiveFoldersList.each { archiveFolder -> + if (fileExists(archiveFolder)) { + def zipFilename = "${prefix}-build-${archiveFolder}${zipFileSuffix}.zip" + zip zipFile: zipFilename, dir: archiveFolder + archiveArtifacts artifacts: zipFilename, fingerprint: true, allowEmptyArchive: true + } + } + } +} + +def performPostBuildActivities() { + // from the logs, make sure + def buildSuccessful = sh(script: "grep -Fxq 'Build successful' app/build_status.log", returnStatus: true) == 0 + if (!buildSuccessful) { + error('Build failed.') + } + + //echo 'Running junit on tests..' + //junit '**/TEST-*.xml' + + echo 'Running the lint-analysis tool..' + androidLint() + + // archive all interesting folders and attach it to this build + archiveFolders('build', '') +} + +def buildApps(String dpcBuildNumber, String releaseChannel) { + // + // At this point, we are already in the folder where the entire source code for this app is + // + + // first inject the build-related credentials into the file system + injectCreds() + + // run the build(s) in the designated docker image + runBuildInDocker(dpcBuildNumber, releaseChannel) + + // ensure build succeeded, and activities such as lint-checks. + performPostBuildActivities() +} + +def uploadtoS3(apkFile, apkFileName, bucket, s3Path, dpcVersionCode) { + withAWS(credentials:"${shoonyDpcPublisherCredId}", region:"${shoonyDpcPublisherAwsRegion}") + { + s3Upload( + acl:'PublicRead', + bucket:"${bucket}", + cacheControl:'cacheControl:\'public,max-age=86400\'', + file:"${apkFile}", + path:"${s3Path}/${apkFileName}", + pathStyleAccessEnabled: true + ) + def postUploadMessage = "APK upload complete \n ► APKName:`${apkFileName}` \n ► S3Path: ${s3Path} \n ► Version: ${dpcVersionCode}" + sendSlackMessage( + slackMessageTitle, + postUploadMessage, + 'good', + slackMessageChannel + ) + } + return 'success' +} + +pipeline +{ + agent { + label 'dpc-builder' + } + + stages { + stage('Initialize') { + steps { + script { + checkout(scm) + releaseChannel = branchToReleaseTypeMap[env.BRANCH_NAME] ?: 'alpha' + time_stamp = new Date().format('yyyyMMddHHmm') + buildNumber = [releaseChannel, time_stamp].join('-') + echo "Build_Number: ${buildNumber}" + } + } + } + stage('Branch indexing: abort') { + when { + allOf { + triggeredBy cause: 'BranchIndexingCause' + not { + changeRequest() + } + } + } + steps { + script { + echo 'Branch discovered by branch indexing. No way this build is gonna run!' + currentBuild.result = 'SUCCESS' + error 'Caught branch indexing...' + } + } + } + stage('Abort running build') { + when { + expression { + return env.BRANCH_NAME ==~ /(develop|RelCandidate|staging|SHN-15702-ci)/ || changeRequest() + } + } + steps { + script + { + def item = Jenkins.instance.getItemByFullName(env.JOB_NAME) + item.getAllJobs().each { job -> + job._getRuns().iterator().each { run -> + def exec = run.getExecutor() + //if the run is not a current build and it has executor (running) then stop it + if ( run.number.toString() != env.BUILD_ID && exec != null ) { + //prepare the cause of interruption + def cause = { "interrupted by build #${run.getId()}" as String } as CauseOfInterruption + exec.interrupt(Result.ABORTED, cause) + } + } + } + } + } + } // stage - Abort running build + + stage("Build") { + steps { + timestamps { + script { + // get a Sampleapp version number + def buildJob = build (job: 'DeviceBuilds/sampleapp-versions', propagate: true) + dpcVersionBuildNumber = "${buildJob.number}" + echo "Version_name: ${dpcVersionBuildNumber}" + // let the builder library build the code and archive it + buildApps(dpcVersionBuildNumber,releaseChannel) + } + } + } + } //Stage - Build + + stage("Upload APK to S3") { + when { + anyOf { + expression { + env.BRANCH_NAME ==~ /(develop|RelCandidate|staging|SHN-15702-ci)/ + } + branch releaseBranch + } + } + steps { + timestamps { + script { + def buildPathS3 = "sampleapp/"+ releaseChannel + def jsonData = readJSON file: 'app/app/build/outputs/apk/release/output-metadata.json' + dpcApkFile = "${pwd()}/app/app/build/outputs/apk/release/app-release.apk" + dpcVersionCode = jsonData.elements[0].versionCode + dpcVersionName = jsonData.elements[0].versionName + dpcApkFilename = "espersdk_sample_v${dpcVersionCode}_${dpcVersionName}.apk" + echo "DPC for has versionCode = ${dpcVersionCode} and versionName = ${dpcVersionName}" + uploadtoS3(dpcApkFile, dpcApkFilename, shoonyDpcPublisherS3Bucket, buildPathS3, dpcVersionCode) + def fileNameSuffix = branchToFileName[env.BRANCH_NAME] ?: 'test' + finalDpcApkFilename = "espersdk_sample_latest_${fileNameSuffix}.apk" + uploadtoS3(dpcApkFile, finalDpcApkFilename, shoonyDpcPublisherS3Bucket, buildPathS3, dpcVersionCode) + } + } + } + } //Stage Upload + } //Stages + + post { + failure { + script { + if (env.CHANGE_ID == null) { + def messageText = "Failed Job Details: \n ► Branch: `${env.BRANCH_NAME}`\n ► Job: `${env.JOB_NAME}`\n ► buildUrl: ${env.BUILD_URL}\n ► Commit: `${env.GIT_COMMIT}` \n " + def messageColor = 'danger' + sendSlackMessage( + slackMessageTitle, + messageText, + messageColor, + slackMessageChannel + ) + } + } + } + cleanup { + echo 'Cleaning workspace..' + cleanWs() + echo "Done. Exiting script" + } + } //post +} //pipeline \ No newline at end of file diff --git a/app/app/build.gradle b/app/app/build.gradle index 1a9bf10..4f54f8b 100644 --- a/app/app/build.gradle +++ b/app/app/build.gradle @@ -3,17 +3,37 @@ plugins { id 'kotlin-android' } +def keystorePropertiesFile = rootProject.file("keystore.properties") +// Initialize a new Properties() object called keystoreProperties. +def keystoreProperties = new Properties() +// Load your keystore.properties file into the keystoreProperties object. +keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) + +def vMajor = 1 +def vMinor = 0 +def buildNum = System.getenv("BUILD_NUMBER") ?: 0 +def fourDigitBuildnumber = System.getenv("FOURDIGIT_BUILD_NUMBER") ?: 0 +def shVersionCode = (vMajor * 10000000) + (vMinor * 10000) + (buildNum as Integer) + android { compileSdkVersion 30 - buildToolsVersion "30.0.3" + buildToolsVersion "29.0.3" + + signingConfigs { + config { + keyAlias keystoreProperties['keyAlias'] + keyPassword keystoreProperties['keyPassword'] + storeFile file(keystoreProperties['storeFile']) + storePassword keystoreProperties['storePassword'] + } + } defaultConfig { applicationId "io.esper.sdksample" minSdkVersion 19 targetSdkVersion 30 - versionCode 1 - versionName "1.0" - + versionCode shVersionCode as Integer + versionName "v${vMajor}.${vMinor}.${fourDigitBuildnumber}" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -21,6 +41,7 @@ android { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + signingConfig signingConfigs.config } } diff --git a/jenkins/Utils.groovy b/jenkins/Utils.groovy new file mode 100644 index 0000000..1f2b3bb --- /dev/null +++ b/jenkins/Utils.groovy @@ -0,0 +1,150 @@ +import groovy.json.JsonOutput +import net.sf.json.JSONArray +import net.sf.json.JSONObject +import groovy.json.JsonSlurperClassic +import groovy.transform.Field +import groovy.json.JsonOutput +import net.sf.json.JSONArray +import net.sf.json.JSONObject +import groovy.json.JsonSlurperClassic + +@Field def g_slackMessageChannel = '#kservcicdjobtest' +@Field def g_ShoonyDpcPublisherCredId = 'shoonya_jenkins_cloud_ui_build_user' +@Field def g_slackMessageTitle = 'Sampleapp Build Notification' + +def sendSlackMessage(titleText, messageText, messageColor, channelName) { + JSONArray attachments = new JSONArray() + JSONObject attachment = new JSONObject() + + attachment.put('title', titleText.toString()) + attachment.put('text', messageText.toString()) + attachment.put('color', messageColor) + + attachments.add(attachment) + slackSend(botUser: true, channel: channelName, attachments: attachments.toString()) +} + +def injectCreds() { + // Copy the secret file locally for inclusion into the container. It will be cleaned up automatically + // during the cleanup() stage + withCredentials([file(credentialsId: 'shoonya-dpc-key-jks-file', variable: 'SECRET_FILE')]) + { + sh "cp ${SECRET_FILE} secret.file" + } + + // Copy the keystore.properties file for inclusion into the container. It already points to the secret file as + // "storeFile=/application/secret.file" + withCredentials([file(credentialsId: 'keystore.properties', variable: 'KS_PROPS_FILE')]) + { + sh "cp ${KS_PROPS_FILE} keystore.properties" + } + + withCredentials([[$class: 'UsernamePasswordMultiBinding', + credentialsId: 'jfrog-artifactorycreds', + usernameVariable: 'USERNAME', + passwordVariable: 'PASSWORD']]) + { + g_arifactoryUsername = "${USERNAME}" + g_arifactoryPassword = "${PASSWORD}" + } + + echo 'Secrets in place, listing files.' + sh 'ls -al' +} + +def runBuildInDocker(String dpcBuildNumber, String releaseChannel) { + def latestCommit = "${env.GIT_COMMIT}" + //SDK Publish is separated into different job + def publishEsperDeviceSDK = false + // ecr vars to use for pull docker image + def dockerEcrUrl = '973484954817.dkr.ecr.us-west-2.amazonaws.com' + def dockerEcrParms = 'ecr:us-west-2:c1981b34-fa8a-4d88-b3ce-a7adb5c7f71c' + + // login into ECR and get ready to pull image + docker.withRegistry("https://${dockerEcrUrl}", dockerEcrParms) + { + // pull image (only if it's not already on the box) + def currImage = docker.image('shoonya-android-builder-image:latest') + currImage.pull() + + // get the current user's id -- we will pass this to the container for gosu to kick it in. Note the trim() + def currUserId = sh(returnStdout: true, script: 'id -u $USER').trim() + echo "Current runner's UID = ${currUserId}" + + // make the requested script executable, before mapping it into the container + sh 'chmod a+x jenkins/build_app.sh' + + // set the docker image & tag for use for the pull/run + def dockerImageNameAndTag = "${dockerEcrUrl}/shoonya-android-builder-image:latest" + echo "dockerImageNameAndTag= ${dockerImageNameAndTag}" + + // massage the build numbers + def buildNumberAsInt = dpcBuildNumber as Integer + def fourDigitBuildNumber = String.format('%04d', buildNumberAsInt) + + // first get cached-files from S3, if any, to prevent unecessary dependency downloads during the build + // note that we are making the cli only compare the size and not the timestamp to prevent unnecessary + // downloads from S3. The size is ~275MB. + dir('/buildspace/gradle_dependency_cache') + { + sh 'aws s3 sync s3://shoonya-devops-bucket/GradleDependencyCache/ . --size-only' + } + + // + // + // Start the main build in the downloaded docker image + // -v /buildspace/gradle_dependency_cache:/gradle_dependency_cache \ + // + + sh "docker run --rm -e BUILD_NUMBER=${buildNumberAsInt} \ + -e FOURDIGIT_BUILD_NUMBER=${fourDigitBuildNumber} \ + -e PUBLISH_ESPER_DEVICE_SDK=${publishEsperDeviceSDK} \ + -e ARTIFACTORY_USERNAME=${g_arifactoryUsername} \ + -e ARTIFACTORY_PASSWORD=${g_arifactoryPassword} \ + -e RELEASE_CHANNEL=${releaseChannel} \ + -e LOCAL_USER_ID=${currUserId} \ + -v /buildspace/gradle_dependency_cache:/gradle_dependency_cache \ + -v ${pwd()}/jenkins/build_app.sh:/build_app.sh \ + -v ${pwd()}:/application '${dockerImageNameAndTag}' \ + sh -c /build_app.sh" + } +} + +def archiveFolders(String buildFolderName, String zipFileSuffix) { + def archiveFolderMap = [ + "espersdksample/${buildFolderName}" : ['esper-sdk-sample' : ['outputs', 'reports', 'test-results']] + ] +} + +def performPostBuildActivities() { + // from the logs, make sure + def buildSuccessful = sh(script: "grep -Fxq 'Build successful' build_status.log", returnStatus: true) == 0 + if (!buildSuccessful) { + error('Build failed.') + } + + //echo 'Running junit on tests..' + //junit '**/TEST-*.xml' + + echo 'Running the lint-analysis tool..' + androidLint() + + // archive all interesting folders and attach it to this build + //archiveFolders('build-output', '') +} + +def buildApps(String dpcBuildNumber, String releaseChannel) { + // + // At this point, we are already in the folder where the entire source code for this app is + // + + // first inject the build-related credentials into the file system + injectCreds() + + // run the build(s) in the designated docker image + runBuildInDocker(dpcBuildNumber, releaseChannel) + + // ensure build succeeded, and activities such as lint-checks. + performPostBuildActivities() +} +return this diff --git a/jenkins/build_app.sh b/jenkins/build_app.sh new file mode 100755 index 0000000..f164ae7 --- /dev/null +++ b/jenkins/build_app.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +set -xeuo pipefail +APP_DIR=/application +LOG_FILE="$APP_DIR"/build_status.log +curr_script=$(basename -- "$0") +echo "Setting environment variable indicating that this is a Jenkins' build" +export BUILT_BY_JENKINS="yes" +echo "$curr_script started" | tee $LOG_FILE + +echo "Copying cached files into the .gradle folder for faster builds" +mkdir -p ~/.gradle/{caches,temp} +rm -rf ~/.gradle/caches +pushd ~/.gradle/temp +tar -xzf /gradle_dependency_cache/dependency_cache.tar.gz +popd +mv ~/.gradle/temp/home/nala/.gradle/caches ~/.gradle/caches +rm -rf ~/.gradle/temp +### Remove after fixing install permission +cp -R /usr/lib/android-sdk /application/androidsdk +export ANDROID_SDK_DIR="/application/androidsdk" +### + +echo "Switching to the app directory" | tee -a $LOG_FILE +cd $APP_DIR +if [ $? -ne 0 ];then + echo 'Failed to change to $APP_DIR directory' | tee -a $LOG_FILE + exit +fi + +echo "Setting the sdk path in local.properties" | tee -a $LOG_FILE +echo "sdk.dir=$ANDROID_SDK_DIR" > $ANDROID_PROJ_DIR/local.properties + +echo "Making gradlew executable" | tee -a $LOG_FILE +chmod +x ./gradlew +if [ $? -ne 0 ];then + echo 'Failed to mark gradlew as executable' | tee -a $LOG_FILE + exit +fi + +echo "Creating the wrapper based on what the app has requested" | tee -a $LOG_FILE +gradle wrapper +if [ $? -ne 0 ];then + echo 'Failed to create wrapper' | tee -a $LOG_FILE + exit +fi + + +echo "Cleaning project" | tee -a $LOG_FILE +./gradlew clean +if [ $? -ne 0 ];then + echo 'Failed to clean project' | tee -a $LOG_FILE + exit +fi + +# all flavours of release artifacts only. +echo "Building project" | tee -a $LOG_FILE +./gradlew assembleRelease --stacktrace +if [ $? -ne 0 ];then + echo 'Failed to build project' | tee -a $LOG_FILE + exit +fi + + +echo 'Build successful' | tee -a $LOG_FILE \ No newline at end of file diff --git a/jenkins/sample.groovy b/jenkins/sample.groovy new file mode 100644 index 0000000..3d7133e --- /dev/null +++ b/jenkins/sample.groovy @@ -0,0 +1,4 @@ +def test() { + sh "ls -al" + echo "Hai" +} \ No newline at end of file