Lesson 013 · Jenkins Learning Path

Build, Test and Package Node.js with Jenkins

· Published · 5 min read

Labelled Jenkins CI/CD path separating untrusted pull request validation from trusted immutable artifact approval deployment monitoring and rollback

A reproducible Node.js Pipeline pins the runtime, installs exactly what the lockfile describes, runs tests in a clean workspace and produces one traceable artifact. Jenkins can supply Node through a managed tool or a pinned container image; the repository still owns its package and test commands.

Choose how Jenkins supplies Node

MethodBest fitVersion control
NodeJS plugin toolPersistent Linux or Windows agentsNamed Jenkins installation, selected in Pipeline
Container agentEphemeral Linux agents with container runtimeImmutable image tag, preferably digest
Prebuilt agent imageControlled enterprise fleetImage/configuration manifest and label

Do not depend on whichever node happens to be first in an agent's PATH. Align the supported Node release with the application and dependencies; update it through a tested change.

Prepare the repository

node --version
npm --version
npm install
git add package.json package-lock.json
git commit -m 'Record reproducible Node dependencies'

Commit package-lock.json. Define repository scripts such as lint, test:ci and build. Jenkins should call those reviewed scripts instead of duplicating application logic in Groovy.

{
  "scripts": {
    "lint": "eslint .",
    "test:ci": "jest --ci --coverage --reporters=default --reporters=jest-junit",
    "build": "node scripts/build.js"
  }
}

Configure the NodeJS plugin tool

  1. Install and approve the NodeJS plugin.
  2. Open Manage Jenkins > Tools > NodeJS installations.
  3. Create a named installation such as node-24 for the current LTS version, or another supported LTS version required by your application.
  4. Use the same name in the Jenkinsfile and verify the runtime at the start of every build.
pipeline {
  agent { label 'linux nodejs' }
  tools { nodejs 'node-24' }
  options { timestamps(); disableConcurrentBuilds() }
  stages {
    stage('Runtime') {
      steps { sh 'set -eu; node --version; npm --version' }
    }
    stage('Install') {
      steps { sh 'set -euo pipefail; npm ci --no-audit' }
    }
    stage('Quality') {
      steps { sh 'set -euo pipefail; npm run lint; npm run test:ci' }
    }
    stage('Build') {
      steps { sh 'set -euo pipefail; npm run build; npm pack' }
    }
  }
  post {
    always {
      junit testResults: 'junit.xml', allowEmptyResults: true
      archiveArtifacts artifacts: '*.tgz,coverage/**', fingerprint: true,
        allowEmptyArchive: true
    }
    cleanup { deleteDir() }
  }
}

Use a pinned container agent

pipeline {
  agent {
    docker {
      image 'node:24-bookworm-slim'
      args '--read-only --tmpfs /tmp:rw,noexec,nosuid,size=512m'
      reuseNode false
    }
  }
  stages {
    stage('Test and build') {
      steps {
        sh '''set -euo pipefail
          node --version
          npm ci --no-audit
          npm run lint
          npm run test:ci
          npm run build
        '''
      }
    }
  }
}

For stricter reproducibility, pin the reviewed container by digest and update it deliberately. Confirm the image supports your agent architecture and native dependencies.

Handle npm credentials safely

withCredentials([string(credentialsId: 'npm-read-token', variable: 'NPM_TOKEN')]) {
  sh '''set -euo pipefail
    tmp_npmrc="$(mktemp)"
    trap 'rm -f "$tmp_npmrc"' EXIT
    printf '//registry.npmjs.org/:_authToken=%s\n' "$NPM_TOKEN" >"$tmp_npmrc"
    NPM_CONFIG_USERCONFIG="$tmp_npmrc" npm ci --no-audit
  '''
}

Use a read-only token for installation and a separate tightly controlled publishing credential. Do not archive .npmrc, environment dumps or the npm cache after authenticated operations.

Cache without sacrificing correctness

  • Cache the npm download cache, not writable node_modules.
  • Include operating system, CPU architecture, Node major version and lockfile hash in a cache key.
  • Run npm ci for every build; it removes an existing node_modules and follows the lockfile.
  • Make a cold-cache run part of troubleshooting and periodic validation.

Separate build and publish

stage('Publish package') {
  when { buildingTag() }
  steps {
    input message: 'Publish the tested npm package?'
    withCredentials([string(credentialsId: 'npm-publish-token', variable: 'NPM_TOKEN')]) {
      sh 'set -euo pipefail; ./ci/npm-publish-tested-package.sh'
    }
  }
}

Publish the tested package, not a rebuilt directory. Validate tag, package version and artifact checksum in the publishing script.

Unsafe commands and options

Unsafe: npm install --force or npm install --legacy-peer-deps can conceal dependency incompatibility. They are useful diagnostic options and sometimes application policy, but must be reviewed, documented and tested rather than added automatically.

Unsafe: curl ... | bash for runtime installation executes remote content directly. Use the managed tool, a verified package repository or an approved pinned image.

Troubleshoot

FailureCheckFix
Lockfile mismatchpackage.json and lockfile diffRegenerate locally with the approved npm version and commit both.
Native module build failsCompiler, libc, Python, architectureUse a documented agent image containing required build dependencies.
Works locally onlyNode/npm versions and untracked filesMatch pinned versions and test a clean checkout.
Agent disk growsnpm cache and workspacesApply cache retention and workspace cleanup.

Make the Node.js build reproducible and publishable

Pin the supported Node major version through a reviewed agent image or named Jenkins tool, and commit the lockfile. Use npm ci for a clean dependency tree rather than allowing CI to rewrite it. Cache is disposable acceleration: key it by operating system, architecture, Node version and lockfile digest, and test cold-cache builds. Publish unit-test and coverage reports before workspace cleanup. Package one versioned artifact, calculate its digest and promote those bytes; do not run npm install again on each deployment target.

node --version
npm --version
npm ci --ignore-scripts
# Enable lifecycle scripts only when dependencies and policy require them
npm test -- --ci
npm run build
tar -C dist -czf "app-${BUILD_NUMBER}.tgz" .
sha256sum "app-${BUILD_NUMBER}.tgz" | tee "app-${BUILD_NUMBER}.tgz.sha256"

Evidence and failure exercise

LayerEvidenceFailure response
RuntimeNode and npm versions in consoleUnexpected PATH version fails preflight
DependenciesLockfile digest and npm ci resultLockfile mismatch or install script is investigated
TestsJUnit/coverage with explicit totalsMissing report is not treated as zero failures
PackageArtifact digest mapped to commitEnvironment must not rebuild it

Run the Pipeline with an empty cache, warm cache and one intentionally corrupt cache entry. The first two must produce the accepted dependency state; the third must fail safely or discard the cache. Then inject a failing unit test and ensure reports remain visible.

Independent operating checks

  1. Prove pull-request builds cannot publish to the release registry.
  2. Inspect package contents for source maps, secrets and unnecessary files.
  3. Abort during npm ci and verify workspace cleanup.
  4. Deploy the archived digest to staging without contacting npm.

Official references

Advertisement