DevOps · 35 Days · Week 3 Day 12 — GitHub Actions & Jenkins Pipelines
1 / 22
Week 3 · Day 12

GitHub Actions & Jenkins Pipelines Deep Dive

Multi-job workflows, needs: dependencies, matrix strategy, secrets, caching, artifacts — then the Jenkins equivalent: parallel stages, withCredentials, post conditions, and shared libraries.

⏱ Duration 60 min
📖 Theory 25 min
🔧 Lab 30 min
❓ Quiz 5 min
Session Overview

What we cover today

01
GHA — needs:, Matrix & Parallel Jobs
Fan-out: lint + test(3×) run in parallel. Fan-in: build waits for all. Matrix = 3 Node versions simultaneously.
02
GHA — Secrets & Environment Variables
Encrypted secrets, env: scoping, context expressions. Never leak tokens in logs.
03
GHA — Caching & Artifacts
Cache node_modules between runs (60s → 5s). Pass build artifacts between jobs.
04
Jenkins — Declarative Jenkinsfile Deep Dive
parallel { }, when { }, input (approval gate), withCredentials, post conditions.
05
Jenkins — Shared Libraries
DRY pipelines. Common build logic in one repo. @Library('name'). Reusable workflows in GHA.
06
npm ci vs npm install in CI
Why CI always uses npm ci — locked versions, faster, fails if lock file is stale.
07
🔧 Lab — Multi-job pipeline
Multi-job GHA (lint + matrix test + build) with a secret. Then Jenkins equivalent with parallel stages.
Part 1 of 5 — GitHub Actions

needs: dependencies & matrix strategy

GHA — Multi-job with matrix
name: CI Advanced
on:
  push:
    branches: [ main, 'feat/**' ]
  pull_request:
    branches: [ main ]

jobs:
  # ── Job 1: Lint (runs first, alone) ─────────
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci && npm run lint

  # ── Job 2: Test on 3 Node versions (PARALLEL) ─
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [ 18, 20, 22 ]  # 3 parallel jobs
      fail-fast: false               # don't cancel others on 1 fail
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: \${{ matrix.node-version }}
          cache: 'npm'
      - run: npm ci && npm test

  # ── Job 3: Build (waits for lint AND test) ───
  build:
    needs: [ lint, test ]  # ← fan-in here
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
Fan-out / Fan-in Pattern
Fan-out: lint + test run in parallel — 3 test jobs (Node 18, 20, 22) fire simultaneously. Total time = longest single job, not sum of all.

Fan-in: needs: [lint, test] makes the build job wait until all specified jobs pass. If any one fails, build is skipped.
Matrix strategy options
  • matrix: { node: [18,20,22] } → 3 jobs
  • matrix: { os: [ubuntu, windows], node: [18,20] } → 4 jobs (all combos)
  • include: — add extra variables to a specific combo
  • exclude: — skip a specific combo
  • fail-fast: false — don't cancel siblings if one fails
💡 Time saving with matrix
3 sequential test runs: 3 × 2 min = 6 minutes. Matrix (parallel): max(2, 2, 2) = 2 minutes. Same coverage, 3× faster feedback.
Part 2 of 5 — GitHub Actions

Secrets, environment variables & contexts

Secrets & env: scoping
# === Set secrets in GitHub ===
# Repo → Settings → Secrets → Actions → New
# DOCKER_USERNAME, DOCKER_PASSWORD, APP_VERSION

# === Workflow-level env (all jobs see this) ===
env:
  NODE_ENV: production
  APP_NAME: my-devops-app

jobs:
  deploy:
    runs-on: ubuntu-latest

    # Job-level env (this job only)
    env:
      DOCKER_USER: \${{ secrets.DOCKER_USERNAME }}

    steps:
      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: \${{ secrets.DOCKER_USERNAME }}
          password: \${{ secrets.DOCKER_PASSWORD }}

      - name: Build and push image
        run: |
          docker build -t \${{ secrets.DOCKER_USERNAME }}/\${{ env.APP_NAME }}:\${{ github.sha }} .
          docker push \${{ secrets.DOCKER_USERNAME }}/\${{ env.APP_NAME }}:\${{ github.sha }}

      # WRONG — never do this!
      # - run: echo "Token is ${{ secrets.DOCKER_PASSWORD }}"

