🏫 The School›📮 CI/CD›✅ धडा 03 — तुमची पहिली pipeline: प्रत्येक push वर हिरवा check
🖼️ See the drawing + lab 🏠 Course home 🌿 Branch on GitHub ✏️ View source
🖼️ आकृती आणि labThe drawing + lab पूर्ण पानावर उघडा ↗Open full page ↗

✅ धडा 03 — तुमची पहिली pipeline: प्रत्येक push वर हिरवा check

📍 तुम्ही इथे आहात: 12 पैकी धडा 03 · मागे: lesson-02-pipeline-anatomy · पुढे: lesson-04-cache-parallel-matrix


📦 या ब्रँचमध्ये काय आहे

धडे 01–02, आणि तुमच्या fork मध्ये courier ची पहिली फेरी: एक push, एक run page, एक हिरवा check — आणि तुम्ही मुद्दाम घडवलेला एक लाल. खऱ्या files:

🧒 5 वर्षांच्या मुलाला समजावल्यासारखे

आज mailroom खरोखर उघडते. तुम्ही फटीत एक कागद टाकता 📮 (एक git push), घंटा वाजते, आणि तीन सारख्या तपासणी डेस्कवरचे ✅ तीन कारकून तुमच्या गृहपाठावर तीच उत्तरपत्रिका चालवतात — Node 20 ची फूटपट्टी, 22 ची, आणि 24 ची.

प्रत्येक कारकून शेवटी शिक्का मारतो. ✅ हिरवा: प्रत्येक तपासणी पास झाली. ❌ लाल: एक पास झाली नाही, आणि कारकून कोणत्या step ने हार मानली हे सांगणारी छोटी चिठ्ठी लावतो. शिक्का तुमच्या commit वर लागतो आणि, कागद एखाद्या pull request चा भाग असेल तर, PR वरही — merge करण्याआधी reviewer नेमके जिथे पाहतो तिथेच.

आणखी एक नियम: पहिली फेरी संपण्याआधी दुसरा कागद टाकला तर कारकून जुनी फेरी सोडून नव्या कागदाकडे वळतो — तुम्ही आधीच बदललेला गृहपाठ तपासण्यात काही अर्थ नाही. तेच म्हणजे cancel-in-progress सह concurrency. आणि शिक्क्यावर विश्वास ठेवण्याचा सर्वोत्तम मार्ग म्हणजे मुद्दाम एक लाल शिक्का मिळवणे. म्हणून आपण तेच करणार आहोत.

🗺️ आकृती

flowchart LR
    push["🧑‍💻 git push"]
    run["📮 one run of ci.yml<br/>one page, three jobs"]
    j["✅ checking desks<br/>node --test on 20, 22, 24"]
    green["✅ green check<br/>on the commit and the PR"]
    red["❌ red X<br/>annotation plus log line"]
    fix["🔧 fix, push again"]
    push -->|"1 on push or pull_request"| run
    run -->|"2 three fresh runners"| j
    j -->|"3 exit code 0"| green
    j -->|"4 exit code 1"| red
    red -->|"5 read the log"| fix
    fix -->|"6 newer push cancels the older run"| run

❓ काय

🧠 शिक्का म्हणजे exit code

command exits 0         → step passes → job passes → run is green → commit / PR gets ✅
command exits non-zero  → step fails  → job fails  → run is red   → commit / PR gets ❌

मूलतः एवढाच संपूर्ण करार आहे. CI ला tests बद्दल "माहीत" नसते — त्याला एवढेच माहीत असते की एखादी fail झाली की node --test non-zero exit करते.

🤔 का

जी pipeline तुम्ही फक्त वाचली आहे ती एक वचन आहे. जी तुम्ही हिरवी, लाल आणि पुन्हा हिरवी होताना पाहिली आहे ती एक tool आहे: failure कसे दिसते, message कुठे असतो, आणि फेरीला किती वेळ लागतो हे तुम्हाला माहीत असते — शुक्रवार संध्याकाळच्या hotfix आधीच. तो जलद, दिसणारा प्रतिसाद हाच धडा 01 च्या mailroom चा मुद्दा आहे. "प्रवासाआधी तपासा" हीच कल्पना Docker शाळेचा धडा 12 CI मध्ये built image चा smoke test करतो तेव्हा पुन्हा दिसते.

