The environment Directive

environment sets variables for the pipeline or one stage: literals, Groovy expressions, or a step's output. Steps see them as environment variables, and Groovy can read them too:

decl-env: pipeline-level, stage-level and computed variablesGroovy
pipeline {
  agent { label 'linux' }
  environment {
    APP_NAME  = 'booknest'
    IMAGE_TAG = "${env.BRANCH_NAME ?: 'main'}-${env.BUILD_NUMBER}"
    KERNEL    = sh(script: 'uname -r', returnStdout: true).trim()
  }
  stages {
    stage('Test') {
      environment { PGDATABASE = 'booknest_test' }
      steps {
        echo "Groovy sees IMAGE_TAG=${IMAGE_TAG}"
        sh 'echo "shell: $APP_NAME:$IMAGE_TAG on $KERNEL, PGDATABASE=$PGDATABASE"'
      }
    }
    stage('Package') {
      steps {
        sh 'echo "PGDATABASE=${PGDATABASE:-unset}"'
        withEnv(['NODE_ENV=production']) { sh 'echo "NODE_ENV=$NODE_ENV"' }
      }
    }
  }
}

The lines the steps printed:

Output of 36
Groovy sees IMAGE_TAG=main-1
shell: booknest:main-1 on 6.18.33.2-microsoft-standard-WSL2, PGDATABASE=booknest_test
PGDATABASE=unset
NODE_ENV=production

KERNEL ran uname -r on the agent before the first stage; PGDATABASE existed only in its stage; withEnv scopes a variable to a block. Jenkins 8,793 adds BUILD_NUMBER, JOB_NAME, WORKSPACE, NODE_NAME, BUILD_URL, and in multibranch jobs BRANCH_NAME and CHANGE_ID, hence the main fallback in IMAGE_TAG. Mind the quotes: in sh '...$APP_NAME' the shell expands the variable on the agent; in sh "...${APP_NAME}" Groovy substitutes it before the shell runs. The results match here, but only single quotes are safe for secrets.