# === Environment-scoped secrets (prod vs staging) ===
jobs:
  deploy-prod:
    environment: production  # uses prod-specific secrets
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying to production..."
3 Secret Scopes
  • Repository secrets — available to all workflows in that repo. Settings → Secrets → Actions.
  • Environment secrets — scoped to a specific environment (staging/production). Required reviewers can gate usage.
  • Organisation secrets — shared across all repos in a GitHub org. Manage centrally.

Precedence: Environment > Repository > Organisation

⚠ Never print secrets to logs
GitHub auto-redacts known secret values in logs as ***. But partial values, encoded forms, or derived values may still leak. Never echo/print secret values. Use them only in with: or env: blocks.
💡 github context — useful values
\$\{{ github.sha }} — commit SHA (use as Docker image tag)
\$\{{ github.ref_name }} — branch or tag name
\$\{{ github.actor }} — who triggered the run
\$\{{ github.run_number }} — incremental build number
Part 3 of 5 — GitHub Actions

Caching & Artifacts — speed and job handoffs

Cache node_modules
# === Method 1: cache: 'npm' in setup-node (easiest) ===
steps:
  - uses: actions/setup-node@v4
    with:
      node-version: '20'
      cache: 'npm'       # ← caches ~/.npm automatically
  - run: npm ci          # uses cache on 2nd run → 60s → 5s

# === Method 2: actions/cache (more control) ===
steps:
  - name: Cache node_modules
    uses: actions/cache@v4
    with:
      path: ~/.npm
      key: npm-\${{ runner.os }}-\${{ hashFiles('**/package-lock.json') }}
      restore-keys: npm-\${{ runner.os }}-
  - run: npm ci

# Cache key uses the package-lock.json hash:
# - Same lock file → cache hit (fast)
# - Lock file changed → cache miss → fresh install
Artifact handoff between jobs
jobs:
  build:
    steps:
      - run: npm run build         # creates dist/
      - uses: actions/upload-artifact@v4
        with:
          name: dist-files
          path: dist/
          retention-days: 7

  deploy:
    needs: build
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: dist-files
      - run: ls dist/          # available here!
Cache vs Artifact — key difference
Cache: persists between workflow runs. Used to speed up installs. Automatically invalidated when the key changes.

Artifact: passes files between jobs within the same workflow run. Build output → Deploy job. Expires after retention period.

Cache = performance. Artifact = data handoff.
Cache key strategy

The cache key determines when to use cached data vs fresh:

  • hashFiles('**/package-lock.json') — cache busted when lock file changes
  • runner.os — separate caches per OS
  • restore-keys: — fallback to partial match (slightly stale cache beats full miss)
💡 Cache impact in practice
A typical Node.js project: npm ci takes 60–90 seconds cold. With cache: ~5 seconds. For a team of 10 pushing 20 times/day: saves ~300 minutes of CI time per day. Always use caching.
Part 4 of 5 — Jenkins

Jenkins Declarative — parallel, when, input, withCredentials