🔧 कसे (या repo मध्ये)

ci.yml मधली घंटा — on: push: आणि pull_request:, धडा 02 मध्ये उद्धृत — हिला branch filter नाही, म्हणून तुमच्या fork च्या कोणत्याही branch वरचा push ती वाजवतो, आणि pull request सुद्धा (fork मधल्याच branch वरून आलेला PR दोन्ही वाजवतो — प्रत्येक push ला दोन runs अपेक्षित ठेवा). शिक्का ठरवणारी step server.test.js चालवते आणि कोणत्याही failure वर non-zero exit करते:

      - name: 🧪 test
        run: >
          node --test
          --test-reporter=spec  --test-reporter-destination=stdout
          --test-reporter=junit --test-reporter-destination=test-results.xml

Run page वाचणे. Actions tab → ✅ ci → runs ची यादी. एक उघडा: प्रत्येक job साठी एक box — test (node 20), test (node 22), test (node 24) — डाव्या sidebar मध्येही पुन्हा दिसतात. Steps पाहण्यासाठी job वर click करा; प्रत्येक step उघडली की तिचा log दिसतो. लाल run मध्ये fail झालेल्या step वर खूण असते आणि summary मध्ये annotation असते. Re-run jobs तोच commit पुन्हा चालवते; Re-run failed jobs फक्त लाल असलेले.

जुने runs स्वतःच रद्द होतात:

concurrency:                # a newer push cancels the older run of the same branch
  group: ci-${{ github.ref }}
  cancel-in-progress: true

Group च्या नावात branch (github.ref) आहे, म्हणून फक्त त्याच branch चा जुना run रद्द होतो; तो लाल नाही तर राखाडी दिसतो (GitHub च्या API मध्ये status चे स्पेलिंग cancelled आहे).

दुसरा workflow. तुमच्या fork मध्ये main वरचा push ship.yml सुद्धा वाजवतो: त्याचे test आणि build jobs चालतात, तर push आणि deploy यांना if: vars.AWS_ROLE_ARN != '' चे संरक्षण आहे आणि ते skipped दिसतात — हे जाणूनबुजून; fork मध्ये AWS variables नसतात आणि तरीही तो हिरवाच राहतो.

🔁 हेच CircleCI मध्ये
    docker:
      - image: cimg/node:<< parameters.node >>
    working_directory: ~/repo/app
    steps:
      - checkout:
          path: ~/repo
      # … restore_cache / prepare / save_cache: lesson 04
      - 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

Docker image हा executor आहे, checkout ही स्पष्ट step आहे; workflows: node पुरवते.

🦊 हेच GitLab CI मध्ये
test:
  stage: test
  image: node:${NODE}-alpine
  parallel:
    matrix:                                   # lesson 04: three rulers
      - NODE: ["20", "22", "24"]
  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

Checkout step नाही — GitLab आधी clone करते. प्रत्येक script: ओळ एक step आहे; non-zero exit job थांबवतो.

🎩 हेच Jenkins मध्ये
    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') {
                // … prepare step: lesson 04
                sh '''node --test --test-reporter=spec  --test-reporter-destination=stdout \
                                  --test-reporter=junit --test-reporter-destination=test-results.xml'''

agent { docker { … } } हा कारकून, sh ही step; stage shell च्या exit code नुसार चालते.

🧪 करून पाहा

# 0) in your fork (lesson 02): enable Actions once in the Actions tab, then ring the bell
latest() { gh run list --workflow ci.yml --limit 1 --json databaseId --jq '.[0].databaseId'; }
git commit --allow-empty -m "ring the bell" && git push
gh run watch "$(latest)"             # three jobs → ✅ (a minute or two; a cold runner takes longer)

# 1) earn a red stamp on purpose: /healthz answers "okay" instead of "ok"
sed -i.bak "s/end('ok/end('okay/" app/server.js && rm app/server.js.bak
git commit -am "break /healthz on purpose" && git push
gh run watch "$(latest)"             # ❌ on all three rulers
gh run view "$(latest)" --log-failed | grep -A3 healthz   # expected 'ok\n', actual 'okay\n'

