├── Jenkinsfile ├── README.md ├── jobs ├── cloudflare │ └── seed.groovy └── devops │ ├── checkBucketsDiskUsage.groovy │ ├── checkGCESnapshots.groovy │ ├── checkNonattachedDisks.groovy │ ├── checkUrls.groovy │ └── seed.groovy ├── resources └── google │ └── buckets.json ├── src └── cloudflare │ ├── Caching.groovy │ ├── DNS.groovy │ └── Zone.groovy ├── templates ├── android │ └── Jenkinsfile ├── iOS │ └── Jenkinsfile ├── middleman │ └── Jenkinsfile ├── nodejs │ └── Jenkinsfile └── react │ └── Jenkinsfile └── vars ├── PipelineAndroid.groovy ├── PipelineIOS.groovy ├── PipelineMiddleman.groovy ├── PipelineNodejs.groovy ├── PipelineReact.groovy ├── checkoutSCM.groovy ├── createCNAMERecord.groovy ├── createDNSforBucket.groovy ├── createWebBucket.groovy ├── deployToBucket.groovy ├── setReactBucketWebserver.groovy └── setWebBucketACL.groovy /Jenkinsfile: -------------------------------------------------------------------------------- 1 | library ("ackee-shared-libs@${env.BRANCH_NAME}") 2 | 3 | node(){ 4 | 5 | stage('checkout scm') { 6 | checkout scm 7 | } 8 | 9 | stage('seed jobs') { 10 | 11 | jobDsl targets: 'jobs/**/seed.groovy' 12 | } 13 | 14 | stage('test') { 15 | 16 | // test Google Compute Engine - Storage 17 | createDisk('dev-cluster','test-delete-me') 18 | deleteDisk('dev-cluster','test-delete-me') 19 | 20 | // test Google Container Engine 21 | 22 | // test Cloudflare 23 | 24 | // test other things 25 | } 26 | 27 | stage('lint') { 28 | //TODO: add lint 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jenkins-pipeline-library 2 | this is our jenkins pipeline shared library for CI/CD to our Kubernetes cluster hosted on GKE 3 | 4 | ## These pipelines use these clients/APIs 5 | * Google Cloud 6 | * Kubernetes 7 | * Docker 8 | * Cloudflare 9 | * Jenkins 10 | * Slack 11 | 12 | ## Currently this repo supports 13 | * ReactJS @ Google Storage Buckets - check `templates/react/Jenkinsfile` and `var/PipelineReact` 14 | * NodeJS @ Google Container Engine - check `templates/nodejs/Jenkinsfile` and `var/PipelineNodejs` 15 | * Middleman @ Google Storage Buckets + FTP - check `templates/middleman/Jenkinsfile` and `var/PipelineMiddleman` 16 | * Automated job generating w/ DSL seed job. check `Jenkinsfile` and `jobs/**/seed.groovy` 17 | 18 | ## Soon to be published 19 | * Wordpress @ Kubernetes cluster 20 | * Symfony @ Kubernetes cluster 21 | * Nette @ Kubernetes cluster 22 | * Backup pipeline jobs for mysql/mongodb pods in GKE 23 | * Google compute snapshots pipeline job 24 | * Android build job 25 | * iOS build job 26 | * Android/iOS/Node.js Merge Request builder jobs 27 | 28 | # How to contribute 29 | * feel free to create a pull request or submit an issue 30 | 31 | Our production jenkins-pipeline-shared-library looks much different and is under development. I can't keep up with updating this upstream github repo. 32 | -------------------------------------------------------------------------------- /jobs/cloudflare/seed.groovy: -------------------------------------------------------------------------------- 1 | String folderName = 'cloudflare' 2 | String scriptPath = 'jobs' 3 | 4 | folder(folderName) { 5 | description 'CloudFlare - automatically created cloudflare jobs.' 6 | } 7 | 8 | domains = ["example.com", "ackee.cz", "ackee.de"] 9 | 10 | domains.each { 11 | 12 | def name = it 13 | 14 | pipelineJob("$folderName/${name}-dev-mode") { 15 | 16 | concurrentBuild(false) 17 | 18 | definition { 19 | cps { 20 | script("cfEnableDevMode('${name}')") 21 | sandbox() 22 | } 23 | } 24 | } 25 | 26 | pipelineJob("$folderName/${name}-purge-cache") { 27 | 28 | concurrentBuild(false) 29 | 30 | definition { 31 | cps { 32 | script("cfPurgeAll('${name}')") 33 | sandbox() 34 | } 35 | } 36 | } 37 | 38 | } 39 | 40 | 41 | -------------------------------------------------------------------------------- /jobs/devops/checkBucketsDiskUsage.groovy: -------------------------------------------------------------------------------- 1 | node { 2 | 3 | def report = 'report.log' 4 | sh('echo ""> report.log') 5 | 6 | stage('Check all cloud projects') { 7 | def cloudProjects = getAllCloudprojects() 8 | 9 | for (i=0;i." 20 | 21 | } -------------------------------------------------------------------------------- /jobs/devops/checkGCESnapshots.groovy: -------------------------------------------------------------------------------- 1 | node('linux-docker') { 2 | 3 | def cloudProjects = getAllCloudprojects() 4 | def today 5 | def errors 6 | sh 'rm *.txt || echo nothing to delete' 7 | 8 | stage('list all snapshots') { 9 | 10 | for (i=0;i>alldisks.txt || echo 'no disks found'" 14 | } 15 | 16 | } 17 | 18 | stage('list backup snapshots') { 19 | sh 'echo "" > list.txt ' 20 | for (volume in volumes) { 21 | sh "echo listing ${volume}" 22 | sh "gcloud compute snapshots list --regexp='^backup-.*${volume}.*' 2>>listerr.txt >>list.txt" 23 | } 24 | 25 | 26 | } 27 | archiveArtifacts '*.txt' 28 | color='good' 29 | def err = sh(script: 'wc -l 0) color='warning' 32 | if(err.toInteger() >0) color='danger' 33 | 34 | slackSend channel: 'monitoring', color: color, message: "GCE Snapshots check finished. <$env.JOB_URL/lastSuccessfulBuild/artifact/alldisks.txt|All disks>. Found ${err} errors. Check the <$env.JOB_URL/lastSuccessfulBuild/artifact/list.txt|output>." 35 | 36 | } 37 | -------------------------------------------------------------------------------- /jobs/devops/checkNonattachedDisks.groovy: -------------------------------------------------------------------------------- 1 | node { 2 | 3 | def report = 'report.log' 4 | sh('echo ""> report.log') 5 | 6 | stage('Print all non-attached disks') { 7 | def cloudProjects = getAllCloudprojects() 8 | 9 | for (i=0;i> ${report}") 13 | } 14 | 15 | // delete empty lines in the report file 16 | sh("sed -i '/^\$/d' ${report}") 17 | } 18 | stage('Archive log') { 19 | archiveArtifacts '*.log' 20 | } 21 | 22 | stage('Send notification') { 23 | color='good' 24 | def num = sh(script: "wc -l < ${report}", returnStdout: true).trim() 25 | if(num.toInteger() >0) color='danger' 26 | 27 | slackSend channel: 'monitoring', color: color, message: "GCE Disks check finished. ${num} unattached disks found. See the <$env.JOB_URL/lastSuccessfulBuild/artifact/${report}|output>." 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /jobs/devops/checkUrls.groovy: -------------------------------------------------------------------------------- 1 | @Library('ackee-shared-libs') 2 | import cloudflare.Zone 3 | 4 | node('monitoring') { 5 | 6 | def errors=0 7 | 8 | stage('check urls') { 9 | def z = new Zone(); 10 | String s = z.getZones("status=active&per_page=50") 11 | sh("set +x ; echo '${s}' | jq '.result[].name' | tr -d '\"' >domains.txt") 12 | sh('set +x; while read domain; do echo $domain ; curl -I "https://www.$domain" 2>/dev/null | grep "HTTP\\|Location" || echo "fail" ; echo "------" ;done < domains.txt; echo done') 13 | } 14 | 15 | color='good' 16 | //if(empty.toInteger() >0) color='warning' 17 | if(errors.toInteger() >0) color='danger' 18 | slackSend channel: 'monitoring', color: color, message: "Url check finished. Check <$env.JOB_URL/lastBuild/console|log>." 19 | 20 | } -------------------------------------------------------------------------------- /jobs/devops/seed.groovy: -------------------------------------------------------------------------------- 1 | String folderName = 'devops' 2 | String agentLabel = 'monitoring' 3 | String scriptPath = "jobs/${folderName}" 4 | 5 | folder(folderName) { 6 | description 'DevOps - automatically created jobs.' 7 | } 8 | 9 | pipelineJob("$folderName/checkBackups") { 10 | 11 | label(agentLabel) 12 | 13 | concurrentBuild(false) 14 | 15 | definition { 16 | cps { 17 | script("node('$agentLabel') { checkBackups() }") 18 | sandbox() 19 | } 20 | } 21 | 22 | triggers { 23 | cron('0 12 * * *') 24 | } 25 | } 26 | 27 | pipelineJob("$folderName/checkBucketsDiskUsage") { 28 | 29 | label(agentLabel) 30 | 31 | concurrentBuild(false) 32 | 33 | definition { 34 | cps { 35 | script(readFileFromWorkspace("${scriptPath}/checkBucketsDiskUsage.groovy")) 36 | sandbox() 37 | } 38 | } 39 | 40 | triggers { 41 | cron('@daily') 42 | } 43 | } 44 | 45 | pipelineJob("$folderName/checkGCESnapshots") { 46 | 47 | label(agentLabel) 48 | 49 | concurrentBuild(false) 50 | 51 | definition { 52 | cps { 53 | script(readFileFromWorkspace("${scriptPath}/checkGCESnapshots.groovy")) 54 | sandbox() 55 | } 56 | } 57 | 58 | triggers { 59 | cron('@daily') 60 | } 61 | } 62 | 63 | pipelineJob("$folderName/checkLoadbalancers") { 64 | 65 | label(agentLabel) 66 | 67 | concurrentBuild(false) 68 | 69 | definition { 70 | cps { 71 | script("node('$agentLabel') { checkBackups() }") 72 | sandbox() 73 | } 74 | } 75 | 76 | triggers { 77 | cron('@daily') 78 | } 79 | } 80 | 81 | pipelineJob("$folderName/checkNonattachedDisks") { 82 | 83 | label(agentLabel) 84 | 85 | concurrentBuild(false) 86 | 87 | definition { 88 | cps { 89 | script(readFileFromWorkspace("${scriptPath}/checkNonattachedDisks.groovy")) 90 | sandbox() 91 | } 92 | } 93 | 94 | triggers { 95 | cron('@daily') 96 | } 97 | } 98 | 99 | // no triggers! only manually 100 | pipelineJob("$folderName/checkUrls") { 101 | 102 | label(agentLabel) 103 | 104 | concurrentBuild(false) 105 | 106 | definition { 107 | cps { 108 | script(readFileFromWorkspace("${scriptPath}/checkUrls.groovy")) 109 | sandbox() 110 | } 111 | } 112 | } 113 | 114 | -------------------------------------------------------------------------------- /resources/google/buckets.json: -------------------------------------------------------------------------------- 1 | { 2 | "backups": [ 3 | "abc-backups", 4 | "def-backups", 5 | "ghi-backups", 6 | "jkl-backups", 7 | "mno-prod-backups", 8 | "prq-prod-backups", 9 | "client-backups", 10 | "test-backups" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/cloudflare/Caching.groovy: -------------------------------------------------------------------------------- 1 | // src/cloudflare/Caching.groovy 2 | package cloudflare; 3 | def setDevelopmentMode(String credId, String value) { 4 | 5 | withCredentials([[$class: 'UsernamePasswordMultiBinding', credentialsId: 'cloudflare-api', 6 | usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD'], 7 | string(credentialsId: credId, variable: 'zone'),]) { 8 | 9 | 10 | sh """#!/bin/bash 11 | set -x 12 | curl -X PATCH "https://api.cloudflare.com/client/v4/zones/$zone/settings/development_mode" \ 13 | -H "Content-Type:application/json" \ 14 | -H "X-Auth-Key:$PASSWORD" \ 15 | -H "X-Auth-Email:$USERNAME" \ 16 | --data '{"value":"${value}"}' 17 | """ 18 | } 19 | } 20 | def purgeAll(String credId) { 21 | 22 | withCredentials([[$class: 'UsernamePasswordMultiBinding', credentialsId: 'cloudflare-api', 23 | usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD'], 24 | string(credentialsId: credId, variable: 'zone'),]) { 25 | 26 | 27 | sh """#!/bin/bash 28 | set -x 29 | curl -X DELETE "https://api.cloudflare.com/client/v4/zones/$zone/purge_cache" \ 30 | -H "Content-Type:application/json" \ 31 | -H "X-Auth-Key:$PASSWORD" \ 32 | -H "X-Auth-Email:$USERNAME" \ 33 | --data '{"purge_everything":true}' 34 | """ 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/cloudflare/DNS.groovy: -------------------------------------------------------------------------------- 1 | // src/cloudflare/DNS.groovy 2 | package cloudflare; 3 | def createCNAMErecord(String name, String domain) { 4 | 5 | withCredentials([[$class: 'UsernamePasswordMultiBinding', credentialsId: 'cloudflare-api', 6 | usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD'], 7 | string(credentialsId: credId, variable: 'zone'),]) { 8 | 9 | 10 | sh """#!/bin/bash 11 | curl -X POST "https://api.cloudflare.com/client/v4/$zone/c67af1c878dfb84ee76e7b776517cd2e/dns_records" \ 12 | -H "Content-Type:application/json" \ 13 | -H "X-Auth-Key:$PASSWORD" \ 14 | -H "X-Auth-Email:$USERNAME" \ 15 | --data '{"type":"CNAME","name":"${name}","content":"${domain}","ttl":1,"proxied":true}' 16 | """ 17 | } 18 | } 19 | def getRecord(String filter) { 20 | 21 | withCredentials([[$class: 'UsernamePasswordMultiBinding', credentialsId: 'cloudflare-api', 22 | usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD'], 23 | string(credentialsId: credId, variable: 'zone'),]) { 24 | 25 | 26 | sh """#!/bin/bash 27 | curl -X GET "https://api.cloudflare.com/client/v4/$zone/c67af1c878dfb84ee76e7b776517cd2e/dns_records?$filter" \ 28 | -H "Content-Type:application/json" \ 29 | -H "X-Auth-Key:$PASSWORD" \ 30 | -H "X-Auth-Email:$USERNAME" 31 | """ 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/cloudflare/Zone.groovy: -------------------------------------------------------------------------------- 1 | // src/cloudflare/Zone.groovy 2 | package cloudflare; 3 | String getZones(String filter) { 4 | 5 | String ret 6 | withCredentials([[$class: 'UsernamePasswordMultiBinding', credentialsId: 'cloudflare-api', 7 | usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD']]) { 8 | 9 | 10 | ret = sh(script: """#!/bin/bash 11 | set -x 12 | curl -X GET "https://api.cloudflare.com/client/v4/zones?$filter" \ 13 | -H "Content-Type:application/json" \ 14 | -H "X-Auth-Key:$PASSWORD" \ 15 | -H "X-Auth-Email:$USERNAME" \ 16 | 2>/dev/null 17 | """, returnStdout: true) 18 | 19 | 20 | } 21 | return ret; 22 | } 23 | -------------------------------------------------------------------------------- /templates/android/Jenkinsfile: -------------------------------------------------------------------------------- 1 | PipelineAndroid { 2 | slackChannel = 'ci-android' 3 | 4 | hockeyID='xxxxxx' 5 | 6 | // beta is first because it is default 7 | buildVariants = ["devApiBeta", "devApiDebug", "preProdAApiDebug", "preProdAApiBeta", "preProdBApiDebug", "preProdBApiBeta", "prodApiDebug", "prodApiBeta", "prodApiRelease"] 8 | 9 | mergeRequestBuildVariant = 'devApiBeta' 10 | } 11 | -------------------------------------------------------------------------------- /templates/iOS/Jenkinsfile: -------------------------------------------------------------------------------- 1 | PipelineIOS { 2 | slackChannel = 'ci-ios' 3 | hockeyID = 'xxxxx' 4 | environments = ["Development", "Stage", "Production"] 5 | } 6 | -------------------------------------------------------------------------------- /templates/middleman/Jenkinsfile: -------------------------------------------------------------------------------- 1 | PipelineMiddleman { 2 | 3 | // Define here 4 | slackChannel = "#ci-middleman" 5 | buildCommand = "bundle install --without test development postgres && bundle exec middleman build --verbose" 6 | projectName = 'middleman-template' 7 | buildDir = 'build' 8 | 9 | // Choose either Bucket or FTP and comment out the other block 10 | 11 | // BUCKET 12 | baseURL = 'middleman-template' 13 | 14 | // FTP 15 | //ftpHost = "FTP_HOST" 16 | //ftpUser = "FTP_USER" 17 | //ftpPassword = "FTP_PASSWORD" 18 | 19 | // DO NOT TOUCH THESE UNLESS YOU KNOW WHAT YOU ARE DOING 20 | domainURL = 'example.com' 21 | myTestURL = "${baseURL}.${domainURL}" 22 | 23 | } 24 | -------------------------------------------------------------------------------- /templates/nodejs/Jenkinsfile: -------------------------------------------------------------------------------- 1 | PipelineNodejs{ 2 | 3 | // MODIFY THESE 4 | projectName = 'node-template' 5 | slackChannel = '#ci-nodejs' 6 | appName = 'node-template' 7 | cloudProject = [development: 'dev-cluster-123', master: 'prod-cluster-456'] 8 | buildCommand = 'npm install && npm run postinstall' 9 | 10 | // MODIFY ONLY IF YOU KNOW WHAT YOU ARE DOING 11 | nodeImage = 'node:7' 12 | nodeEnv = "-e NODE_PATH=./app:./config" 13 | nodeTestEnv = '-e NODE_ENV=test -e NODE_PATH=./app:./config' 14 | } 15 | -------------------------------------------------------------------------------- /templates/react/Jenkinsfile: -------------------------------------------------------------------------------- 1 | PipelineReact { 2 | 3 | // MODIFY THESE 4 | slackChannel = '#ci-web' 5 | buildCommand = 'npm install && npm run build' 6 | baseURL = 'react-template' 7 | buildDir = 'www' 8 | 9 | // MODIFY THESE ONLY IF YOU KNOW WHAT ARE YOU DOING 10 | bucketURL = "gs://${baseURL}.example.com/" 11 | nodeEnv = '-e NODE_PATH=./app:./config -e NODE_ENV=production' 12 | nodeImage = 'node:latest' 13 | excludeDir = '.git' 14 | } 15 | -------------------------------------------------------------------------------- /vars/PipelineAndroid.groovy: -------------------------------------------------------------------------------- 1 | def call(body) { 2 | // evaluate the body block, and collect configuration into the object 3 | def config = [:] 4 | body.resolveStrategy = Closure.DELEGATE_FIRST 5 | body.delegate = config 6 | body() 7 | 8 | def agent = config.agent ?: 'android' 9 | 10 | 11 | // now build, based on the configuration provided 12 | node(agent) { 13 | 14 | def workspace = pwd() 15 | 16 | def pipeline = new cz.ackee.Pipeline() 17 | 18 | config.pipelineType = 'android' 19 | 20 | println "pipeline config from Jenkinsfile ==> ${config}" 21 | 22 | pipeline.setEnv(config) 23 | println "flattened pipeline config ==> ${config}" 24 | 25 | env.CHANGELOG_PATH = "outputs/changelog.txt" 26 | def slackChannel = config.slackChannel ?: 'ci-android' 27 | def hockeyID = config.hockeyID 28 | def defChoices = ["DevApiBeta", "DevApiRelease"] 29 | def paramChoices = config.buildVariants ?: defChoices 30 | def hockeyAppApiToken = config.hockeyAppApiToken 31 | 32 | def reason = 'start' 33 | 34 | try { 35 | 36 | properties([ 37 | disableConcurrentBuilds(), 38 | parameters([ 39 | choice(choices: paramChoices.join("\n"), description: 'Build variant of the build', name: 'buildVariant') 40 | ]) 41 | ]) 42 | 43 | stage('Checkout') { 44 | reason = 'checkout' 45 | // Checkout code from repository and update any submodules 46 | pipeline.checkoutScm() 47 | 48 | sh 'git submodule update --init' 49 | sh "mkdir -p outputs" 50 | 51 | // generate changelog 52 | sh "git log --pretty='format:- %s [%ce]' ${env.GIT_COMMIT}...${env.GIT_PREVIOUS_COMMIT} > ${env.CHANGELOG_PATH}" 53 | openTasks high: 'FIXME', normal: 'TODO', pattern: '**/*.kt' 54 | } 55 | 56 | stage('Build') { 57 | reason = 'build' 58 | //branch name from Jenkins environment variables 59 | sh "touch outputs/mapping.txt" 60 | sh "chmod +x gradlew" 61 | withCredentials([[$class : 'AmazonWebServicesCredentialsBinding', credentialsId: 'android-gradle-aws', 62 | accessKeyVariable: 'AWS_ACCESS_KEY', secretKeyVariable: 'AWS_SECRET_KEY']]) { 63 | //build your gradle flavor, passes the current build number as a parameter to gradle 64 | sh "./gradlew -Pkotlin.incremental=false -Porg.gradle.parallel=false -Porg.gradle.daemon=false -PAWS_ACCESS_KEY=$AWS_ACCESS_KEY -PAWS_SECRET_KEY=$AWS_SECRET_KEY clean assemble${params.buildVariant}" 65 | } 66 | } 67 | 68 | stage('Test') { 69 | reason = 'test' 70 | withCredentials([[$class : 'AmazonWebServicesCredentialsBinding', credentialsId: 'android-gradle-aws', 71 | accessKeyVariable: 'AWS_ACCESS_KEY', secretKeyVariable: 'AWS_SECRET_KEY']]) { 72 | sh "./gradlew -Pkotlin.incremental=false -Porg.gradle.parallel=false -Porg.gradle.daemon=false -PAWS_ACCESS_KEY=$AWS_ACCESS_KEY -PAWS_SECRET_KEY=$AWS_SECRET_KEY test${params.buildVariant}UnitTest" 73 | } 74 | step $class: 'JUnitResultArchiver', testResults: "app/build/test-results/**/*.xml" 75 | } 76 | 77 | stage('Archive') { 78 | reason = 'archive' 79 | //tell Jenkins to archive the apks 80 | archiveArtifacts artifacts: 'outputs/*.apk', fingerprint: true 81 | archiveArtifacts artifacts: 'outputs/mapping.txt', fingerprint: true 82 | } 83 | 84 | stage('Upload to HockeyApp') { 85 | reason = 'hockeyapp' 86 | //archive 'outputs/*.apk' 87 | step([ 88 | $class : 'HockeyappRecorder', 89 | applications: [ 90 | [apiToken : hockeyAppApiToken, 91 | downloadAllowed : true, 92 | filePath : 'outputs/App.apk', 93 | dsymPath : 'outputs/mapping.txt', 94 | tags : 'android, internal, ios', 95 | mandatory : false, 96 | notifyTeam : false, 97 | releaseNotesMethod: [$class: 'FileReleaseNotes', fileName: env.CHANGELOG_PATH], 98 | uploadMethod : [$class: 'VersionCreation', appId: hockeyID]] 99 | ], 100 | debugMode : false, 101 | failGracefully: false]) 102 | } 103 | currentBuild.result = 'SUCCESS' 104 | } catch (e) { 105 | currentBuild.result = 'FAILURE' 106 | throw e 107 | } finally { 108 | pipeline.notifySlack(slackChannel: slackChannel, reason: reason) 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /vars/PipelineIOS.groovy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/groovy 2 | 3 | def call(body) { 4 | // evaluate the body block, and collect configuration into the object 5 | def config = [:] 6 | body.resolveStrategy = Closure.DELEGATE_FIRST 7 | body.delegate = config 8 | body() 9 | 10 | def agent = config.agent ?: 'ios' 11 | 12 | 13 | // now build, based on the configuration provided 14 | node(agent) { 15 | 16 | def workspace = pwd() 17 | 18 | def pipeline = new cz.ackee.Pipeline() 19 | 20 | config.pipelineType = 'ios' 21 | 22 | println "pipeline config from Jenkinsfile ==> ${config}" 23 | 24 | pipeline.setEnv(config) 25 | println "flattened pipeline config ==> ${config}" 26 | 27 | env.FASTLANE_SKIP_UPDATE_CHECK = 1 28 | env.FASTLANE_DISABLE_COLORS = 1 29 | env.CHANGELOG_PATH = "outputs/changelog.txt" 30 | def slackChannel = config.slackChannel ?: 'ci-ios' // don't care if it exists 31 | def hockeyID = config.hockeyID 32 | def hockeyAppApiToken = config.hockeyAppApiToken 33 | 34 | def reason = 'start' 35 | 36 | try { 37 | 38 | properties([ 39 | disableConcurrentBuilds(), 40 | ]) 41 | 42 | stage('Checkout') { 43 | reason = 'checkout' 44 | // Checkout code from repository and update any submodules 45 | pipeline.checkoutScm() 46 | sh "mkdir -p outputs" 47 | 48 | // generate changelog 49 | pipeline.generateChangelog path: env.CHANGELOG_PATH, format: 'format:- %s [%ce]' 50 | } 51 | 52 | stage('Prepare') { 53 | reason = 'prepare' 54 | sh "security unlock -p ${env.MACHINE_PASSWORD} ~/Library/Keychains/login.keychain" 55 | reason = 'bundler' 56 | sh "bundle install --path ~/.bundle" 57 | } 58 | 59 | stage('Carthage') { 60 | reason = 'carthage' 61 | sh "bundle exec fastlane cart" 62 | } 63 | 64 | stage('Pods') { 65 | reason = 'cocoapods' 66 | sh "bundle exec fastlane pods" 67 | } 68 | 69 | stage('Test') { 70 | reason = 'test' 71 | sh "bundle exec fastlane test type:unit" 72 | junit allowEmptyResults: true, testResults: 'fastlane/test_output/report.junit' 73 | } 74 | 75 | stage('Provisioning') { 76 | reason = 'provisioning' 77 | sh "bundle exec fastlane provisioning configuration:AdHoc" 78 | } 79 | 80 | stage('Build') { 81 | reason = 'build' 82 | sh "bundle exec fastlane beta" 83 | } 84 | 85 | stage('Upload to HockeyApp') { 86 | reason = 'hockeyapp' 87 | 88 | step([ 89 | $class : 'HockeyappRecorder', 90 | applications: [ 91 | [apiToken : hockeyAppApiToken, 92 | downloadAllowed : true, 93 | filePath : 'outputs/App.ipa', 94 | dsymPath : 'outputs/App.app.dSYM.zip', 95 | tags : 'android, internal, ios', 96 | mandatory : false, 97 | notifyTeam : false, 98 | releaseNotesMethod: [$class: 'FileReleaseNotes', fileName: env.CHANGELOG_PATH], 99 | uploadMethod : [$class: 'VersionCreation', appId: hockeyID]] 100 | ], 101 | debugMode : false, failGracefully: false]) 102 | } 103 | currentBuild.result = 'SUCCESS' 104 | } catch (e) { 105 | currentBuild.result = 'FAILURE' 106 | throw e 107 | } finally { 108 | pipeline.notifySlack(channel: slackChannel, reason: reason) 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /vars/PipelineMiddleman.groovy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/groovy 2 | 3 | import cloudflare.DNS 4 | 5 | def call(body) { 6 | // evaluate the body block, and collect configuration into the object 7 | def config = [:] 8 | body.resolveStrategy = Closure.DELEGATE_FIRST 9 | body.delegate = config 10 | body() 11 | 12 | // now build, based on the configuration provided 13 | node() { 14 | 15 | def workspace = pwd() 16 | def projectName = config.projectName 17 | def branch = env.BRANCH_NAME 18 | def appDir = '/usr/src/app' 19 | def slackChannel = config.slackChannel 20 | env.SLACK_CHANNEL = slackChannel 21 | 22 | def buildCommand = config.buildCommand 23 | def buildDir = config.buildDir 24 | def myURL = (config.myURL ==null) ? config.baseURL : config.myURL 25 | 26 | //defaults 27 | def revproxyURL= config.revproxyURL ?: 'somerevproxy.domain.org' 28 | def bucketURL = "gs://$myURL/" 29 | 30 | //bucket 31 | def baseURL = config.baseURL 32 | 33 | // ftp 34 | def ftpHost = config.ftpHost 35 | def ftpUser = config.ftpUser 36 | def ftpPassword = config.ftpPassword 37 | 38 | def deployToFtp = (ftpHost == null) ? false : true 39 | 40 | try { 41 | checkoutScm() 42 | 43 | stage('Build'){ 44 | sh("docker run -v ${workspace}:${appDir} -w ${appDir} inzinger/middleman-base:2.2 bash -c '${buildCommand}'") 45 | } 46 | 47 | if (deployToFtp) { 48 | 49 | stage('Deploy to FTP') { 50 | sh('cd build; rm .htaccess') 51 | sh('cd build; find . -type f -not -path ".git" -exec curl -T {} ftp://${ftpHost}/{} --ftp-create-dirs --user ${ftpUser}:${ftpPassword} \\;') 52 | } 53 | 54 | 55 | } else { 56 | 57 | stage('Set Bucket') { 58 | // fail silently 59 | def d = new DNS(); 60 | d.getRecord("name=${baseURL}") 61 | d.createCNAMErecord(baseURL, revproxyURL) 62 | sh("gsutil mb $bucketURL || echo error") 63 | } 64 | 65 | stage('Deploy to Bucket'){ 66 | def exclude = "\\.git" 67 | sh("gsutil -m rsync -R -x '${exclude}' ${buildDir} ${bucketURL}") 68 | } 69 | stage('Set bucket ACL'){ 70 | sh("gsutil -m acl ch -R -u AllUsers:R ${bucketURL}") 71 | } 72 | stage('Set bucket index and error pages'){ 73 | sh("gsutil -m web set -m index.html ${bucketURL}") 74 | } 75 | } 76 | 77 | } catch (err) { 78 | currentBuild.result = "FAILURE" 79 | println(err.toString()); 80 | println(err.getMessage()); 81 | println(err.getStackTrace()); 82 | throw err 83 | } finally { 84 | slackNotifyBuild() 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /vars/PipelineNodejs.groovy: -------------------------------------------------------------------------------- 1 | // vars/PipelineNodejs.groovy 2 | 3 | def call(body) { 4 | // evaluate the body block, and collect configuration into the object 5 | def config = [:] 6 | body.resolveStrategy = Closure.DELEGATE_FIRST 7 | body.delegate = config 8 | body() 9 | 10 | properties([disableConcurrentBuilds()]) 11 | def agent = config.agent ?: '' 12 | 13 | def pipeline = new cz.ackee.Pipeline() 14 | 15 | config.pipelineType = 'nodejs' 16 | 17 | // now build, based on the configuration provided 18 | node(agent) { 19 | 20 | def workspace = pwd() 21 | 22 | println "pipeline config from Jenkinsfile ==> ${config}" 23 | 24 | pipeline.setEnv(config) 25 | println "flattened pipeline config ==> ${config}" 26 | 27 | def projectName = config.projectName 28 | def appName = config.appName 29 | def branch = config.branch 30 | def cloudProject = config.cloudProject 31 | 32 | def appDir = '/usr/src/app' 33 | 34 | // nodeEnv based on branch 35 | def nodeEnv = config.nodeEnv 36 | //add rest of params 37 | nodeEnv = nodeEnv + " -v ${workspace}:${appDir} -w ${appDir}" 38 | 39 | def nodeTestEnv = "${config.nodeTestEnv} -v ${workspace}:${appDir} -w ${appDir}" 40 | def imageTag = "eu.gcr.io/${cloudProject}/${projectName}/${appName}:${branch}.${env.BUILD_NUMBER}" 41 | 42 | def namespace = config.namespace 43 | 44 | def slackChannel = config.slackChannel 45 | 46 | // defaults 47 | def appRole = config.appRole ?: 'server' // server | client | apiserver 48 | def appTier = config.appTier ?: 'backend' // backend | frontend 49 | 50 | // buildCommand 51 | def buildCommand = config.buildCommand 52 | 53 | def nodeImage = config.nodeImage 54 | def cloverReportDir = config.cloverReportDir ?: 'coverage' 55 | def cloverReportFileName = config.cloverReportFileName ?: 'clover.xml' 56 | def pattern = config.pattern ?: 'checkstyle-result.xml' 57 | def usePreviousBuildAsReference = config.usePreviousBuildAsReference ?: true 58 | def dryRun = config.dryRun ?: false 59 | def reason = 'checkout' 60 | 61 | try { 62 | 63 | stage('Checkout'){ 64 | 65 | pipeline.checkoutScm() 66 | } 67 | 68 | stage('Build') { 69 | 70 | reason='build' 71 | docker.image(nodeImage).inside(nodeEnv) { 72 | sh buildCommand 73 | } 74 | 75 | docker.build(imageTag) 76 | } 77 | 78 | stage('Docker push image') { 79 | 80 | reason='docker image push' 81 | pipeline.imagePush(cloudProject: cloudProject, imageTag: imageTag) 82 | } 83 | 84 | stage('Test') { 85 | 86 | reason='test' 87 | if (config.runTests){ 88 | 89 | docker.image(nodeImage).inside(nodeTestEnv) { 90 | sh "npm run ci-test" 91 | } 92 | echo "npm run ci-test finished. currentBuild.result=${currentBuild.result}" 93 | 94 | //junit allowEmptyResults: true, healthScaleFactor: 10.0, keepLongStdio: true, testResults: 'test.xml' 95 | step([$class: 'JUnitResultArchiver', allowEmptyResults: true, healthScaleFactor: 10.0, keepLongStdio: true, testResults: 'test.xml']) 96 | echo "junit finished. currentBuild.result=${currentBuild.result}" 97 | 98 | step([$class: 'CloverPublisher',cloverReportDir: cloverReportDir, cloverReportFileName: cloverReportFileName, failingTarget: [conditionalCoverage: 0, methodCoverage: 0, statementCoverage: 0], healthyTarget: [conditionalCoverage: 80, methodCoverage: 70, statementCoverage: 80], unhealthyTarget: [conditionalCoverage: 0, methodCoverage: 0, statementCoverage: 0]]) 99 | echo "CloverPublisher finished. currentBuild.result=${currentBuild.result}" 100 | 101 | //publishHTML([allowMissing: false, alwaysLinkToLastBuild: false, keepAll: false, reportDir: 'clover-report', reportFiles: 'index.html', reportName: 'Coverage HTML Report', reportTitles: '']) 102 | //publishHTML([allowMissing: false, alwaysLinkToLastBuild: false, keepAll: false, reportDir: 'clover', reportFiles: 'index.html', reportName: 'Coverage HTML Report 2', reportTitles: '']) 103 | 104 | if (currentBuild.result == 'UNSTABLE') { 105 | throw new RuntimeException("shit's fucked") 106 | } 107 | } 108 | else { 109 | echo 'tests skipped' 110 | } 111 | } 112 | 113 | 114 | stage('Lint') { 115 | 116 | reason='lint' 117 | if (config.runLint){ 118 | docker.image(nodeImage).inside(nodeTestEnv) { 119 | sh "npm run ci-lint" 120 | } 121 | 122 | checkStyle(pattern, usePreviousBuildAsReference) 123 | } 124 | else { 125 | echo 'skipped' 126 | } 127 | } 128 | 129 | stage('Deploy') { 130 | 131 | if(currentBuild.result != 'UNSTABLE') reason='deploy' 132 | if (dryRun == false) { 133 | pipeline.kubeCreateNamespace(cloudProject: cloudProject, namespace: namespace) 134 | 135 | def folders = ["services", "deployment"] 136 | for (folder in folders){ 137 | def path = config.envFolder + "/" + folder 138 | Map values = ['APP_NAME': appName, 139 | 'PROJECT_NAME': projectName, 140 | 'APP_ROLE': appRole, 141 | 'APP_TIER': appTier, 142 | 'ENV_NAME': (branch.equalsIgnoreCase('master') ? 'production' : branch), 143 | 'IMAGE_URL': imageTag 144 | ] 145 | 146 | pipeline.replaceInTemplates(folder: path, toReplace: values) 147 | pipeline.kubeDeploy(cloudProject: cloudProject, namespace: namespace, yamlFolder: path) 148 | } 149 | } 150 | } 151 | 152 | stage('K8s Info') 153 | 154 | pipeline.printKubeInfo(config) 155 | } 156 | 157 | catch (err) { 158 | currentBuild.result = "FAILURE" 159 | println(err.toString()); 160 | println(err.getMessage()); 161 | println(err.getStackTrace()); 162 | throw err 163 | } 164 | finally { 165 | pipeline.notifySlack(channel: slackChannel, reason: reason) 166 | } 167 | } 168 | 169 | } 170 | -------------------------------------------------------------------------------- /vars/PipelineReact.groovy: -------------------------------------------------------------------------------- 1 | // vars/buildReact.groovy 2 | def call(body) { 3 | // evaluate the body block, and collect configuration into the object 4 | def config = [:] 5 | body.resolveStrategy = Closure.DELEGATE_FIRST 6 | body.delegate = config 7 | body() 8 | // now build, based on the configuration provided 9 | node() { 10 | 11 | 12 | def workspace = pwd() 13 | def appDir = '/usr/src/app' 14 | def nodeEnv = "${config.nodeEnv} -v ${workspace}:${appDir} -w ${appDir}" 15 | // TODO: verify if not null and if the value is correct 16 | def nodeImage = config.nodeImage 17 | def buildCommand = config.buildCommand 18 | def baseURL = config.baseURL 19 | def bucketURL = config.bucketURL 20 | def buildDir = config.buildDir 21 | def slackChannel = config.slackChannel 22 | def excludeDir = config.excludeDir 23 | 24 | try { 25 | 26 | checkoutScm() 27 | 28 | stage('Build') { 29 | docker.image(nodeImage).inside(nodeEnv) { 30 | sh buildCommand 31 | } 32 | } 33 | stage('Set Bucket') { 34 | createDNSforBucket(baseURL) 35 | createWebBucket(bucketURL) 36 | } 37 | 38 | stage('Deploy to Bucket'){ 39 | deployToBucket(buildDir, bucketURL, excludeDir) 40 | } 41 | 42 | stage('Set bucket ACL'){ 43 | setWebBucketACL(bucketURL) 44 | } 45 | 46 | stage('Set bucket index and error pages'){ 47 | setReactBucketWebserver(bucketURL) 48 | } 49 | 50 | } catch (err) { 51 | currentBuild.result = "FAILURE" 52 | slackSend message: "Build #$env.BUILD_NUMBER ${baseURL} *failed* (<$env.BUILD_URL|open>)", channel : slackChannel, color: "bad" 53 | throw err 54 | } 55 | slackSend message: "Build #$env.BUILD_NUMBER ${baseURL} *completed* started by $env.GIT_AUTHOR_NAME (<$env.BUILD_URL|open>)", channel : slackChannel, color: "good" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /vars/checkoutSCM.groovy: -------------------------------------------------------------------------------- 1 | def call() { 2 | stage('Checkout') { 3 | checkout scm 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /vars/createCNAMERecord.groovy: -------------------------------------------------------------------------------- 1 | import cloudflare.DNS 2 | 3 | def call(String name, String domain) { 4 | 5 | def d = new DNS(); 6 | d.getRecord("name=${name}") 7 | d.createCNAMErecord(baseURL, domain) 8 | 9 | } 10 | -------------------------------------------------------------------------------- /vars/createDNSforBucket.groovy: -------------------------------------------------------------------------------- 1 | def call(String name) { 2 | def revproxyURL='c.storage.googleapis.com' 3 | createCNAMERecord(name, revproxyURL) 4 | } 5 | -------------------------------------------------------------------------------- /vars/createWebBucket.groovy: -------------------------------------------------------------------------------- 1 | def call(String bucketURL) { 2 | // don't fail if bucket exists 3 | sh("gsutil mb -c regional -l europe-west1 $bucketURL 2>&1 | grep 'ServiceException: 409' && echo 'Bucket exists'") 4 | } 5 | -------------------------------------------------------------------------------- /vars/deployToBucket.groovy: -------------------------------------------------------------------------------- 1 | def call(String source, String dest, String excludeDir) { 2 | sh("gsutil -m rsync -R -x '${excludeDir}' ${source} ${dest}") 3 | } 4 | -------------------------------------------------------------------------------- /vars/setReactBucketWebserver.groovy: -------------------------------------------------------------------------------- 1 | def call(String bucketURL) { 2 | // serve all requests with index.html 3 | sh("gsutil -m web set -m index.html -e index.html ${bucketURL}") 4 | } 5 | -------------------------------------------------------------------------------- /vars/setWebBucketACL.groovy: -------------------------------------------------------------------------------- 1 | def call(String bucketURL) { 2 | sh("gsutil -m acl ch -R -u AllUsers:R ${bucketURL}") 3 | } 4 | --------------------------------------------------------------------------------