Jenkinsfile — Advanced Declarative
pipeline {
    agent any
    tools { nodejs 'NodeJS-20' }

    environment {
        APP_NAME = 'my-devops-app'
    }

    stages {
        stage('Checkout') {
            steps { checkout scm }
        }

        // Parallel: Lint + Test run simultaneously
        stage('Quality Checks') {
            parallel {
                stage('Lint') {
                    steps { sh 'npm ci && npm run lint' }
                }
                stage('Test') {
                    steps {
                        sh 'npm ci && npm test'
                    }
                    post {
                        always {
                            junit '**/test-results/*.xml'
                        }
                    }
                }
            }
        }

        // Only build on main branch
        stage('Build Docker') {
            when { branch 'main' }
            steps {
                withCredentials([usernamePassword(
                    credentialsId: 'docker-hub-creds',
                    usernameVariable: 'DOCKER_USER',
                    passwordVariable: 'DOCKER_PASS'
                )]) {
                    sh """
                        docker login -u $DOCKER_USER -p $DOCKER_PASS
                        docker build -t $DOCKER_USER/$APP_NAME:$BUILD_NUMBER .
                        docker push $DOCKER_USER/$APP_NAME:$BUILD_NUMBER
                    """
                }
            }
        }

        // Human approval gate before production
        stage('Approve Production') {
            when { branch 'main' }
            steps {
                input message: 'Deploy to production?',
                      ok: 'Deploy'
            }
        }

        stage('Deploy') {
            when { branch 'main' }
            steps {
                sh 'echo "Deploying $APP_NAME:$BUILD_NUMBER"'
            }
        }
    }

    post {
        always  { echo 'Pipeline finished' }
        success { slackSend '✅ Build passed' }
        failure { slackSend '❌ Build failed — check logs' }
        cleanup { cleanWs() }  // delete workspace
    }
}
Key Jenkins features used
  • parallel { stage('a') stage('b') } — run stages simultaneously (= GHA jobs in parallel)
  • when { branch 'main' } — conditional stage — only runs on main (= GHA if: github.ref == 'refs/heads/main')
  • withCredentials([...]) — inject credentials securely into steps (= GHA secrets.*)
  • input — pauses pipeline until a human clicks Approve (= GHA Environments with required reviewers)
  • post { always/success/failure } — runs after pipeline regardless of result
  • cleanWs() — clean workspace plugin — avoids stale files
withCredentials vs env:
In Jenkins, secrets are stored in the Credentials Store, not environment variables. withCredentials injects them as environment variables only inside its block — they're masked in logs and not accessible outside the block. Equivalent of GitHub's secrets.*.
Part 5 of 5

Shared Libraries & Reusable Workflows — DRY pipelines

Jenkins Shared Library

10 teams have the same "build Docker + push + deploy" logic in their Jenkinsfiles. One change needs to be made in 10 places. Solution: Shared Library.

// vars/buildAndPush.groovy (in shared-lib repo)
def call(Map config) {
    sh "docker build -t ${config.image}:${config.tag} ."
    sh "docker push ${config.image}:${config.tag}"
}

// Any Jenkinsfile using the library:
@Library('jenkins-shared-lib') _

pipeline {
    agent any
    stages {
        stage('Build') {
            steps {
                buildAndPush(
                    image: 'myorg/myapp',
                    tag:   env.BUILD_NUMBER
                )
            }
        }
    }
}
GitHub Actions Reusable Workflow
# .github/workflows/reusable-build.yml
on:
  workflow_call:          # ← makes it reusable
    inputs:
      image-name:
        required: true
        type: string
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: docker build -t \${{ inputs.image-name }} .

# Caller workflow in any repo:
jobs:
  build-app:
    uses: org/shared-workflows/.github/workflows/reusable-build.yml@main
    with:
      image-name: myorg/myapp
Why DRY pipelines matter
An organisation with 20 microservices shouldn't have 20 copies of the same Docker build logic. When the Docker registry URL changes, do you update 20 Jenkinsfiles?

Shared Library / Reusable Workflow: update in one place → all pipelines get it automatically.
Jenkins SL structure
jenkins-shared-lib/ (separate Git repo)
├── vars/
│   ├── buildAndPush.groovy   ← global functions
│   └── runTests.groovy
├── src/
│   └── org/helper/Utils.groovy
└── resources/
    └── config.yaml

Register in Jenkins: Manage Jenkins → System → Global Pipeline Libraries → add repo URL + name.

