चार कुरिअर कंपन्या, तोच मार्ग, वेगळे फॉर्म. खालची प्रत्येक फाइल repo मध्ये आहे आणि त्याच conveyor चे वर्णन करते: cache आणि JUnit अहवालासह तीन Node आवृत्त्यांवर चाचणी → commit SHA ने tag केलेली image build, smoke-test आणि push → staging वर deploy → माणूस मंजुरी देतो → production. धडा 11 तक्ता समजावतो; धडे 03–10 प्रत्येक तुकडा चारही बोलींमध्ये दाखवतात.
एक ओळ आडवी वाचा आणि तुम्हाला भेटणारी कोणतीही pipeline भाषांतरित करता येईल. keyword खऱ्या फाइली वापरतात तेच आहेत.
| संकल्पना | ⚙️ GitHub Actions | 🔄 CircleCI | 🦊 GitLab CI | 🎩 Jenkins |
|---|---|---|---|---|
| Config फाइल | .github/workflows/*.yml | .circleci/config.yml | .gitlab-ci.yml | Jenkinsfile |
| Trigger 🔔 | on: push / pull_request | default प्रत्येक push; filters: ने संकुचित करा | default प्रत्येक push; rules: if: ने संकुचित करा | प्रत्येक push (multibranch); when { branch } ने संकुचित करा |
| संपूर्ण run | workflow | workflow | pipeline | pipeline |
| कामाचे एकक | job (runs-on) | job (docker: executor) | stage मधील job | stage (+ agent) |
| एक काम | steps: - run: / uses: | steps: - run: / orb आज्ञा | एक script: ओळ | steps { sh '…' } |
| यंत्र | runner (ubuntu-latest) | executor (cimg/node:22.12) | runner (image: node:22-alpine) | agent (docker { image }) |
| Matrix 📏 | strategy: matrix: | matrix: parameters: | parallel: matrix: | matrix { axes { … } } |
| Cache 🗄️ | actions/cache, key hashFiles() ने | restore_cache / save_cache, key checksum ने | cache: key: files: | built-in नाही — टिकणारा workspace किंवा Job Cacher plugin |
| Artifact 📎 | upload-artifact / download-artifact | store_artifacts / persist_to_workspace | artifacts: paths: | archiveArtifacts / stash |
| चाचणी अहवाल 📋 | JUnit XML artifact म्हणून upload करा | store_test_results | artifacts: reports: junit (MR widget) | junit पायरी |
| Secrets 🔐 | secrets.X (झाकलेले) · vars.X (उघड) | project env var · context: प्रत्येक job ला | CI/CD variables (झाकलेले, संरक्षित) | Credentials + withCredentials / withAWS |
| Cloud ओळख 🪪 | permissions: id-token: write + configure-aws-credentials | aws-cli/setup with role_arn | id_tokens: + assume-role-with-web-identity | plugin किंवा agent वरील instance role |
| फक्त main वर | on: push: branches: [main] | filters: branches: only: main | rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH | when { branch 'main' } |
| मंजुरीचे गेट 🛑 | environment: + required reviewers | type: approval | when: manual | input पायरी |
| Environments | environment: staging / production | context + approval job | environment: name: | stage (plugin आणखी देतात) |
node prepare.js, node --test आणि docker build … && docker push चालवतात. logic script आणि package.json मध्ये ठेवा, YAML पातळ ठेवा, म्हणजे विक्रेता बदलणे हे पुनर्लेखन न राहता भाषांतर होते.📄 .github/workflows/ci.yml
# ✅ The checking desk (lessons 03–05): runs on EVERY push and pull request.
# Fork this repo, push a commit, and watch this go green (or red) in the Actions tab.
name: ✅ ci
on:
push:
pull_request:
permissions:
contents: read # lesson 06: the job's own token gets the least it needs
concurrency: # a newer push cancels the older run of the same branch
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
name: test (node ${{ matrix.node }})
runs-on: ubuntu-latest
strategy:
fail-fast: false # lesson 04: let all three rulers finish, even if one fails
matrix:
node: [20, 22, 24] # three rulers — the same homework checked three ways
defaults:
run:
working-directory: app
steps:
- name: 📥 checkout
uses: actions/checkout@v4
- name: 🟢 node ${{ matrix.node }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- name: 🗄️ cache the slow preparation (lesson 04)
id: cache
uses: actions/cache@v4
with:
path: app/prepared
key: prepared-${{ hashFiles('app/prepare.js') }} # change prepare.js → new key → miss
- name: ⏳ prepare (slow on a miss, skipped on a hit)
if: steps.cache.outputs.cache-hit != 'true'
run: node prepare.js
- name: 🧪 test
run: >
node --test
--test-reporter=spec --test-reporter-destination=stdout
--test-reporter=junit --test-reporter-destination=test-results.xml
- name: 📎 upload the report card (lesson 05)
if: always() # especially when tests FAIL — that's when you need the report
uses: actions/upload-artifact@v4
with:
name: test-results-node${{ matrix.node }}
path: app/test-results.xml
retention-days: 7
📄 .github/workflows/ship.yml
# 📦 The photocopier (lesson 07): build the image, prove it runs, push it to ECR
# tagged with the commit SHA — then hand it to the delivery van (deploy.yml).
#
# Runs on every push to main. The PUSH and DEPLOY jobs only run when the repo has the
# variables below (Settings → Secrets and variables → Actions → Variables); a plain
# fork stays green and simply skips them:
# AWS_ROLE_ARN arn:aws:iam::123456789012:role/learn-cicd-school-ci (iam/ shows the role)
# AWS_REGION ap-south-1
# ECR_REPOSITORY hello-courier
# EKS_CLUSTER school-eks (deploy.yml only)
name: 📦 ship
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
id-token: write # lesson 06: lets this run ask for an OIDC badge — no stored AWS keys
jobs:
test:
runs-on: ubuntu-latest
defaults: { run: { working-directory: app } }
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
- run: node --test
build:
name: build & smoke-test the image
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 🍱 docker build (version label = short SHA)
run: docker build --build-arg APP_VERSION=${GITHUB_SHA::7} -t hello-courier:${GITHUB_SHA} app
- name: 🧪 test the BOX, not just the code
run: |
docker run -d --rm -p 3000:3000 --name smoke hello-courier:${GITHUB_SHA}
for i in $(seq 1 20); do curl -fsS localhost:3000/healthz && break; sleep 0.5; done
curl -fsS localhost:3000/ | grep -q "version ${GITHUB_SHA::7}"
docker stop smoke
- name: 💾 keep the image for the push job (same run, different machine)
run: docker save hello-courier:${GITHUB_SHA} | gzip > image.tar.gz
- uses: actions/upload-artifact@v4
with: { name: image, path: image.tar.gz, retention-days: 1 }
push:
name: push to ECR
needs: build
if: vars.AWS_ROLE_ARN != '' # skipped on forks without AWS — still green
runs-on: ubuntu-latest
outputs:
image: ${{ steps.meta.outputs.image }}
steps:
- uses: actions/download-artifact@v4
with: { name: image }
- run: docker load < image.tar.gz
- name: 🪪 show the badge, get a day pass (OIDC → temporary credentials)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN }}
aws-region: ${{ vars.AWS_REGION }}
- name: 🎫 log in to ECR
id: ecr
uses: aws-actions/amazon-ecr-login@v2
- name: 🏷️ tag with the commit SHA and push
id: meta
env:
IMAGE: ${{ steps.ecr.outputs.registry }}/${{ vars.ECR_REPOSITORY }}:${{ github.sha }}
run: |
docker tag hello-courier:${GITHUB_SHA} "$IMAGE"
docker push "$IMAGE"
echo "image=$IMAGE" >> "$GITHUB_OUTPUT"
deploy:
name: 🚚 deliver
needs: push
if: vars.EKS_CLUSTER != ''
permissions:
contents: read
id-token: write
uses: ./.github/workflows/deploy.yml # lesson 08–10: staging → approval → production
with:
image: ${{ needs.push.outputs.image }}
📄 .github/workflows/deploy.yml
# 🚚 The delivery van (lessons 08–10): staging first, then a human signs, then production.
#
# Called by ship.yml after a successful push, or started by hand (Actions → 🚚 deploy → Run).
# The "production" environment must be created once in Settings → Environments with
# "Required reviewers" — THAT checkbox is the approval gate (lesson 08). Nothing here
# deploys until a named human clicks Approve.
name: 🚚 deploy
on:
workflow_call:
inputs:
image:
description: full image reference, e.g. 123456789012.dkr.ecr.ap-south-1.amazonaws.com/hello-courier:<sha>
required: true
type: string
workflow_dispatch:
inputs:
image:
description: full image reference to deploy
required: true
type: string
permissions:
contents: read
id-token: write
jobs:
staging:
runs-on: ubuntu-latest
environment: staging # the practice notice board
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with: { role-to-assume: "${{ vars.AWS_ROLE_ARN }}", aws-region: "${{ vars.AWS_REGION }}" }
- name: 🔧 point kubectl at the cluster
run: aws eks update-kubeconfig --region "${{ vars.AWS_REGION }}" --name "${{ vars.EKS_CLUSTER }}"
- name: 📌 pin the new notice (rolling update, lesson 09)
run: |
sed -i "s|IMAGE_PLACEHOLDER|${{ inputs.image }}|" k8s/deployment.yaml
kubectl apply -n staging -f k8s/
kubectl rollout status -n staging deployment/hello-courier --timeout=180s
- name: 🧪 smoke-test staging from inside the cluster
run: kubectl run -n staging smoke --rm -i --restart=Never --image=curlimages/curl -- -fsS http://hello-courier/healthz
production:
needs: staging
runs-on: ubuntu-latest
environment: production # 🛑 required reviewers = the principal's signature
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with: { role-to-assume: "${{ vars.AWS_ROLE_ARN }}", aws-region: "${{ vars.AWS_REGION }}" }
- run: aws eks update-kubeconfig --region "${{ vars.AWS_REGION }}" --name "${{ vars.EKS_CLUSTER }}"
- name: 📌 roll it out to the main board
run: |
sed -i "s|IMAGE_PLACEHOLDER|${{ inputs.image }}|" k8s/deployment.yaml
kubectl apply -n production -f k8s/
kubectl rollout status -n production deployment/hello-courier --timeout=180s
# rollback = kubectl rollout undo deployment/hello-courier -n production (lesson 09)
# …or, better, re-run this workflow with yesterday's image tag: the tag IS the version.
📄 .circleci/config.yml
# 🔄 The SAME pipeline in the CircleCI dialect (lesson 11).
# test (matrix) → build-and-push (context = the badge) → hold-for-approval → deploy-to-eks
# Modeled on a real pipeline: only the jobs that talk to AWS receive the context (least privilege).
# The context `school-cicd` must provide AWS_ROLE_ARN (OIDC) or AWS keys, AWS_ACCOUNT_ID,
# AWS_DEFAULT_REGION, ECR_REPOSITORY, EKS_CLUSTER.
version: 2.1
orbs:
aws-cli: circleci/aws-cli@5.1 # `aws-cli/setup` with role_arn = OIDC, no stored keys
parameters:
node-versions:
type: string
default: "20.18,22.12,24.0"
jobs:
test:
parameters:
node:
type: string
docker:
- image: cimg/node:<< parameters.node >>
working_directory: ~/repo/app
steps:
- checkout:
path: ~/repo
- restore_cache: # lesson 04: the drawer
keys:
- prepared-v1-{{ checksum "prepare.js" }}
- run:
name: ⏳ prepare (skipped when restored from cache)
command: node prepare.js
- save_cache:
key: prepared-v1-{{ checksum "prepare.js" }}
paths: [prepared]
- run:
name: 🧪 test
command: |
mkdir -p test-results
node --test --test-reporter=spec --test-reporter-destination=stdout \
--test-reporter=junit --test-reporter-destination=test-results/junit.xml
- store_test_results: # lesson 05: the report card, parsed
path: test-results
- store_artifacts: # …and the raw file, downloadable
path: test-results
build-and-push:
docker:
- image: cimg/aws:2024.03 # AWS CLI + Docker CLI preinstalled
steps:
- checkout
- setup_remote_docker # a Docker engine for this job to build with
- aws-cli/setup: # 🪪 OIDC badge → temporary credentials
role_arn: ${AWS_ROLE_ARN}
region: ${AWS_DEFAULT_REGION}
- run:
name: 🍱 build, smoke-test, tag with the commit SHA, push
command: |
REGISTRY="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com"
IMAGE="${REGISTRY}/${ECR_REPOSITORY}:${CIRCLE_SHA1}"
docker build --build-arg APP_VERSION=${CIRCLE_SHA1:0:7} -t "$IMAGE" app
docker run -d --rm -p 3000:3000 --name smoke "$IMAGE"
sleep 2 && docker exec smoke wget -qO- localhost:3000/healthz && docker stop smoke
aws ecr get-login-password --region "$AWS_DEFAULT_REGION" \
| docker login --username AWS --password-stdin "$REGISTRY"
docker push "$IMAGE"
deploy-to-eks:
parameters:
namespace:
type: string
docker:
- image: cimg/aws:2024.03
steps:
- checkout
- aws-cli/setup:
role_arn: ${AWS_ROLE_ARN}
region: ${AWS_DEFAULT_REGION}
- run:
name: 🔧 kubectl → cluster
command: |
curl -sLO "https://dl.k8s.io/release/$(curl -sL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
sudo install kubectl /usr/local/bin/kubectl
aws eks update-kubeconfig --region "$AWS_DEFAULT_REGION" --name "$EKS_CLUSTER"
- run:
name: 📌 apply and wait for the rollout
command: |
IMAGE="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${ECR_REPOSITORY}:${CIRCLE_SHA1}"
sed -i "s|IMAGE_PLACEHOLDER|${IMAGE}|" k8s/deployment.yaml
kubectl apply -n << parameters.namespace >> -f k8s/
kubectl rollout status -n << parameters.namespace >> deployment/hello-courier --timeout=180s
workflows:
test-build-deploy:
jobs:
- test: # NO context: tests never see cloud credentials
matrix:
parameters:
node: ["20.18", "22.12", "24.0"]
- build-and-push:
context: school-cicd
requires: [test]
filters:
branches:
only: main # only main gets built and pushed
- deploy-to-eks:
name: deploy-staging
namespace: staging
context: school-cicd
requires: [build-and-push]
- hold-for-approval: # 🛑 the principal's signature: click Approve in the UI
type: approval
requires: [deploy-staging]
- deploy-to-eks:
name: deploy-production
namespace: production
context: school-cicd
requires: [hold-for-approval]
📄 .gitlab-ci.yml
# 🦊 The SAME pipeline in the GitLab CI dialect (lesson 11).
# stages: test → build → deploy (jobs in one stage run in parallel)
# Set these CI/CD variables on the project: AWS_ROLE_ARN, AWS_REGION, AWS_ACCOUNT_ID,
# ECR_REPOSITORY, EKS_CLUSTER. Credentials come from an ID token (OIDC) — no stored keys.
stages: [test, build, deploy]
variables:
AWS_REGION: ap-south-1
test:
stage: test
image: node:${NODE}-alpine
parallel:
matrix: # lesson 04: three rulers
- NODE: ["20", "22", "24"]
cache: # lesson 04: the drawer, keyed by the file that defines the work
key:
files: [app/prepare.js]
paths: [app/prepared/]
script:
- cd app
- node prepare.js # prints "already exists" on a cache hit
- node --test --test-reporter=spec --test-reporter-destination=stdout
--test-reporter=junit --test-reporter-destination=test-results.xml
artifacts: # lesson 05: the report card — parsed into the MR widget
when: always
reports:
junit: app/test-results.xml
paths: [app/test-results.xml]
expire_in: 1 week
.aws-badge: &aws-badge # 🪪 OIDC: exchange GitLab's ID token for temporary AWS credentials
id_tokens:
GITLAB_OIDC_TOKEN:
aud: https://gitlab.com
before_script:
- >
export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s"
$(aws sts assume-role-with-web-identity
--role-arn "$AWS_ROLE_ARN" --role-session-name "gitlab-${CI_PIPELINE_ID}"
--web-identity-token "$GITLAB_OIDC_TOKEN" --duration-seconds 3600
--query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' --output text))
build-and-push:
stage: build
image: docker:27
services: [docker:27-dind]
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # only the default branch gets built and pushed
<<: *aws-badge
script:
- apk add --no-cache aws-cli curl
- REGISTRY="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"
- IMAGE="${REGISTRY}/${ECR_REPOSITORY}:${CI_COMMIT_SHA}"
- docker build --build-arg APP_VERSION=${CI_COMMIT_SHORT_SHA} -t "$IMAGE" app
- docker run -d --rm -p 3000:3000 --name smoke "$IMAGE" && sleep 2 && docker exec smoke wget -qO- localhost:3000/healthz && docker stop smoke
- aws ecr get-login-password --region "$AWS_REGION" | docker login --username AWS --password-stdin "$REGISTRY"
- docker push "$IMAGE"
.deploy: &deploy
stage: deploy
image: amazon/aws-cli:2.17.0
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
<<: *aws-badge
script:
- curl -sLO "https://dl.k8s.io/release/$(curl -sL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" && install kubectl /usr/local/bin/kubectl
- aws eks update-kubeconfig --region "$AWS_REGION" --name "$EKS_CLUSTER"
- IMAGE="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_REPOSITORY}:${CI_COMMIT_SHA}"
- sed -i "s|IMAGE_PLACEHOLDER|${IMAGE}|" k8s/deployment.yaml
- kubectl apply -n "$NAMESPACE" -f k8s/
- kubectl rollout status -n "$NAMESPACE" deployment/hello-courier --timeout=180s
deploy-staging:
<<: *deploy
variables: { NAMESPACE: staging }
environment: { name: staging }
deploy-production:
<<: *deploy
needs: [deploy-staging]
variables: { NAMESPACE: production }
environment: { name: production }
when: manual # 🛑 the principal's signature: a human presses ▶ in the UI
📄 Jenkinsfile
// 🎩 The SAME pipeline in the Jenkins (declarative) dialect — lesson 11.
// ✅ test (matrix) → 📦 build & push → 🛑 approve → 🚚 deploy
// Needs on the controller: Docker Pipeline, JUnit and Pipeline: AWS Steps plugins, plus a
// credential `aws-school-cicd` (IAM keys or, better, a role assumed by the agent).
pipeline {
agent none
options { timestamps(); disableConcurrentBuilds() }
environment {
AWS_REGION = 'ap-south-1'
ECR_REPOSITORY = 'hello-courier'
EKS_CLUSTER = 'school-eks'
}
stages {
stage('✅ test') {
matrix { // lesson 04: three rulers, in parallel
axes { axis { name 'NODE'; values '20', '22', '24' } }
agent { docker { image "node:${NODE}-alpine" } }
stages {
stage('test') {
steps {
dir('app') {
// Jenkins has no built-in cache step: a persistent agent keeps the workspace
// (so prepared/ survives between builds); ephemeral agents need the Job Cacher plugin.
sh 'node prepare.js'
sh '''node --test --test-reporter=spec --test-reporter-destination=stdout \
--test-reporter=junit --test-reporter-destination=test-results.xml'''
}
}
post { always { junit 'app/test-results.xml' } } // lesson 05: the report card
}
}
}
}
stage('📦 build & push') {
when { branch 'main' }
agent any
steps {
withAWS(credentials: 'aws-school-cicd', region: env.AWS_REGION) {
sh '''
ACCOUNT=$(aws sts get-caller-identity --query Account --output text)
REGISTRY="${ACCOUNT}.dkr.ecr.${AWS_REGION}.amazonaws.com"
IMAGE="${REGISTRY}/${ECR_REPOSITORY}:${GIT_COMMIT}"
docker build --build-arg APP_VERSION=$(echo "$GIT_COMMIT" | cut -c1-7) -t "$IMAGE" app # POSIX sh: no ${VAR:0:7}
docker run -d --rm -p 3000:3000 --name smoke "$IMAGE" && sleep 2 \
&& docker exec smoke wget -qO- localhost:3000/healthz && docker stop smoke
aws ecr get-login-password --region "$AWS_REGION" | docker login --username AWS --password-stdin "$REGISTRY"
docker push "$IMAGE"
'''
}
}
}
stage('🚚 staging') {
when { branch 'main' }
agent any
steps { script { deployTo('staging') } }
}
stage('🛑 approve') {
when { branch 'main' }
options { timeout(time: 2, unit: 'DAYS') }
steps { input message: 'Deploy to production?', ok: 'Approve' } // the principal's signature
}
stage('🚚 production') {
when { branch 'main' }
agent any
steps { script { deployTo('production') } }
}
}
}
def deployTo(String namespace) {
withAWS(credentials: 'aws-school-cicd', region: env.AWS_REGION) {
sh """
ACCOUNT=\$(aws sts get-caller-identity --query Account --output text)
IMAGE="\${ACCOUNT}.dkr.ecr.${env.AWS_REGION}.amazonaws.com/${env.ECR_REPOSITORY}:${env.GIT_COMMIT}"
aws eks update-kubeconfig --region ${env.AWS_REGION} --name ${env.EKS_CLUSTER}
sed -i "s|IMAGE_PLACEHOLDER|\${IMAGE}|" k8s/deployment.yaml
kubectl apply -n ${namespace} -f k8s/
kubectl rollout status -n ${namespace} deployment/hello-courier --timeout=180s
"""
}
}