Server--:--:--You--:--:--

Jenkins Pipeline as Code

By Prabath Thalangama· August 31, 2026· 3 min read
#jenkins#ci-cd#pipeline

Introduction

Configuring Jenkins jobs through the web UI doesn't version, doesn't review, and doesn't survive a server rebuild. A Jenkinsfile in the repository does. Modern Jenkins pipelines come in two flavours — declarative (structured, preferred) and scripted (raw Groovy, escape hatch).

Declarative pipeline structure

pipeline {
    agent { label 'linux && docker' }

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

    environment {
        REGISTRY = 'registry.example.com'
        IMAGE    = "${REGISTRY}/app:${env.GIT_COMMIT.take(8)}"
    }

    stages {
        stage('Build') {
            steps {
                sh 'make build'
            }
        }

        stage('Test') {
            parallel {
                stage('unit')  { steps { sh 'make test-unit' } }
                stage('lint')  { steps { sh 'make lint' } }
            }
            post {
                always { junit 'reports/**/*.xml' }
            }
        }

        stage('Publish') {
            when { branch 'main' }
            steps {
                withCredentials([usernamePassword(
                    credentialsId: 'registry-creds',
                    usernameVariable: 'REG_USER',
                    passwordVariable: 'REG_PASS')]) {
                    sh 'echo "$REG_PASS" | docker login $REGISTRY -u "$REG_USER" --password-stdin'
                    sh 'docker push $IMAGE'
                }
            }
        }
    }

    post {
        failure { slackSend channel: '#ci', message: "Failed: ${env.BUILD_URL}" }
        cleanup { cleanWs() }
    }
}

Key concepts

  • agent — where stages run. agent none at top level + per-stage agents lets each stage pick its own executor / container.
  • stages / stage / steps — the required nesting. Steps are the actual commands.
  • when — gate a stage on branch, tag, changeset, environment, or an expression.
  • parallel — run stages concurrently; the enclosing stage finishes when all branches do.
  • postalways / success / failure / unstable / changed / cleanup blocks, at pipeline or stage level.
  • environment — env vars; combine with credentials('id') to bind a secret into a var (still, prefer withCredentials for tighter scope).

Credentials

Never put secrets in the Jenkinsfile. Store them in Jenkins credentials and bind:

withCredentials([
    string(credentialsId: 'aws-oidc-role', variable: 'ROLE_ARN'),
    file(credentialsId: 'kubeconfig-prod', variable: 'KUBECONFIG')
]) {
    sh 'kubectl apply -f k8s/'
}

Jenkins masks bound credential values in the console. The same caveats apply as any CI (see Handling secrets in CI).

Agents in containers

agent {
    docker {
        image 'node:20'
        args  '-v $HOME/.cache:/root/.cache'
    }
}

Or Kubernetes plugin: each build gets a fresh pod with your defined containers. This keeps the Jenkins controller clean and builds reproducible.

Shared libraries

Common logic (deploy steps, notifications, versioning) goes in a shared library repo, loaded with @Library('acme-ci@main'):

vars/
  buildDockerImage.groovy      # -> step: buildDockerImage(name: 'app')
src/
  com/acme/ci/Deployer.groovy
@Library('acme-ci@main') _
pipeline {
    agent any
    stages {
        stage('build') { steps { buildDockerImage(name: 'app', push: env.BRANCH_NAME == 'main') } }
    }
}

Now 40 repos share one implementation and one place to fix a bug.

Verification and troubleshooting

  • "Scripts not permitted to use method..." — the script security sandbox blocked a Groovy call. Move it into a shared library (trusted), or approve it in Manage Jenkins → In-process Script Approval (sparingly).
  • Pipeline hangs on an input/agent — no executor matches the label, or an input step is waiting for a human. Check Build Executor Status.
  • Credentials empty in the shellwithCredentials scope doesn't wrap the sh step, or the credentialsId is wrong / not visible to this folder.
  • docker not found on the agent — the agent label promised Docker but the node lacks it, or the Jenkins user isn't in the docker group.
  • Every build runs on the controlleragent any with no separate agents configured. Add agents; keep the controller for orchestration only.
  • Declarative feels too restrictive — drop into a script { } block for that one stage rather than converting the whole pipeline to scripted.
  • Multibranch pipeline not detecting branches — webhook not configured, or the branch source's discovery rules exclude it.
PrabathStuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.