💡 npm ci vs npm install in CI
Always use npm ci in pipelines: installs exact locked versions from package-lock.json, fails if lock file is stale (catches drift), never modifies lock file, faster than npm install in CI. npm install is for development; npm ci is for CI.
Hands-On Lab

🔧 Multi-Job Pipeline & Jenkinsfile

Multi-job GHA with matrix + secret → Jenkins equivalent with parallel stages + credentials

⏱ 30 minutes
my-devops-app ✓
GitHub secret ready ✓
🔧 Lab — Steps

Multi-job pipeline exercises

1
Create multi-job GitHub Actions workflow
Write .github/workflows/ci-advanced.yml with 3 jobs: lint, test (matrix: Node 18 + 20), build (needs: both). Verify 4 jobs run in the Actions tab (lint + test×2 parallel + build).
2
Add a secret and use it in the build job
GitHub repo → Settings → Secrets → add APP_VERSION=1.0.0. Reference it in the build job with \$\{{ secrets.APP_VERSION }}. Verify it appears as *** in logs.
3
Add npm cache to all jobs
Add cache: 'npm' to every actions/setup-node step. Trigger the workflow twice. Compare run times — second run should be ~60s faster.
4
Write the equivalent Jenkinsfile with parallel stages
Create / update the Jenkinsfile with a parallel { } block for lint + test. Add when { branch 'main' } to the build stage. Add a post { success/failure } block.
5
Add withCredentials to Jenkinsfile
In Jenkins: add a "Username with password" credential with ID app-version-creds. Use withCredentials in the Jenkinsfile build stage to inject it.
🔧 Lab — GitHub Actions Code

.github/workflows/ci-advanced.yml

ci-advanced.yml
name: CI Advanced

on:
  push:
    branches: [ main, 'feat/**' ]
  pull_request:
    branches: [ main ]

