fbpx

Procés d’integració contínua basat en Github flow amb Jenkins

Procés d’integració contínua basat en Github flow amb Jenkins

Integració continua

En la següent entrada mostrarem el disseny d’un procés de integració contínua basant en el branch workflow de github i les pipelines de Jenkins. En aquest procés també s’ha afegit l’anàlisi estàtic de codi amb l’eina Sonarqube1 i el repositori de binaris Nexus2.

Github Flow i personalització

Per dissenyar el procés d'integració contínua s'ha utilitzat el paradigma de model de branques proposat per Github : github Flow

https://guides.github.com/introduction/flow/

Crear una branca ( Branching)

Crear una branca ( branching)
Crear una branca ( branching )

Quan es treballa en un projecte, hi haurà diverses funcions o idees diferents en progrés en qualsevol moment donat, algunes de les quals estaran preparades per desplegar i’d’altres no. El concepte de branching existeix per tal d’ajudar a gestionar aquest flux de treball.

Quan es crea una branca, s’està creant un entorn per provar idees noves. Els canvis que es fan en una branca no afecten la branca mestra, sabent que la branca no es fusionarà fins que estigui preparada per ser revisada per algú amb qui s’està col·laborant. .

Afegir commits

Afegir commits
Afegir commits

Un cop creada la branca, és el moment de començar a fer canvis. Cada vegada que s’afegeix, edita o suprimeix un fitxer, cal fer un commit per tal d’afegir lo a la branca. És possible (obligatori) afegir missatges per cada commit per tal de realitzar un seguiment del progrés mentre es treballa en una branca.

Els commits també creen una història transparent del treball que els altres poden seguir per entendre el que heu fet i per què. Cada Commits té un missatge de Commit associat, que és una descripció que explica per què es va fer un canvi concret. A més, cada commit es considera una unitat separada de canvi. Aquesta característica permetrà recuperar els canvis si es troba un error, o si es decideix apuntar en una direcció diferent de desenvolupament.

Obrir  Pull Request

Obrir pull request

Els Pull Requests inicien una discussió sobre els  commits. Com que aquests estan molt integrats amb el repositori Git subjacent, qualsevol pot veure exactament quins canvis es fusionaran si s’accepta la petició.

Es pot obrir un Pull Requests en qualsevol moment durant el procés de desenvolupament.

Comentar i revisar el codi

Comentar i revisar el codi

Una vegada que s’ha obert un Pull Request, es realitza una revisió de codi de forma sistemàtica. Es designa un revisor (que pot ser una persona o un equip) i juntament amb la persona que ha fet la branca, es revisa el codi. L’ equip que revisa els canvis pot tenir preguntes o comentaris. Potser l’estil de codificació no coincideixi amb les directrius del projecte, no existeixen proves unitàries, o potser tot està correcte. Els  Pull Requests estan dissenyats per encoratjar i capturar aquest tipus de converses.

També es pot continuar pujant a la branca  a la llum de la discussió i comentaris sobre el  pull request. Si algú fa un comentari que indica que s’ha oblidat de fer alguna cosa o si hi ha un error en el codi, es pot arreglar a la branca en qüestió i pujar el canvi. GitHub mostrarà els  nous commits i els comentaris addicionals que es pugin rebre a la vista unificada de Pull Requests

Build / Unit Test / Static Code Analysys/ Deploy / Automatic Functional Tests / Manual Functional Tests

Github Webhooks

webhooks
Jenkins webhooks

            Els webhooks de github3 permeten a aplicacions subscriure’s a certs esdeveniments que es produeixen en Github.

En el nostre cas, l’aplicació de Jenkins està subscrita a l’event de Pull Request del Github. És a dir, cada vegada que un desenvolupador realitza un pull request,  Jenkins inicia l’execució d’ un pipeline.

Jenkins Pipeline

El Pipeline de  Jenkins 4 és un conjunt de plugins que donen suport a implementar i integrar continuous delivery pipelines  a Jenkins.

Una  continuous delivery (CD) pipeline és una expressió automatitzada del procés per obtenir el software des del control de versions fins al l’entrega als usuaris i clients.

Sistemes que intervenen el  procés de CI:

CI Systems

Pipeline Pull Request:

El  pipeline que s’ha dissenyat en aquest projecte  quan es produeix l’event  consta de les següents fases:

  • 1.- Checkout
  • 2.-Build. (npm & nodejs)
  • 3.-Execució tests unitaris.  (Jasmine & Karma)
  • 3.-Anàlisi Estàtic de Codi ( Sonaqube).
  • 4.- Desplegament en entorn de test ( En el nostre cas en Docker Containers). En el cas del front-end es desplega en una instància de nginx. En el cas dels serveis de Mock amb Dropwizard, sobre un Apache Tomcat Server.
  • 5.- Execució proves automàtiques Funcionals sobre l’entorn de test. (Jasmine & Karma)
  • 6.- Proves funcionals manuals.
  • 7.-Fi del procés.
Fases Jenkins
Vista de les fases de Jenkins

Un cop s’ha revisat el Pull Request i tots els processos del pipeline s’han executat satisfactòriament,  es procedirà a realitzar un merge del codi en la branca master.

En cas que hi hagi algun error, s’aturarà l’execució del pipeline i s’enviarà un email a l’administrador de la plataforma indicant la naturalesa de l’error.

Merge / Promote

Un cop els  canvis s’han verificat, és procedeix a realitzar el Merge del codi a la branca master.

Un cop les branques s’han fusionat, els Pull Requests conserven un registre dels canvis històrics del  codi. Com que aquests canvis es poden cercar, es permet que qualsevol usuari torni enrere en el temps a entendre els motius de la  decisió que s’ha realitzat.

Promote
Promoció de la branca

Pipeline Branca Master

Un cop s’ha realitzat el Merge amb la branca màster, s’iniciarà un nova instància del procés de CI amb Jenkins. En aquest cas, el pipeline executarà els següents processos:

  • 1.- Checkout del codi.
  • 2.- Build
  • 3.- Execució tests unitaris.
  • 3.- Anàlisi Estàtic de Codi ( Sonaqube)
  • 4.- Desplegament en entorn de Test ( En el nostre cas en Containers Docker)
  • 5.- Execució proves automàtiques Funcionals
  • 6.- Proves funcionals manuals.
  • 7.- Promoció: en aquest punt, si totes les validacions s’han realitzat correctament, el procés restarà a l’espera de la promoció a QA. El responsable de promoció podrà realitzar l aprovació. Si es decideix promocionar el buid, s’executaran les tasques que es detallen a continuació:
  • 8.-Número de control de versió: S’assignarà un número de versió del codi i dels objectes del projecte presents  a la branca màster, fer servir l’estratègia de Semantic Versioning5

En aquest cas, el procés de CI restarà a l’espera que  l’usuari responsable del número de versió o release mànager, esculli una estratègia de versionament seleccionant una de les següents opcions que apareixeran en un formulari:

Major,Minor,Patch,Premajor,Preminor,Prepatch,Prerelease,from-git

Cada una d’aquestes opcions realitzarà un canvi en l’atribut version del fitxer del projecte package.json seguint les directrius  proposades per l’esquema semver6 .

En aquesta fase del procés de CI també es persistirà de nou aquest fitxer en el repositori i a continuació es procedirà a realitzar automàticament un tag en el repositori amb el número de versió per tal que es permeti recuperar de forma fàcil qualsevol configuració (codi, artefactes) de forma unívoca.

9.- Desplegament dels binaris : Es desplegaran / emmagatzemaran els binaris i artefactes resultants de l’execució del build en el repositori Nexus amb el número de versió assignat anteriorment.

10. Fi del procés.

Binaris en Nexus

Binaris desplegats en Nexus

Check the pipeline code:

pipeline {

	environment { 
                REGISTRY_URL='${YOUR_DOCKER_REGISTRY_URL}'
                SONAR_SCANNER='SonarScanner'
            	}

   agent none
   
   tools { 
        maven 'maven3' 
        jdk 'JDK 8' 
        nodejs 'NodeJS'
    }
    
    stages {
    			  stage('Checkout') {	
			  agent { label 'master' }  
			  steps{   
           			echo "Starting checkout"
           			checkout scm
    				}
			  }
					
			stage('SonarQube Analysis'){
	        	 agent { label 'master' }  
	        	steps{
	        	
	        	//typescript installed to avoid following error
	        	//https://github.com/SonarSource/SonarTS/issues/453
	        	sh 'npm install -g typescript'
	        	
	        	script {
	        	
	        			def props = readJSON file: 'frontend/package.json'
	        			echo "${props['version']}"
	        			def properties = readProperties file: 'app.properties'
	        			def scannerHome = tool 'SonarScanner';
    						withSonarQubeEnv('SonarQube-7.0') {
      						sh "${scannerHome}/bin/sonar-scanner -Dsonar.projectName='Project Name'  -Dsonar.projectKey=projectKey -Dsonar.sourceEncoding=utf-8 -Dsonar.sources=frontend/src -Dsonar.projectVersion=${props['version']} -Dsonar.branch=${env.BRANCH_NAME} "
    					}
    					
	        	
						}
					}
	  		  		
				}
				stage('SonarQube Quality Gate'){
				
				 agent { label 'master' }  
				
				 options {
                		timeout(time: 1, unit: 'HOURS') 
            		}
				
					steps{
				
						script {
						
    						def qg = waitForQualityGate() // Reuse taskId previously collected by withSonarQubeEnv
    						if (qg.status != 'OK') {
    						
    								 emailext (
            						subject: "Pipeline aborted due to quality gate failure: ${qg.status}",
            						mimeType: 'text/html',
            							body: """<p>STARTED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]':</p>
              							<p>Check console output at "<a href='/ca/${env.BUILD_URL}/'>${env.JOB_NAME} [${env.BUILD_NUMBER}]</a>"</p>""",
           				 				recipientProviders: [[$class: 'DevelopersRecipientProvider']]
          						)
          						
          							//      								error "Pipeline aborted due to quality gate failure: ${qg.status}"
      						}
    					
  					}
  					
				}
				}
				
				
				stage ('Build Artifact'){
			
			
			agent {
		        docker {
		            image 'node:9.11.1'
		            args '-v /etc/passwd:/etc/passwd -v /etc/group:/etc/group -v /var/lib/jenkins/.config:/var/lib/jenkins/.config -v /var/lib/jenkins/.npmrc:/var/lib/jenkins/.npmrc -v /var/lib/jenkins/.npm:/var/lib/jenkins/.npm'
		            
		            
		        }
    		}
			
				steps{
						dir ('frontend/') {
    					sh 'node --version' 
						sh 'npm -v'
						sh 'npm install'
						sh 'npm run build-test'
						}
					  
				}
				
			}
				stage('Build Docker Image') {	
			  
			  agent { label 'master' }  
			  
			  steps{   
			  
			  script{
			  			def properties = readProperties file: 'app.properties'
			  			def props = readJSON file: 'frontend/package.json'
			  			
			  			
			  		   // STOP DOCKER IMAGE
			  		   	def idContainer = sh returnStdout: true, script: "docker ps -a -q --filter name=${properties.serviceName} --format=\"{{.ID}}\""
			  		   	if (idContainer?.trim()) {
			  		   	sh "docker rm \$(docker stop ${idContainer})"
			  		   	}
			  			// BUILD DOCKER IMAGE
						sh "docker build -t ${properties.serviceName}:${props['version']} ."
						sh "docker run -d --name ${properties.serviceName} -p 8096:80   ${properties.serviceName}:${props['version']}"
						
			
					}
		
			      }
			}
				
			
			stage ('Build Artifact RC'){
			
			agent {
		        docker {
		            image 'node:9.11.1'
		            args '-v /etc/passwd:/etc/passwd -v /etc/group:/etc/group -v /var/lib/jenkins/.config:/var/lib/jenkins/.config -v /var/lib/jenkins/.npmrc:/var/lib/jenkins/.npmrc -v /var/lib/jenkins/.npm:/var/lib/jenkins/.npm'
		            
		        }
    		}
    		
    		 when { branch "master" }
			
				steps{
						
						dir ('frontend/') {
						
    					sh 'npm install -registry http://registry.npmjs.org/'
						sh 'npm run build'
						}
						
					  
				}
				
			}
			
			
			stage('SonarQube Transpiled Code Analysis'){
			
			 agent { label 'master' }  
			
			
			 when { branch "master" }
			
						
			
					
					steps{
					
						 script{
								 
								 def userInput = true
								 def didTimeout = false
								 
								 	try {
								 	
						    			timeout(time: 3600, unit: 'SECONDS') { // change to a convenient timeout for you
							        		userInput = input(
							        		id: 'Proceed1', message: 'Do you want to analize transpiled dist code?', parameters: [
							        		[$class: 'BooleanParameterDefinition', defaultValue: true, description: '', name: 'Please confirm you agree with this']
					        				])
				    					}
									} catch(err) { // timeout reached or input false
										// Note: This code will require you to approve the getCauses() method inside of script security under Manage Jenkins> In-process Script Approval:
							    		def user = err.getCauses()[0].getUser()
							    		if('SYSTEM' == user.toString()) { // SYSTEM means timeout.
							        		didTimeout = true
							    		} else {
							        	
							        	userInput = false
							        		echo "Aborted by: [${user}]"
							    		}
									}
									
								if (didTimeout) {
			        			// do something on timeout
			       				 echo "no input was received before timeout"
			       				 
			    				} else if (userInput == true) {
			        			// do something
			       					def props = readJSON file: 'frontend/package.json'
				        			echo "${props['version']}"
				        			def properties = readProperties file: 'app.properties'
				        			def scannerHome = tool 'SonarScanner';
			    						withSonarQubeEnv('SonarQube-7.0') {
			      						sh "${scannerHome}/bin/sonar-scanner -Dsonar.projectName='Project Name'  -Dsonar.projectKey=projectKey -Dsonar.sourceEncoding=utf-8 -Dsonar.sources=frontend/dist -Dsonar.projectVersion=${props['version']} -Dsonar.branch=${env.BRANCH_NAME} "
			    					}
			    					
				        	
							    } else {
							        // do something else
							        echo "no transpiled code analysis"
							        //currentBuild.result = 'FAILURE'
							    } 
						  			
									
					
								}
								
					}
					
					
					}
			
			
			stage('Promotion Service'){
			
			 agent { label 'master' }  
			
			
			 when { branch "master" }
			
										
					steps{
					
					 script{
					 
					 def userInput = true
					 def didTimeout = false
					 
					 	try {
					 	
			    			timeout(time: 3600, unit: 'SECONDS') { // change to a convenient timeout for you
				        		userInput = input(
				        		id: 'Proceed1', message: 'Do you want to promote?', parameters: [
				        		[$class: 'BooleanParameterDefinition', defaultValue: true, description: '', name: 'Please confirm you agree with this']
		        				])
	    					}
						} catch(err) { // timeout reached or input false
							// Note: This code will require you to approve the getCauses() method inside of script security under Manage Jenkins> In-process Script Approval:
				    		def user = err.getCauses()[0].getUser()
				    		if('SYSTEM' == user.toString()) { // SYSTEM means timeout.
				        		didTimeout = true
				    			} else {
				        		userInput = false
				        		echo "Aborted by: [${user}]"
				    		}
						}
						
					if (didTimeout) {
        			// do something on timeout
       				 echo "no input was received before timeout"
       				 
    				} else if (userInput == true) {
        			// do something
       					def properties = readProperties file: 'app.properties'
			  			def props = readJSON file: 'frontend/package.json'
			  		   // STOP DOCKER IMAGE
			  			def idContainer = sh returnStdout: true, script: "docker ps -a -q --filter name=${properties.serviceName}-rc --format=\"{{.ID}}\""
				  		   	if (idContainer?.trim()) {
				  		   	sh "docker rm \$(docker stop ${idContainer})";
				  		   	}
			  			// BUILD DOCKER IMAGE
						sh "docker build -t ${properties.serviceName}:${props['version']}-rc ."
						sh "docker run -d --name ${properties.serviceName}-rc -p 8097:80   ${properties.serviceName}:${props['version']}-rc"
				    } else {
				        // do something else
				        echo "this was not successful"
				        currentBuild.result = 'FAILURE'
				    } 
			  			
						
		
					}
					
						
       
   				 }
   				 
   					
				}	
			
				
			stage ('Deploy Artifact'){
				
					agent {
				        docker {
				            image 'timbru31/node-alpine-git'
				            args '-v /etc/passwd:/etc/passwd -v /etc/group:/etc/group -v /var/lib/jenkins/.gitconfig:/var/lib/jenkins/.gitconfig  -v /var/lib/jenkins/.config:/var/lib/jenkins/.config -v /var/lib/jenkins/.npmrc:/var/lib/jenkins/.npmrc -v /var/lib/jenkins/.npm:/var/lib/jenkins/.npm'
		     
				            
				        }
    			}
				
				
				
				 when { branch "master" }
				 
				 	
				
					 options {
                		timeout(time: 1, unit: 'HOURS') 
            		}
				
						steps{
						
						 
						
							script {
			
						     def distribute = input(
						            id: 'userInput', message: "Do you want to distibute code? ", parameters: [
						            [$class: 'ChoiceParameterDefinition', description: 'delimiters within string', ,choices: 'yes\nno\n', name: 'select']
						    ])
						    
						     switch (distribute) {
						     
						   	  case "yes":
						   	  
						   	  	def typeVersion = input(
						            id: 'userInput', message: "Which Version? ", parameters: [
						            [$class: 'ChoiceParameterDefinition', description: 'delimiters within string',choices: 'major\nminor\npatch\npremajor\npreminor\nprepatch\nprerelease\nfrom-git\n', name: 'select']
						    	])
						   	  
						   	  
						   	    def workspace = pwd();
						   	    
						   	    
						   	    dir ('frontend/') {
							   	    sh "npm version ${typeVersion}";
							   	  	sh "cp package.json ${workspace}/frontend/dist/";
							   	  	sh "cp .npmrc ${workspace}/frontend/dist/";
							   	  
						   	  	}
						   	  	
						   	  	
						   	  	withCredentials([usernamePassword(credentialsId: 'token', passwordVariable: 'GIT_PASSWORD', usernameVariable: 'GIT_USERNAME')]) {
   									
   								     									sh "git add ${workspace}/frontend/package.json";
							   	  	sh "git commit -m \"Set version number\"";
    								sh 'git push https://${GIT_USERNAME}:${GIT_PASSWORD}@{YOUR GIT URL} HEAD:master';
								}
									
									
						   	  	dir ('frontend/dist/') {
						   	  		
									try {
    								    								
    								sh "npm publish";
    								
    								} catch (e){
    								
    								
   											 sh "cp /root/.npm/_logs/*  ."
   											 error "Pipeline aborted due to error publishing package"
    								      
    								}

   											
									
    								
								}
						   	  
						            break
						      case "no":
						      	break
						     }
						}
						
						}
			
				}
    
				stage('Cleanup'){
				
				agent {
		        docker {
		            image 'node:9.11.1'
		            args '-v /etc/passwd:/etc/passwd -v /etc/group:/etc/group -v /var/lib/jenkins/.config:/var/lib/jenkins/.config -v /var/lib/jenkins/.npmrc:/var/lib/jenkins/.npmrc -v /var/lib/jenkins/.npm:/var/lib/jenkins/.npm'
		     
		            
		        }
    		}
				steps{
				
							dir ('frontend/') {
							 		echo 'prune and cleanup'
			         				sh 'npm prune'
			         				//sh 'rm node_modules -rf'
								}
					}
				}
					
	        
		
	}
	        
	        
    
    
    
    
}

