Jenkins Jenkinsfile and Version Control

A Jenkinsfile is a text file that defines your pipeline as code. Storing it in your Git repository alongside your application code means your pipeline evolves with your project, can be reviewed like any other code, and can be rolled back when something goes wrong.

Think of the Jenkinsfile as the instruction manual for your project's delivery process. When the manual lives in a filing cabinet only the admin can access, changes are slow and opaque. When it lives in your codebase, everyone can read it, suggest improvements, and trace every change.

Where the Jenkinsfile Lives

The Jenkinsfile goes in the root directory of your repository by default. Jenkins reads it automatically when a Pipeline job is configured to use "Pipeline script from SCM."

Repository root:
  myapp/
    ├── src/
    │   └── main/
    │       └── java/
    ├── tests/
    ├── pom.xml
    ├── Dockerfile
    ├── README.md
    └── Jenkinsfile   ← Pipeline definition lives here

You can also name it differently or store it in a sub-folder, but the default convention is Jenkinsfile in the repository root. Stick to the convention unless you have a specific reason to change it.

Configuring Jenkins to Use a Jenkinsfile

Step 1: Create a Pipeline job (New Item → Pipeline)

Step 2: In the job configuration, scroll to "Pipeline"

Step 3: Set "Definition" to:
        "Pipeline script from SCM"

Step 4: Set SCM to "Git"

Step 5: Enter Repository URL and Credentials

Step 6: Set Branch: */main  (or your target branch)

Step 7: Set "Script Path" to:
        Jenkinsfile  (default — in repo root)
        OR
        ci/Jenkinsfile  (if stored in a sub-folder)

Step 8: Save

After saving, every time the pipeline runs, Jenkins fetches the latest Jenkinsfile from Git and executes it. If your Jenkinsfile changes in a commit, the next build automatically uses the updated version.

Benefits of Jenkinsfile in Version Control

Benefit 1: Change History

Every modification to your pipeline appears in Git history. You can see who changed what, when, and why (through commit messages).

Git log example:
  a3f9c12  Add deploy stage for production  (2 days ago)  [Dev A]
  b8d2e45  Add security scan to pipeline    (1 week ago)  [Dev B]
  c1a7f90  Initial Jenkinsfile              (2 weeks ago) [Dev C]

Run: git diff b8d2e45..a3f9c12 Jenkinsfile
→ See exactly what changed between those two commits

Benefit 2: Code Review

Pipeline changes go through the same pull request (PR) process as application code. A senior engineer reviews and approves changes before they reach the main branch.

Without version control:
  Admin changes pipeline → Nobody knows → Problem occurs → Hard to trace

With Jenkinsfile in Git:
  Dev creates PR with pipeline change
  → Team reviews in GitHub/GitLab
  → Reviewer approves or requests changes
  → Merge to main branch
  → Jenkins uses updated Jenkinsfile automatically

Benefit 3: Rollback

If a pipeline change causes problems, roll back by reverting the commit in Git. Jenkins immediately starts using the previous version.

Revert to previous pipeline:
  git revert a3f9c12
  git push origin main

Jenkins next build → Uses Jenkinsfile from before a3f9c12

Benefit 4: Branch-Specific Pipelines

Different branches can have different Jenkinsfiles. The feature branch pipeline skips deployment. The main branch pipeline deploys to production.

Branch: feature/login
  Jenkinsfile:
    stages: Checkout → Build → Unit Tests
    (no deploy — feature branch work only)

Branch: main
  Jenkinsfile:
    stages: Checkout → Build → Unit Tests
            → Integration Tests → Docker Build
            → Deploy to Production

Jenkinsfile Best Practices

Practice 1: Use Environment Variables for Configuration

Never hardcode values like server addresses, image names, or version numbers. Define them in the environment block so they are easy to change in one place.

Bad practice:
  sh 'docker build -t registry.company.com/myapp:1.0.42 .'
  sh 'kubectl set image deploy/myapp app=registry.company.com/myapp:1.0.42'

Good practice:
  environment {
      REGISTRY = 'registry.company.com'
      APP      = 'myapp'
      TAG      = "${env.BUILD_NUMBER}"
  }
  steps {
      sh "docker build -t ${REGISTRY}/${APP}:${TAG} ."
      sh "kubectl set image deploy/${APP} app=${REGISTRY}/${APP}:${TAG}"
  }

Practice 2: Keep Stages Focused

Each stage should do one thing. A stage called "Build and Test and Deploy" is hard to debug when it fails. Break it into separate stages: Build, Test, Deploy.

Practice 3: Use Credentials Properly

Never put passwords, tokens, or secrets directly in the Jenkinsfile. Store them in Jenkins Credentials and reference them by ID.

Bad practice:
  sh "curl -u admin:mypassword123 https://api.server.com/deploy"

Good practice:
  withCredentials([usernamePassword(
      credentialsId: 'deploy-api-creds',
      usernameVariable: 'API_USER',
      passwordVariable: 'API_PASS'
  )]) {
      sh "curl -u ${API_USER}:${API_PASS} https://api.server.com/deploy"
  }

Practice 4: Add Timeout to Prevent Stuck Builds

options {
    timeout(time: 30, unit: 'MINUTES')
}

Practice 5: Validate Syntax Before Pushing

Jenkins provides a Declarative Linter that checks your Jenkinsfile syntax without running the pipeline. Access it at:

http://your-jenkins:8080/pipeline-syntax/

Paste your Jenkinsfile and click Validate. Jenkins reports errors without wasting a build run.

Jenkinsfile for a Node.js Application — Real Example

pipeline {
    agent { label 'nodejs-agent' }

    environment {
        NODE_ENV = 'production'
        APP_NAME = 'frontend-app'
        VERSION  = "2.${env.BUILD_NUMBER}"
    }

    options {
        timeout(time: 20, unit: 'MINUTES')
        buildDiscarder(logRotator(numToKeepStr: '15'))
        disableConcurrentBuilds()
    }

    stages {
        stage('Install') {
            steps {
                sh 'npm ci'
            }
        }

        stage('Lint') {
            steps {
                sh 'npm run lint'
            }
        }

        stage('Test') {
            steps {
                sh 'npm test -- --ci --coverage'
            }
            post {
                always {
                    junit 'test-results/junit.xml'
                }
            }
        }

        stage('Build') {
            steps {
                sh 'npm run build'
                archiveArtifacts artifacts: 'dist/**', fingerprint: true
            }
        }

        stage('Deploy') {
            when {
                branch 'main'
            }
            steps {
                withCredentials([sshUserPrivateKey(
                    credentialsId: 'deploy-key',
                    keyFileVariable: 'SSH_KEY'
                )]) {
                    sh "rsync -avz -e 'ssh -i ${SSH_KEY}' dist/ deploy@webserver:/var/www/${APP_NAME}/"
                }
            }
        }
    }

    post {
        failure {
            mail to: 'frontend-team@company.com',
                 subject: "FAILED: ${APP_NAME} build #${env.BUILD_NUMBER}",
                 body: "Pipeline failed. See: ${env.BUILD_URL}"
        }
        success {
            echo "Version ${VERSION} deployed successfully."
        }
    }
}

Key Points

  • The Jenkinsfile lives in the root of your Git repository alongside application code.
  • Configure the Pipeline job to use "Pipeline script from SCM" to read the Jenkinsfile from Git.
  • Version controlling the Jenkinsfile enables change history, code review, and rollback.
  • Different branches can have different pipeline behaviors through different Jenkinsfile content.
  • Never put secrets in the Jenkinsfile — use Jenkins Credentials and reference them by ID.

Leave a Comment