jobs:
  # ── 1. Lint ─────────────────────────
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci
      - run: npm run lint || echo "No lint script — OK"

  # ── 2. Test — matrix Node 18 + 20 ───
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [ 18, 20 ]
      fail-fast: false
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: \${{ matrix.node-version }}
          cache: 'npm'
      - run: npm ci && npm test

  # ── 3. Build (needs lint + test) ─────
  build:
    needs: [ lint, test ]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    env:
      APP_VERSION: \${{ secrets.APP_VERSION }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci
      - name: Build
        run: echo "Building version $APP_VERSION"
      - name: Upload dist
        uses: actions/upload-artifact@v4
        with:
          name: build-\${{ github.sha }}
          path: dist/
          if-no-files-found: warn
Jenkinsfile — Parallel + Credentials
pipeline {
    agent any
    tools { nodejs 'NodeJS-20' }

    stages {
        stage('Checkout') {
            steps { checkout scm }
        }

        // Parallel lint + test (= GHA parallel jobs)
        stage('Quality') {
            parallel {
                stage('Lint') {
                    steps {
                        sh 'npm ci'
                        sh 'npm run lint || echo "No lint"'
                    }
                }
                stage('Test') {
                    steps {
                        sh 'npm ci && npm test'
                    }
                }
            }
        }

        stage('Build') {
            when { branch 'main' }
            steps {
                withCredentials([
                    string(credentialsId: 'app-version',
                           variable: 'APP_VERSION')
                ]) {
                    sh """
                        echo "Building $APP_VERSION"
                        npm ci
                    """
                }
            }
        }
    }

    post {
        success { echo '✅ Build passed!' }
        failure { echo '❌ Build failed!' }
        always  { cleanWs() }
    }
}
💡 Commit both files
ci: add multi-job pipeline with matrix and parallel stages
Knowledge Check

Quiz Time

3 questions · 5 minutes · matrix, secrets, Jenkins credentials

Day 12 knowledge check →
QUESTION 1 OF 3
In GitHub Actions, what does needs: [lint, test] do in a job definition?
A
Runs lint and test inside the current job as steps
B
Makes the current job wait until both lint and test jobs complete successfully
C
Installs the lint and test npm packages before the job runs
D
Skips the current job if lint or test fail
QUESTION 2 OF 3
Why does CI pipelines always use npm ci instead of npm install?
A
npm ci skips devDependencies, making builds smaller
B
npm ci only works on Linux runners
C
npm ci installs exact locked versions from package-lock.json and fails if the lock file is out of sync — ensuring reproducible builds
D
npm ci is faster because it skips the download phase
QUESTION 3 OF 3
In Jenkins, what is the equivalent of GitHub Actions \$\{{ secrets.TOKEN }} for injecting credentials securely?
A
environment { TOKEN = 'hardcoded-value' }
B
withCredentials([string(credentialsId: 'token-id', variable: 'TOKEN')])
C
sh 'cat /etc/jenkins/secrets/TOKEN'
D
params.TOKEN
Day 12 — Complete

What you learned today

needs: + Matrix
Fan-out parallel jobs. needs: fan-in. Matrix = same job N configs simultaneously.
🔐
Secrets
Repo / Environment / Org scopes. Never print. withCredentials in Jenkins.
🚀
Cache + Artifacts
cache: 'npm' → 60s → 5s. upload-artifact / download-artifact for job handoff.
🏗
Jenkinsfile
parallel { }, when { branch }, input (approval), withCredentials, post conditions.
Day 12 Action Items
  1. ci-advanced.yml live — 4 jobs visible in Actions tab ✓
  2. Matrix (Node 18 + 20) both green ✓
  3. Jenkinsfile with parallel stages committed ✓
  4. Commit: ci: add multi-job pipeline with matrix
Tomorrow — Day 13
Testing in CI

Jest unit tests, code coverage thresholds, test reports published to GitHub Actions, coverage badge. Add a real test suite and gate the pipeline on 80% coverage.

Jest --coverage coverageThreshold LCOV report
📌 Reference

GitHub Actions advanced — complete patterns

Advanced job patterns
# Conditional on branch + event
if: github.ref == 'refs/heads/main' && github.event_name == 'push'

# Continue even if a step fails
- run: npm run lint
  continue-on-error: true

# Set output from one step, use in next
- id: version
  run: echo "tag=$(git describe --tags)" >> $GITHUB_OUTPUT
- run: echo "Version is \${{ steps.version.outputs.tag }}"

# Timeout on a step
- run: npm test
  timeout-minutes: 10

# Retry a flaky step
- uses: nick-fields/retry@v3
  with:
    timeout_minutes: 5
    max_attempts: 3
    command: npm test

# Environment-gated deployment
jobs:
  deploy-prod:
    environment:
      name: production
      url: https://myapp.com
    # GitHub will show approval UI before this runs
Jenkins advanced patterns
// Run stage only on specific branch
when {
    anyOf {
        branch 'main'
        branch 'release/*'
    }
}

// Environment variables from credentials
withCredentials([
    usernamePassword(
        credentialsId: 'docker-hub',
        usernameVariable: 'DOCKER_USER',
        passwordVariable: 'DOCKER_PASS'),
    string(
        credentialsId: 'slack-token',
        variable: 'SLACK_TOKEN')
]) {
    sh 'docker login -u $DOCKER_USER -p $DOCKER_PASS'
}

// Parameterised build (human input at trigger)
parameters {
    choice(
        name: 'ENVIRONMENT',
        choices: ['staging', 'production'],
        description: 'Deploy target')
    booleanParam(
        name: 'SKIP_TESTS',
        defaultValue: false)
}
// Use: params.ENVIRONMENT

// Archive build artifacts
post {
    success {
        archiveArtifacts artifacts: 'dist/**'
        publishHTML(target: [
            reportDir: 'coverage',
            reportFiles: 'index.html',
            reportName: 'Coverage Report'
        ])
    }
}
📌 Reference — Side-by-Side

GitHub Actions vs Jenkins — concept mapping

Concept GitHub Actions Jenkins
Pipeline file.github/workflows/ci.ymlJenkinsfile
Top-level blockjobs:pipeline { stages { } }
Execution unitjobstage('Name')
Runner/Agentruns-on: ubuntu-latestagent any / agent { label 'x' }
Shell commandrun: npm testsh 'npm test'
Secrets\$\{{ secrets.NAME }}withCredentials([...])
Paralleljobs run in parallel by defaultparallel { stage('a') stage('b') }
Sequentialneeds: [job-a]stages execute top-to-bottom by default
Conditionalif: github.ref == 'refs/heads/main'when { branch 'main' }
Approval gateenvironment: production (required reviewers)input 'Deploy to prod?'
Post actionsif: always() / failure() / success()post { always { } failure { } }
Reusableworkflow_call + uses: org/repo/wf@main@Library('shared-lib') + function call
Env varsenv: at workflow/job/step levelenvironment { VAR = 'val' }
📌 Reference

Full pipeline lifecycle — from push to production

git push feat/x CI Runs lint+test PR Open review Merge to main CD Runs build+push → Staging auto deploy approval → Production 🚀 Live!
What runs at each step
  • feat/x push — triggers CI workflow (lint + test). Status check on PR.
  • Merge to main — triggers CD workflow (build Docker image + push to registry).
  • Staging auto-deploy — Helm upgrade or kubectl apply using the new image tag.
  • Approval — GitHub Environment required reviewers (or Jenkins input).
  • Production — same deploy command targeting prod namespace/cluster.
💡 Image tag strategy
Tag Docker images with the git commit SHA: myapp:\$\{{ github.sha }}. This gives full traceability — you always know exactly which code is running in production. Semantic version tags (v1.2.3) for releases.
📌 Troubleshooting

Common pipeline issues & fixes

Problem Cause Fix
Workflow not triggeringYAML syntax error or wrong on: branch patternUse yamllint or GitHub's YAML validator. Check branch pattern matches.
npm ci fails "missing package-lock.json"package-lock.json not committedRun npm install locally, commit the lock file. Never gitignore package-lock.json.
Secret shows as "***" in unexpected placeNormal — GitHub redacts known secret valuesExpected behaviour. Never intentionally print secrets.
matrix job fails on one version onlyVersion-specific API or syntax differenceClick the failing version in Actions tab. Add fail-fast: false to see all results.
build job skipped (yellow ○)needs: job failed OR if: condition not metCheck if: condition. If needs job failed, fix that first. Run on correct branch.
cache always missing (no speedup)Cache key changing every runKey must include stable part (os) + changing part (lock file hash). Use restore-keys: for fallback.
Jenkins: "No such tool" NodeJSNodeJS tool not configured or name mismatchManage Jenkins → Tools → NodeJS. Name must exactly match Jenkinsfile tools { nodejs 'NAME' }.
Jenkins: parallel stages not runningOnly 1 agent availableParallel requires separate agents. Docker agent or add more Jenkins agents.
withCredentials secret is emptycredentialsId doesn't match stored credential IDManage Jenkins → Credentials → check exact ID value. IDs are case-sensitive.
Week 3 Progress

Week 3 — 2 of 5 days complete

Day Topic Lab Output Status
Day 11CI/CD ConceptsFirst pipeline live in Actions tab
Day 12 ← TODAYPipelines Deep DiveMulti-job GHA + Jenkins parallel stages
Day 13Testing in CIJest + 80% coverage gateTomorrow
Day 14Artifacts & DockerDocker image pushed to GHCR on mergeThu
Day 15Continuous DeploymentStaging auto + prod approval gateFri
Pipeline Building Blocks So Far
✅ Trigger on push/PR
✅ Multi-job with dependencies (needs:)
✅ Matrix for parallel testing
✅ Secrets management
✅ npm cache (60s → 5s installs)
✅ Artifact handoff between jobs
✅ Jenkins parallel + withCredentials

Still to add: Tests + coverage, Docker build/push, CD deploy
Your pipeline file structure by Day 15
my-devops-app/
├── .github/
│   └── workflows/
│       ├── ci.yml          ← Day 11 (basic)
│       ├── ci-advanced.yml ← Day 12 (matrix)
│       ├── test.yml        ← Day 13 (Jest)
│       ├── docker.yml      ← Day 14 (Docker)
│       └── deploy.yml      ← Day 15 (CD)
└── Jenkinsfile             ← Days 11-12
📌 Deep Dive

npm ci vs npm install — why CI always uses npm ci

npm ci vs npm install
# npm install (development)
npm install
# → Reads package.json
# → Resolves versions (may get newer patch)
# → Updates package-lock.json if needed
# → Merges with existing node_modules
# → Fine for developer machines

# npm ci (CI/production — always use this)
npm ci
# → Reads package-lock.json only
# → Installs EXACT versions (no resolution)
# → Fails if package-lock.json is missing
# → Fails if package-lock.json is out of sync with package.json
# → Deletes node_modules FIRST (clean install)
# → Never modifies package-lock.json
# → FASTER in CI (no resolution step)

# === The lock file sync problem ===
# Developer adds package, forgets to commit lock file
# npm install: silently resolves → works
# npm ci: FAILS → catches the problem ← correct behaviour

# === Fix: always commit package-lock.json ===
git add package.json package-lock.json
git commit -m "chore: add express dependency"
Why npm ci matters for reproducibility
If developer A uses lodash 4.17.20 and the CI build uses 4.17.21 (npm install picked it up), you have a discrepancy. A bug in 4.17.21 breaks CI but passes on developer's machine.

npm ci ensures: developer machine = CI = staging = production. Same exact dependency versions everywhere. This is the single biggest reason for "works on my machine" problems in CI.
npm ci performance
  • No version resolution → faster
  • Deterministic → better for caching (same cache key = same result)
  • Cache hit: npm ci with cache ~5 sec vs cold 60+ sec
  • Works great with actions/setup-node cache: 'npm'
💡 Other lock files
yarn.lockyarn install --frozen-lockfile
pnpm-lock.yamlpnpm install --frozen-lockfile
requirements.txtpip install -r requirements.txt
All follow the same principle: locked versions in CI.
📌 One-Page Reference

Day 12 — everything at a glance

GitHub Actions patterns
# Matrix
strategy:
  matrix:
    node: [ 18, 20 ]
  fail-fast: false
steps:
  - uses: actions/setup-node@v4
    with:
      node-version: \${{ matrix.node }}
      cache: 'npm'

# Fan-in
build:
  needs: [ lint, test ]
  if: github.ref == 'refs/heads/main'

# Secrets
env:
  TOKEN: \${{ secrets.MY_TOKEN }}

# Artifact upload
- uses: actions/upload-artifact@v4
  with:
    name: dist
    path: dist/

# Conditional step
- if: failure()
  run: echo "Step failed!"
Jenkins Declarative patterns
// Parallel stages
stage('Quality') {
    parallel {
        stage('Lint')  { steps { sh '...' } }
        stage('Test')  { steps { sh '...' } }
    }
}

// Conditional stage
stage('Deploy') {
    when { branch 'main' }
    steps { sh 'deploy.sh' }
}

// Credentials
withCredentials([
    usernamePassword(
        credentialsId: 'docker-hub',
        usernameVariable: 'USER',
        passwordVariable: 'PASS')
]) { sh 'docker login -u $USER -p $PASS' }

// Approval gate
input message: 'Deploy to prod?', ok: 'Go!'

// Post conditions
post {
    success { echo '✅' }
    failure { echo '❌' }
    always  { cleanWs() }
}
💡 Always use npm ci in CI
npm ci in pipelines. npm install on developer machines. Commit package-lock.json. Never gitignore it.