# 2) fix it (the good file is one commit back), then push twice quickly: the first run is canceled
git checkout HEAD~1 -- app/server.js && git commit -am "fix /healthz" && git push
git commit --allow-empty -m "and again" && git push
gh run list --workflow ci.yml --limit 3     # "fix /healthz" canceled, "and again" ✅

# 3) a pull-request check: same workflow, second bell (-R keeps the PR inside YOUR fork)
ME=$(gh api user --jq .login)
git checkout -b pr-check && git commit --allow-empty -m "try a PR" && git push -u origin pr-check
gh pr create -R "$ME/learn-cicd-school" --base main --head pr-check --fill
gh pr checks pr-check -R "$ME/learn-cicd-school" --watch    # the checks a reviewer sees

⚠️ नेहमीच्या चुका

⏭️ पुढे

तुमच्या अगदी पहिल्या run मध्ये तीनही डेस्कनी node prepare.js सुरुवातीपासून चालवले. नंतरच्या runs नी तसे केले नाही. Mailroom नेमके त्यासाठीच तासलेल्या पेन्सिलींचा ड्रॉवर 🗄️ ठेवते — जर तुम्ही तिला ड्रॉवरची किल्ली कशावरून आहे ते सांगितले तर. ⚡

git checkout lesson-04-cache-parallel-matrix

✅ Lesson 03 — Your first pipeline: green check on every push

📍 You are here: Lesson 03 of 12 · Previous: lesson-02-pipeline-anatomy · Next: lesson-04-cache-parallel-matrix


📦 What's in this branch

Lessons 01–02, plus the courier's first round in your fork: a push, a run page, a green check — and a red one you cause on purpose. Real files:

🧒 Explain like I'm 5

Today the mailroom opens for real. You drop a sheet in the slot 📮 (a git push), the bell rings, and three clerks at three identical checking desks ✅ run the same answer key over your homework — with a Node 20 ruler, a 22, and a 24.

Each clerk ends with a stamp. ✅ green: every check passed. ❌ red: one did not, and the clerk pins a short note saying which step gave up. The stamp lands on your commit and, if the sheet is part of a pull request, on the PR — right where a reviewer looks before merging.

One more rule: drop a second sheet before the first round is done and the clerk abandons the old round for the new sheet — no point grading homework you have already replaced. That is concurrency with cancel-in-progress. And the best way to trust a stamp is to earn a red one on purpose. So we will.

🗺️ Diagram

flowchart LR
    push["🧑‍💻 git push"]
    run["📮 one run of ci.yml<br/>one page, three jobs"]
    j["✅ checking desks<br/>node --test on 20, 22, 24"]
    green["✅ green check<br/>on the commit and the PR"]
    red["❌ red X<br/>annotation plus log line"]
    fix["🔧 fix, push again"]
    push -->|"1 on push or pull_request"| run
    run -->|"2 three fresh runners"| j
    j -->|"3 exit code 0"| green
    j -->|"4 exit code 1"| red
    red -->|"5 read the log"| fix
    fix -->|"6 newer push cancels the older run"| run

❓ What

🧠 The stamp is an exit code

command exits 0         → step passes → job passes → run is green → commit / PR gets ✅
command exits non-zero  → step fails  → job fails  → run is red   → commit / PR gets ❌

By default that is the whole contract. CI does not "know" about tests — it knows that node --test exits non-zero when one fails.

🤔 Why

A pipeline you have only read is a promise. One you have watched go green, red, and green again is a tool: you know what a failure looks like, where the message is, and how long the loop takes — before a Friday-evening hotfix. That fast, visible feedback is the point of lesson 01's mailroom. The same "test it before it travels" idea reappears when the Docker school's lesson 12 smoke-tests the built image in CI.

🔧 How (in this repo)