def setNewVersion(version){

		 
	      // Run the maven build
	      sh "mvn clean versions:set -DnewVersion=${version}"

	
}
    
    
    String getShortCommit(){

 	sh "git rev-parse --short HEAD > .git/commit-id"                        
    def commit_id = readFile('.git/commit-id')
    echo "Git commit $commit_id"
    def shortGitCommit = commit_id[0..7]
    return shortGitCommit;

}


 private String getCommitIdCmd() {
        String _cmd = "git rev-parse --short HEAD > .git/commit-id";

        return _cmd;
    }

  private String createEnvLabels() {
        String _envLabels = "";

        for (property in this.getEnvPropertiesByEnvironment()) {
            _envLabels += "-e " + property.key + "=" + property.value + " ";
        }

        return _envLabels;
  }
    
     public boolean serviceExists() {
        String _cmd = this.serviceExistsCmd();
        this.runCmd(_cmd);

        boolean _created = false;

        // Read status file.
        def _out = this.pipeline.readFile('status').trim();

        // If status value is not 0, service was created.
        if (Integer.parseInt(_out) > 0) {
            _created = true;
        }

        this.logger.info "created = " + _created;

        return _created;
    }
    
     private String serviceExistsCmd(serviceName) {
        String _cmd = "docker service ls";
        _cmd += " | grep -e  \"[[:space:]]" + serviceName + "[[:space:]]\"";
        _cmd += " | wc -l";
        _cmd += " > status";

        return _cmd;
    }
    
     private Properties getEnvPropertiesByEnvironment(serviceEnvironment) {
        String _envKey = serviceEnvironment != "rc" ? "qa" : "rc";
        def _environmentVars = new Properties();

       
        for (property in this.serviceEnvironmentsVars) {
            String key = property.key;
            String[] envKeySet = key.tokenize('.');
            String envK = envKeySet[0];
            String elem = envKeySet[1];

            if (envK == _envKey) {
                this.logger.info(elem + "=" + property.value);
                _environmentVars.setProperty(elem, property.value);
            }
        }

        return _environmentVars;
    }
    

  1. https://www.sonarqube.org/ []
  2. https://www.sonatype.com/product-nexus-repository []
  3. https://developer.github.com/webhooks/ []
  4. https://jenkins.io/doc/book/pipeline/ []
  5. https://docs.npmjs.com/getting-started/semantic-versioning []
  6. https://semver.org/ []

Necessites ajuda amb aquest tema?

Estaré encantat d'atendre't.

You can give me support if you find this content is usefull!

Buy Me a Coffee

Deixa un comentari

L'adreça electrònica no es publicarà. Els camps necessaris estan marcats amb *

caCatalan