The bell in ci.yml — on: push: and pull_request:, quoted in lesson 02 — has no branch filter, so a push to any branch of your fork rings it, and so does a pull request (a PR from a branch inside the fork rings both — expect two runs per push). The step that decides the stamp runs server.test.js and exits non-zero on any failure:

      - name: 🧪 test
        run: >
          node --test
          --test-reporter=spec  --test-reporter-destination=stdout
          --test-reporter=junit --test-reporter-destination=test-results.xml

Reading the run page. Actions tab → ✅ ci → the list of runs. Open one: a box per job — test (node 20), test (node 22), test (node 24) — repeated in the left sidebar. Click a job for its steps; each expands to its log. On a red run the failed step is marked and the summary carries the annotation. Re-run jobs reruns the same commit; Re-run failed jobs only the red ones.

Stale runs cancel themselves:

concurrency:                # a newer push cancels the older run of the same branch
  group: ci-${{ github.ref }}
  cancel-in-progress: true

The group name includes the branch (github.ref), so only the older run of the same branch is canceled; it shows gray, not red (GitHub's API spells the status cancelled).

The other workflow. A push to main in your fork also rings ship.yml: its test and build jobs run, while push and deploy are guarded by if: vars.AWS_ROLE_ARN != '' and show as skipped — by design; a fork has no AWS variables and stays green anyway.

🔁 The same thing in CircleCI
    docker:
      - image: cimg/node:<< parameters.node >>
    working_directory: ~/repo/app
    steps:
      - checkout:
          path: ~/repo
      # … restore_cache / prepare / save_cache: lesson 04
      - 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

A Docker image is the executor, checkout is an explicit step; workflows: supplies node.

🦊 The same thing in GitLab CI
test:
  stage: test
  image: node:${NODE}-alpine
  parallel:
    matrix:                                   # lesson 04: three rulers
      - NODE: ["20", "22", "24"]
  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

No checkout step — GitLab clones first. Each script: line is a step; a non-zero exit stops the job.

🎩 The same thing in Jenkins
    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') {
                // … prepare step: lesson 04
                sh '''node --test --test-reporter=spec  --test-reporter-destination=stdout \
                                  --test-reporter=junit --test-reporter-destination=test-results.xml'''

agent { docker { … } } is the clerk, sh the step; the stage follows the shell's exit code.

🧪 Try it

# 0) in your fork (lesson 02): enable Actions once in the Actions tab, then ring the bell
latest() { gh run list --workflow ci.yml --limit 1 --json databaseId --jq '.[0].databaseId'; }
git commit --allow-empty -m "ring the bell" && git push
gh run watch "$(latest)"             # three jobs → ✅ (a minute or two; a cold runner takes longer)

# 1) earn a red stamp on purpose: /healthz answers "okay" instead of "ok"
sed -i.bak "s/end('ok/end('okay/" app/server.js && rm app/server.js.bak
git commit -am "break /healthz on purpose" && git push
gh run watch "$(latest)"             # ❌ on all three rulers
gh run view "$(latest)" --log-failed | grep -A3 healthz   # expected 'ok\n', actual 'okay\n'

# 2) fix it (the good file is one commit back), then push twice quickly: the first run is canceled
git checkout HEAD~1 -- app/server.js && git commit -am "fix /healthz" && git push
git commit --allow-empty -m "and again" && git push
gh run list --workflow ci.yml --limit 3     # "fix /healthz" canceled, "and again" ✅

# 3) a pull-request check: same workflow, second bell (-R keeps the PR inside YOUR fork)
ME=$(gh api user --jq .login)
git checkout -b pr-check && git commit --allow-empty -m "try a PR" && git push -u origin pr-check
gh pr create -R "$ME/learn-cicd-school" --base main --head pr-check --fill
gh pr checks pr-check -R "$ME/learn-cicd-school" --watch    # the checks a reviewer sees

⚠️ Common mistakes

⏭️ Next

On your very first run, three desks each ran node prepare.js from scratch. Later runs did not. The mailroom keeps a drawer of sharpened pencils 🗄️ for exactly that — if you tell it what the drawer is keyed by. ⚡

git checkout lesson-04-cache-parallel-matrix
← Previouspipeline anatomyNext →cache parallel matrix

This page is the lesson's README from the lesson-03-first-pipeline branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.