সরাসরি পর্বে যান
পর্ব ৫৮ · ৭০-এর মধ্যে · মডিউল ৯

Continuous integration — pipeline, build, test আর দ্রুত feedback

Continuous Integration — Pipelines, Builds, Tests and Fast Feedback

Taught in Bangla, this lesson defines continuous integration as a practice, not a tool, following Fowler's practices. It models a pipeline as a DAG with fail-fast and critical path, covers matrix builds, parallelism, test sharding, cache keys versus artifacts, the same pipeline in GitHub Actions and GitLab CI YAML, CI security with OIDC and SHA pinning, required checks, merge queues and CI metrics.

এই পর্বে যা শিখবেন

  • CI-এর জন্মকথা — Booch (১৯৯১), Extreme Programming আর Martin Fowler-এর লেখা (২০০০, ২০০৬) — আর তার অনুশীলনগুলো
  • একটি pipeline-এর গঠন: trigger, job, step, stage, runner; DAG ধরে চালানো, fail-fast, critical path — চালানো যায় এমন মডেলে
  • Matrix build, parallelism, test sharding, lockfile-এর hash দিয়ে cache key, আর cache বনাম artifact
  • GitHub Actions আর GitLab CI-তে একই pipeline-এর YAML; CI-এর secret, OIDC, fork-এর PR আর SHA pinning
  • Main সবুজ রাখা (required check, merge queue, আগে revert), CI-এর মেট্রিক, স্থানীয়ভাবে একই script, আর CI theatre চেনা

১ · Continuous integration কী — “integration hell” থেকে দিনে বহুবার মেলানো

পর্ব ১৭-এ (Branching strategy) হিসাব করে দেখেছেন, একটি branch যত দিন আলাদা থাকে, মেলানোর সময় conflict আর লুকানো ভুলের ঝুঁকি তত বাড়ে। বহু সপ্তাহ আলাদা কাজের পর একসাথে মেলানোর সেই যন্ত্রণার নাম integration hell. Continuous integration (CI) এর উল্টো উত্তর: মেলানো কষ্টের হলে, সেটি আরও ঘন ঘন করুন — এত ঘন যে প্রতিবারের পরিবর্তন ছোট, আর ভাঙলে কারণ খুঁজতে মিনিট লাগে, সপ্তাহ নয়।

একটি দলীয় ক্রিকেট ম্যাচের স্কোরবোর্ড ভাবুন। প্রতিটি বলের পরে স্কোরার রান যোগ করেন, আর কেউ ভুল দেখলে তখনই ধরেন — “এই বলে চার নয়, লেগ-বাই”। এবার ভাবুন, স্কোরার পুরো ইনিংস খাতায় আলাদা আলাদা টুকে রাখলেন, আর খেলা শেষে একবারে যোগ করলেন। মোট মেলেনি — কিন্তু কোন ওভারের কোন বলে ভুল, কেউ মনে করতে পারছেন না। CI হলো প্রতি বলে স্কোর হালনাগাদ: প্রতিটি ছোট পরিবর্তন সাথে সাথে মূল খাতায় ওঠে, আর একটি যন্ত্র সাথে সাথে যোগফল মিলিয়ে দেখে।

একটু ইতিহাস। “Continuous integration” কথাটি প্রায়ই Grady Booch-এর Object Oriented Design with Applications (১৯৯১) বইয়ের সাথে যুক্ত করা হয়; তবে Booch দিনে কয়েকবার মেলানোর কথা বলেননি। অনুশীলনটিকে আজকের রূপ দেয় Kent Beck-এর Extreme Programming (XP) — Extreme Programming Explained (১৯৯৯) বইয়ের বারোটি অনুশীলনের একটি: কয়েক ঘণ্টা পরপর মেলানো আর প্রতিবার সব test চালানো। এরপর Martin Fowler (Matt Foemmel-এর সাথে) ২০০০ সালে “Continuous Integration” লেখাটি প্রকাশ করেন, ২০০৬ সালে বড় করে সংশোধন করেন (লেখার সময়ের তথ্য অনুযায়ী ২০২৪ সালে আবার হালনাগাদ)। প্রথম জনপ্রিয় open-source CI server CruiseControl আসে ThoughtWorks থেকে, ২০০১ সালের দিকে; পরে Hudson (যা থেকে ২০১১ সালে Jenkins), Travis CI, GitLab CI, আর ২০১৮–১৯ সালে GitHub Actions.

CI একটি অনুশীলন, tool নয়

এই পর্বের সবচেয়ে জরুরি বাক্য এটি। CI tool — যে software প্রতিটি push-এ build আর test চালায় — CI-এর একটি অংশ মাত্র। মূল অনুশীলন মানুষের অভ্যাস: সবাই ঘন ঘন mainline-এ মেলান, আর mainline ভাঙলে সবাই থেমে আগে সেটি ঠিক করেন. একটি দল যদি প্রতিটি feature branch-এ চমৎকার pipeline চালায়, কিন্তু branch-গুলো তিন সপ্তাহ পরে main-এ মেলে, তাহলে integration হচ্ছে তিন সপ্তাহে একবার — “continuous” নয়। Jez Humble-এর একটি বহুল-উদ্ধৃত পরীক্ষায় তিনটি প্রশ্ন আছে (শব্দ উৎসভেদে একটু আলাদা):

  1. দলের প্রত্যেকে কি অন্তত দিনে একবার mainline-এ (feature branch-এ নয়) কোড মেলান?
  2. প্রতিটি commit কি স্বয়ংক্রিয় build আর test চালায়?
  3. Build ভাঙলে কি সাধারণত দশ মিনিটের মধ্যে আবার ঠিক হয়?

তিনটিতেই “হ্যাঁ” না হলে, যতই সুন্দর tool থাকুক, দলটি পুরো অর্থে CI করছে না। এই কারণেই CI trunk-based development-এর ঘনিষ্ঠ সঙ্গী (পর্ব ১৭): ছোট, স্বল্পজীবী branch বা সরাসরি trunk, আর অসম্পূর্ণ কাজ feature flag-এর আড়ালে।

মূল ধারণা

CI-এর লক্ষ্য একটি দ্রুত, বিশ্বাসযোগ্য সংকেত: “এই মুহূর্তে mainline-এ সবার কাজ একসাথে চলে কি না”। সংকেতটি যত দ্রুত আসে আর যত কম মিথ্যা বলে, দল তত সাহস নিয়ে ছোট ছোট ধাপে এগোতে পারে। বাকি সব — pipeline, cache, matrix — এই সংকেতকে দ্রুত আর নির্ভরযোগ্য করার কৌশল।

২ · Fowler-এর অনুশীলনগুলো — একটি repository থেকে “সবাই ফল দেখতে পান”

Fowler-এর ২০০৬ সংস্করণে প্রায় দশটি অনুশীলন ছিল। নিচের তালিকা সেই লেখার মূল কথাগুলোর সারাংশ — শব্দ হুবহু নয়, আর ২০২৪-এর সংশোধনে কিছু নাম ও ক্রম বদলেছে বলে জানা যায়; মূল লেখাটি নিজে পড়ে মিলিয়ে নিন।

অনুশীলন (সারাংশ)কেনআজকের সাধারণ রূপ
একটি source repositorybuild-এর যা লাগে সব এক জায়গায়, version-সহGit repository; build script, test, CI-র YAML-ও সেখানে
Build স্বয়ংক্রিয়একটি command-এ শূন্য থেকে চলার মতো systemnpm ci && npm run build, make, Gradle, Bazel
Build নিজেই test করে (self-testing build)compile হলেই চলে, এমন নয়unit ও integration test; একটি fail মানে পুরো build লাল
সবাই প্রতিদিন mainline-এ commit করেনপার্থক্য ছোট থাকে, conflict ছোট থাকেছোট PR, দিনেই merge; trunk-based development
প্রতিটি commit একটি integration যন্ত্রে mainline build করে“আমার কম্পিউটারে চলে” যথেষ্ট নয়প্রতিটি push আর PR-এ পরিষ্কার runner-এ pipeline
ভাঙা build সাথে সাথে ঠিকলাল mainline-এর ওপর সবার কাজ অনিশ্চিত“stop the line”, আগে revert (অংশ ৮)
Build দ্রুত রাখাধীর feedback মানে কম commit, বড় batchten-minute build-এর নির্দেশিকা, parallel, cache (অংশ ৪–৫)
Production-এর মতো পরিবেশে testপরিবেশের পার্থক্যে লুকানো ভুলcontainer-এ একই database-এর version, staging
সর্বশেষ চলার মতো ফল সবার হাতের কাছেtester, product owner সহজে নতুন version দেখেনpipeline-এর artifact, preview environment
সবাই দেখতে পান কী ঘটছেলাল build লুকিয়ে থাকে নাPR-এর status check, badge, chat-এ বার্তা
Deployment স্বয়ংক্রিয়হাতে করা deploy ভুল আর ভয়ের উৎসcontinuous delivery — পর্ব ৫৯

“দশ মিনিট” সংখ্যাটি নিয়ে সাবধান হোন। এটি এসেছে XP থেকে — Beck-এর বইয়ের দ্বিতীয় সংস্করণে (২০০৪) “Ten-Minute Build” আলাদা অনুশীলন — আর Fowler একে নির্দেশিকা হিসেবে উল্লেখ করেছেন। এটি কোনো মান-সংস্থার নিয়ম নয়, অভিজ্ঞতার সীমা: এর বেশি হলে মানুষ অপেক্ষা না করে অন্য কাজে যান, আর commit জমিয়ে বড় করে পাঠাতে শুরু করেন। বড় project তাই প্রায়ই ভাগ করে: দ্রুত “commit stage” প্রতিটি push-এ, ধীর suite পরে — Jez Humble ও David Farley-এর Continuous Delivery (২০১০) বইয়ের deployment pipeline, পর্ব ৫৯-এ।

“CI” আর “CI/CD” শব্দ প্রায়ই একসাথে বলা হয়, কিন্তু ধাপ আলাদা। CI: প্রতিটি পরিবর্তন মেলানো আর যাচাই। Continuous delivery: প্রতিটি সবুজ build যেকোনো সময় release করার মতো অবস্থায় রাখা। Continuous deployment: প্রতিটি সবুজ build মানুষের অনুমোদন ছাড়াই production-এ যায়। এই পর্ব প্রথমটির; বাকি দুটি পর্ব ৫৯-এ, আর deploy-এর কৌশল (blue-green, canary) System Design কোর্সের Deployment আর release পর্বে।

৩ · Pipeline-এর গঠন — trigger থেকে status check

একটি CI pipeline হলো স্বয়ংক্রিয় ধাপের সাজানো ক্রম। শব্দগুলো platform-ভেদে একটু আলাদা, কিন্তু কাঠামো প্রায় সব জায়গায় একই:

  • Trigger: কোন ঘটনায় pipeline চলবে — branch-এ push, pull request খোলা বা নতুন commit, সময়সূচি (যেমন প্রতি রাতে; nightly build), বা কেউ হাতে চালালেন।
  • Job: কাজের একক, যা একটি runner-এ নিজের আলাদা, নতুন পরিবেশে চলে। দুটি job-এর মধ্যে file আপনা-আপনি যায় না — পাঠাতে হয় artifact দিয়ে (অংশ ৫)।
  • Step: একটি job-এর ভেতরে পরপর চলা ধাপ — একটি shell command (npm test) বা অন্যের লেখা একটি তৈরি action. কোনো command শূন্য ছাড়া অন্য exit code দিলে step ব্যর্থ, আর সাধারণত job সেখানেই থামে।
  • Stage: job-গুলোর দল। একই stage-এর job সমান্তরালে চলে; আগের stage সফল হলে পরেরটি শুরু হয়। আরও সূক্ষ্ম পথ: প্রতিটি job বলে দেয় সে কোন job-এর জন্য অপেক্ষা করবে (GitHub Actions-এ needs, GitLab-এ needs) — তখন pipeline একটি DAG.
  • Runner বা agent: যে যন্ত্র job চালায় — platform-এর দেওয়া hosted runner (প্রতিটি job-এ নতুন virtual machine, কাজ শেষে মুছে ফেলা হয়) বা দলের নিজের self-hosted runner.
  • Status reporting: শেষে ফল ফেরত যায় commit বা PR-এ, status check হিসেবে — সবুজ টিক, লাল ক্রস, বা হলুদ “চলছে”। পর্ব ১৬-এ (Pull request আর fork) দেখেছেন, branch protection চাইলে নির্দিষ্ট check সবুজ না হলে merge বন্ধ থাকে।

একটি সাধারণ pipeline-এ ধাপগুলোর ক্রম মোটামুটি এমন — সস্তা আর দ্রুত check আগে, যাতে সবচেয়ে সাধারণ ভুল সবচেয়ে আগে ধরা পড়ে:

একটি CI pipeline — তিনটি stage, সমান্তরাল job আর artifact-এর প্রবাহ

Solid arrow = the next stage starts only if every job above passed · dashed = artifact handed on

Pipeline-এর তিনটি stage, সমান্তরাল job আর artifact Trigger: push / PR ধাপ ১ · Build ধাপ ২ · Test (matrix) ধাপ ৩ · Verify lint build unit · ubuntu unit · windows unit · macos integration security scan artifact: dist/ ✔ status check সবুজ সব সফল হলে
ছবি ৫৮.১ — একটি stage-এর সব job সফল হলে তবেই পরের stage শুরু হয় (গাঢ় তীর)। Job-গুলো আলাদা যন্ত্রে চলে, তাই build-এর তৈরি dist/ নিজে থেকে test job-এ পৌঁছায় না — সেটি artifact হিসেবে আপলোড আর পরের job-এ ডাউনলোড হয় (ভাঙা কমলা তীর)। ধাপ ২-এর তিনটি job একটি matrix: একই test, তিনটি operating system-এ।

DAG ধরে চালানো আর fail-fast — একটি ছোট pipeline runner

Platform-গুলো ভেতরে যা করে, তার একটি খেলনা মডেল নিচে। প্রতিটি stage-এর needs তালিকা বলে সে কার জন্য অপেক্ষা করবে; runner প্রথমে একটি topological order বানায় (প্রতিটি stage তার সব needs-এর পরে), তারপর চালায়। কোনো need সফল না হলে stage-টি চলেই না — fail fast. সময় আর ফল (pass বা fail) দেওয়া মান; আসলে কিছু চালানো হচ্ছে না। আর ধরা হয়েছে, যথেষ্ট runner আছে, তাই যার needs শেষ, সে সাথে সাথে শুরু করে:

// Tiny CI pipeline runner. Stages form a DAG through "needs".
// Durations (seconds) and pass/fail are STATED inputs; no real work runs.
const STAGES = [
  { name: "checkout",    needs: [],                                   sec: 5,   ok: true  },
  { name: "install",     needs: ["checkout"],                         sec: 40,  ok: true  },
  { name: "lint",        needs: ["install"],                          sec: 20,  ok: true  },
  { name: "build",       needs: ["install"],                          sec: 60,  ok: true  },
  { name: "unit",        needs: ["build"],                            sec: 90,  ok: false },
  { name: "security",    needs: ["install"],                          sec: 45,  ok: true  },
  { name: "integration", needs: ["unit"],                             sec: 180, ok: true  },
  { name: "artifact",    needs: ["lint", "integration", "security"],  sec: 15,  ok: true  },
];
function runPipeline(stages) {
  const by = Object.fromEntries(stages.map(s => [s.name, s]));
  const order = [], seen = new Set();
  const visit = n => { if (seen.has(n)) return; seen.add(n); by[n].needs.forEach(visit); order.push(n); };
  stages.forEach(s => visit(s.name));                 // topological order (DFS)
  const state = {}, end = {};
  for (const n of order) {
    const s = by[n], blocked = s.needs.filter(d => state[d] !== "passed");
    if (blocked.length) {                              // fail-fast: dependents never start
      state[n] = "skipped"; console.log(`SKIP ${n.padEnd(12)} needs ${blocked.join(", ")}`); continue;
    }
    const start = Math.max(0, ...s.needs.map(d => end[d]));  // parallel: start when needs finish
    end[n] = start + s.sec; state[n] = s.ok ? "passed" : "failed";
    console.log(`${s.ok ? "PASS" : "FAIL"} ${n.padEnd(12)} ${start}s -> ${end[n]}s`);
  }
  const pick = st => order.filter(n => state[n] === st);
  const ran = order.filter(n => n in end);
  console.log(`wall clock ${Math.max(...ran.map(n => end[n]))}s, serial would be ${ran.reduce((a, n) => a + by[n].sec, 0)}s`);
  console.log(`status: ${pick("failed").length ? "FAILED" : "PASSED"}` +
    ` | failed: ${pick("failed").join(", ") || "-"} | skipped: ${pick("skipped").join(", ") || "-"}`);
}
runPipeline(STAGES);

আউটপুট:

PASS checkout     0s -> 5s
PASS install      5s -> 45s
PASS lint         45s -> 65s
PASS build        45s -> 105s
FAIL unit         105s -> 195s
PASS security     45s -> 90s
SKIP integration  needs unit
SKIP artifact     needs integration
wall clock 195s, serial would be 260s
status: FAILED | failed: unit | skipped: integration, artifact

lint, build আর security শুধু install-এর ওপর নির্ভর করে, তাই তিনটিই ৪৫ সেকেন্ডে একসাথে শুরু। unit ব্যর্থ, তাই তার ওপর নির্ভরশীল integration চলেনি, আর তার ওপর নির্ভরশীল artifact-ও না — ব্যর্থতা DAG বেয়ে নিচে ছড়ায়। কিন্তু security unit-এর ওপর নির্ভর করে না, তাই নিজের মতো শেষ করেছে — বাস্তব platform-এর সাধারণ আচরণও এটি। ব্যর্থতার খবর এসেছে ১৯৫ সেকেন্ডে, integration-এর ১৮০ সেকেন্ডের অপেক্ষা ছাড়াই। যে job চলেছে, এক যন্ত্রে পরপর চালালে লাগত ৫ + ৪০ + ২০ + ৬০ + ৯০ + ৪৫ = ২৬০ সেকেন্ড; সমান্তরালে ১৯৫ — দীর্ঘতম পথ checkout → install → build → unit (৫ + ৪০ + ৬০ + ৯০)। এই পথের নাম critical path: এর বাইরের job (lint, security) যত দ্রুতই হোক, মোট সময় কমে না।

এবার একটি সত্যিকারের PR-এর জীবন, push থেকে সবুজ check পর্যন্ত। কাল্পনিক অনলাইন বাজার অ্যাপ bazar-cart-এ নাদিয়া coupon-এর একটি পরিবর্তন পাঠালেন। সময়গুলো উদাহরণের জন্য ধরা:

  1. PushPR #42
  2. Queuerunner খোঁজা
  3. Buildlint ‖ build
  4. Test৩টি OS
  5. Verifyintegration ‖ scan
  6. Checkসবুজ
PR #42 · feature/coupon → main
ঘড়ি (মিনিট:সেকেন্ড)
কী চলছে
Cache / artifact
PR-এর check
Merge বোতাম
  1. Push. নাদিয়া feature/coupon branch-এ নতুন commit push করলেন; PR #42 আগে থেকেই খোলা। এখনো কোনো check নেই।
  2. Trigger. Platform “PR-এ নতুন commit” ঘটনাটি দেখে pipeline #318 তৈরি করল — ৭টি job: lint, build, তিনটি unit, integration আর security. PR-এ হলুদ বিন্দু: pending.
  3. Queue আর runner. ৪০ সেকেন্ড অপেক্ষার পর একটি নতুন ubuntu virtual machine job হাতে নিল। এই অপেক্ষাই queue time — runner কম থাকলে ব্যস্ত দুপুরে এটি মিনিটে গড়ায়।
  4. Setup. Checkout, Node 24 বসানো, তারপর dependency. Lockfile বদলায়নি, তাই cache key হুবহু মিলেছে — install ১২ সেকেন্ডে (cache না থাকলে ধরুন ৭০ সেকেন্ড)। ঘড়ি ০১:১০।
  5. Lint ‖ build. দুটি job দুটি runner-এ একসাথে। Lint শেষ আগে, build ০২:১০-এ; build-এর তৈরি dist/ artifact হিসেবে আপলোড। ২/৭ সবুজ।
  6. Matrix test. তিনটি unit job তিনটি operating system-এ একসাথে, প্রত্যেকে dist/ ডাউনলোড করে। Windows-এর path বা line ending-এর ভুল এখানে ধরা পড়ত। ০৪:১০-এ ৫/৭ সবুজ।
  7. Verify. Integration test একটি database container-এর সাথে (আড়াই মিনিট) আর dependency-র নিরাপত্তা scan (এক মিনিট) একসাথে। Test report-ও artifact. ০৬:৪০-এ ৭/৭.
  8. সবুজ check. সব required check সবুজ — সাত মিনিটের নিচে, দশ মিনিটের নির্দেশিকার মধ্যে। Merge-এর জন্য এখন শুধু reviewer-এর approval বাকি; দলে merge queue থাকলে PR সেখানে সারিতে দাঁড়াবে (অংশ ৮)।

৪ · Matrix, parallelism আর test sharding

একটি library অনেক জায়গায় চলবে — Linux, Windows, macOS; Node-এর তিনটি version. প্রতিটির জন্য আলাদা job হাতে লেখা ভুলপ্রবণ। Matrix build একবার job লিখে বলে দেয় কোন মাত্রার সব জোড়ায় চালাতে হবে: ৩টি OS × ৩টি version = ৯টি job. দুটি নিয়মে ছাঁটা যায়:

  • exclude: কিছু জোড়া বাদ — যেমন পুরোনো version-টি macOS-এ পরীক্ষা অপ্রয়োজনীয় বা দামি।
  • include: GitHub Actions-এর documentation অনুযায়ী (লেখার সময়ে) include বস্তু matrix-এর নিজের key-তে কোনো বিদ্যমান জোড়ার সাথে মিললে সেই job-এ বাড়তি key যোগ হয়, না মিললে নতুন job; exclude আগে, include পরে। আসল নিয়মের আরও সূক্ষ্ম অবস্থা আছে; নিচের মডেল সরলীকৃত।

Matrix job-গুলো কত দ্রুত শেষ হবে, তা নির্ভর করে কয়টি runner একসাথে পাওয়া যায় তার ওপর। প্রতিটি job-এর সময় নিচে দেওয়া — মাপা নয়। Scheduler সারির ক্রমে job নেয়, আর যে runner আগে খালি হয়, তাকে দেয়:

// Matrix expansion (simplified GitHub-Actions-style rules), then scheduling on N runners.
const matrix  = { os: ["ubuntu", "windows", "macos"], node: [22, 24, 26] };
const exclude = [{ os: "macos", node: 22 }];
const include = [{ os: "ubuntu", node: 24, coverage: true },   // matches a job: adds a key
                 { os: "ubuntu-arm", node: 24 }];              // matches none: new job
const MINUTES = { ubuntu: 4, "ubuntu-arm": 5, windows: 7, macos: 6 };   // STATED job times
const minutes = j => MINUTES[j.os] + (j.coverage ? 2 : 0);     // coverage costs 2 more

function expand(m, exc, inc) {
  let jobs = [{}];                                              // cartesian product
  for (const [k, vals] of Object.entries(m)) jobs = jobs.flatMap(j => vals.map(v => ({ ...j, [k]: v })));
  const fits = (j, r) => Object.keys(r).every(k => j[k] === r[k]);
  jobs = jobs.filter(j => !exc.some(e => fits(j, e)));          // exclude first
  for (const add of inc) {
    const keys = Object.keys(add).filter(k => k in m);          // only the matrix's own keys
    const hit = jobs.filter(j => keys.every(k => j[k] === add[k]));
    if (hit.length) hit.forEach(j => Object.assign(j, add)); else jobs.push({ ...add });
  }
  return jobs;
}
function schedule(jobs, runners) {                              // queue order, earliest-free runner
  const free = Array(runners).fill(0);
  for (const j of jobs) { const r = free.indexOf(Math.min(...free)); free[r] += minutes(j); }
  return Math.max(...free);
}
const jobs = expand(matrix, exclude, include);
jobs.forEach((j, i) => console.log(`job ${i + 1}: ${j.os}/node${j.node}${j.coverage ? " +coverage" : ""} ${minutes(j)} min`));
const total = jobs.reduce((a, j) => a + minutes(j), 0);
console.log(`${jobs.length} jobs, ${total} runner-minutes`);
for (const n of [1, 2, 4, jobs.length]) console.log(`${String(n).padStart(2)} runners -> wall clock ${schedule(jobs, n)} min`);

আউটপুট:

job 1: ubuntu/node22 4 min
job 2: ubuntu/node24 +coverage 6 min
job 3: ubuntu/node26 4 min
job 4: windows/node22 7 min
job 5: windows/node24 7 min
job 6: windows/node26 7 min
job 7: macos/node24 6 min
job 8: macos/node26 6 min
job 9: ubuntu-arm/node24 5 min
9 jobs, 52 runner-minutes
 1 runners -> wall clock 52 min
 2 runners -> wall clock 26 min
 4 runners -> wall clock 16 min
 9 runners -> wall clock 7 min

৩ × ৩ = ৯টি জোড়া থেকে exclude একটি বাদ দিল (macOS/Node 22) — ৮টি। প্রথম include-এর os আর node একটি বিদ্যমান job-এর সাথে মেলে, তাই নতুন job নয়, শুধু সেই job-এ coverage যোগ হলো (৪ + ২ = ৬ মিনিট)। দ্বিতীয় include-এর ubuntu-arm কোনো জোড়ায় নেই, তাই নতুন job ৯। মোট কাজ ৫২ runner-মিনিট — এটি খরচ, runner যতই থাকুক। দেয়াল-ঘড়ির সময় runner-এর সংখ্যায় নামে, কিন্তু একটি সীমা পর্যন্ত: ৯টি runner-এও ৭ মিনিট, কারণ দীর্ঘতম একক job (Windows) ৭ মিনিটের। আর ৪টি runner-এ ১৬ মিনিট, যদিও ৫২ ÷ ৪ = ১৩ — সারির ক্রমে নেওয়ায় শেষে একটি runner একা ৫ মিনিটের job টানছে। ভালো সাজানোয় ১৩ সম্ভব কি না, অনুশীলন ১-এ দেখবেন।

os × node · ৪টি runner (R1–R4) · মিনিট
ধাপ
Job-এর সংখ্যা
বদল
R1
R2
R3
R4
শেষ
  1. Matrix লেখা। তিনটি OS আর Node-এর তিনটি version — সব জোড়া মিলে ৩ × ৩ = ৯টি job, একটি মাত্র job-এর বিবরণ থেকে।
  2. Exclude. macOS-এ Node 22 বাদ: ৮টি। Exclude সবসময় include-এর আগে প্রয়োগ হয়।
  3. Include ১. {os: ubuntu, node: 24, coverage: true} — os আর node একটি বিদ্যমান job-এর সাথে মেলে, তাই job বাড়েনি; সেই job-এ coverage চালু, সময় ৬ মিনিট।
  4. Include ২. {os: ubuntu-arm, node: 24} কোনো জোড়ার সাথে মেলে না — নতুন job ৯, ৫ মিনিট। মোট ৫২ runner-মিনিট।
  5. ৪টি runner-এ সাজানো। সারির ক্রমে: R1 পায় j1, তারপর ৪ মিনিটে j5, ১১ মিনিটে j9 — শেষ ১৬-তে। বাকিরা ১১–১৩-তে খালি। দেয়াল-ঘড়ি ১৬ মিনিট।
  6. Fail-fast. ধরুন windows/Node 24 (j5) ৯ মিনিটে লাল হলো। GitHub Actions-এ strategy.fail-fast-এর ডিফল্ট true (লেখার সময়ে): matrix-এর চলমান আর অপেক্ষমাণ job বাতিল — j6, j7, j8 থামল, j9 শুরুই হলো না। খবর এলো ৯ মিনিটে, ১৬-তে নয়; কিন্তু অন্য OS-এও একই ভুল কি না, তা অজানা থাকল — তাই অনেকে PR-এ fail-fast রাখেন, আর রাতের পূর্ণ run-এ বন্ধ করেন।

Parallelism আর test sharding

Matrix একই test-কে ভিন্ন পরিবেশে চালায়। Test sharding উল্টো: একই পরিবেশে, একটি বড় suite-কে কয়েকটি ভাগে ভেঙে কয়েকটি runner-এ। ১২ মিনিটের suite চারটি shard-এ ভাঙলে আদর্শে ৩ মিনিট — কিন্তু তিনটি শর্তে:

  • প্রতিটি shard নিজের setup-এর খরচ দেয়: setup ২ মিনিট হলে চার shard-এর প্রতিটি ৫ মিনিট, মোট ২০ runner-মিনিট (একটিতে ছিল ১৪)। Cache এখানে সবচেয়ে কাজের (অংশ ৫)।
  • ভাগ সময়ে সমান হতে হবে, test-এর সংখ্যায় নয়; অনেক tool আগের run-এর সময় দেখে ভাগ করে।
  • Test-গুলো স্বাধীন হতে হবে: পর্ব ৫১-এর (Test pyramid আর integration test) hermetic test-এর শর্ত; suite-এর গতি আর flaky test সেখানে বিস্তারিত।

৫ · Cache আর artifact — একই রকম দেখতে, কাজ আলাদা

Hosted runner প্রতিটি job-এ শূন্য থেকে শুরু করে। তাই দুটি প্রশ্ন বারবার আসে: “গতবার যে dependency নামিয়েছিলাম, আবার নামাব কেন?” আর “build job-এর তৈরি file test job পাবে কীভাবে?” প্রথমটির উত্তর cache, দ্বিতীয়টির artifact.

বিষয়CacheArtifact
উদ্দেশ্যগতি: আগের কাজের ফল আবার ব্যবহারফল হস্তান্তর: এক job থেকে আরেক job-এ, বা মানুষের হাতে
উদাহরণ~/.npm, pip-এর download, compiler-এর build cachedist/, test report, coverage report, binary
না থাকলেpipeline ধীর, কিন্তু ফল একই হওয়া উচিতপরের job চলতেই পারে না
চেনার উপায়key — সাধারণত OS + lockfile-এর hashনাম, আর কোন pipeline run-এর
নির্ভরযোগ্যতা“থাকতেও পারে” — মুছে যেতে পারে, ধরে নেওয়া যাবে নাসেই run-এর জন্য নিশ্চিত, নির্দিষ্ট মেয়াদ পর্যন্ত

সবচেয়ে গুরুত্বপূর্ণ সারি শেষটি: cache ছাড়াও pipeline-কে সঠিক ফল দিতে হবে. Cache শুধু গতি দেয়। তাই cache-এর key এমন হতে হবে, যা dependency বদলালেই বদলায় — নইলে পুরোনো dependency দিয়ে test pass করবে, অথচ নতুন যন্ত্রে ভাঙবে। সবচেয়ে প্রচলিত কৌশল: lockfile-এর হুবহু byte-এর hash. GitHub Actions-এর hashFiles('**/package-lock.json') বা setup-node-এর cache: npm এই কাজই করে; GitLab-এ cache:key:files.

নিচের Python প্রোগ্রাম চারটি run-এ cache key হিসাব করে, আর একটি নকল cache store-এ খোঁজে। Key হুবহু না মিললে “restore keys”-এর মতো একটি prefix দিয়ে সবচেয়ে নতুন কাছাকাছি cache নেয় — partial hit; তারপর package manager শুধু বদলানো অংশ নামায়:

# Dependency-cache key from a lockfile's exact bytes (like hashFiles() in CI).
import hashlib

def cache_key(os_name, lock_text):
    digest = hashlib.sha256(lock_text.encode("utf-8")).hexdigest()[:12]
    return f"{os_name}-pip-{digest}"

def restore(store, key, prefix):
    if key in store:
        return "exact hit"
    partial = [k for k in store if k.startswith(prefix)]   # like restore-keys
    return f"partial hit ({partial[-1]})" if partial else "miss"

lock_v1 = "requests==2.32.3\nurllib3==2.2.3\n"
runs = [
    ("1 first run",              lock_v1),
    ("2 only app code changed",  lock_v1),                     # lockfile identical
    ("3 a comment added",        lock_v1 + "# pinned on 2026-01-15\n"),
    ("4 urllib3 upgraded",       lock_v1.replace("2.2.3", "2.3.0")),
]
store = []                                   # saved cache keys, oldest first
for label, lock in runs:
    key = cache_key("linux", lock)
    result = restore(store, key, "linux-pip-")
    print(f"{label:25} {key}  {result}")
    if result != "exact hit":
        store.append(key)                    # job ends: save under the new key
print("keys saved:", len(store))

আউটপুট:

1 first run               linux-pip-91146d24db58  miss
2 only app code changed   linux-pip-91146d24db58  exact hit
3 a comment added         linux-pip-67521875981f  partial hit (linux-pip-91146d24db58)
4 urllib3 upgraded        linux-pip-e6c8af766b30  partial hit (linux-pip-67521875981f)
keys saved: 3

Run ১-এ store খালি — miss; job-এর শেষে নতুন key-তে cache জমা। Run ২-এ শুধু app-এর কোড বদলেছে, lockfile হুবহু একই, তাই key একই — exact hit, dependency নামাতে হয়নি। Run ৩ শিক্ষণীয়: dependency একটিও বদলায়নি, শুধু একটি comment যোগ হয়েছে, তবু key বদলেছে — hash হুবহু byte দেখে, অর্থ নয়। ক্ষতি সামান্য (একবার নতুন cache জমা), আর উল্টো ভুলের চেয়ে নিরাপদ। Run ৪-এ সত্যিকারের বদল (urllib3-এর version), নতুন key; prefix দিয়ে আগের cache পাওয়া গেল, package manager শুধু নতুন version নামাবে। Partial hit-এ পুরোনো package থেকে যেতে পারে, তাই install-এর command (যেমন npm ci) lockfile ধরে ঠিক অবস্থা বানাক। SHA-256-এর প্রথম ১২টি hex অক্ষর নেওয়া শুধু ছোট দেখাতে; আসল platform-এর key-এর গঠন আলাদা।

Cache আর নিরাপত্তা: একটি cache যদি অবিশ্বস্ত job-ও লিখতে পারে (যেমন fork-এর PR), তাহলে কেউ cache-এ বিষাক্ত package রেখে পরের বিশ্বস্ত job-কে দিয়ে চালাতে পারে — cache poisoning. Platform-গুলো সাধারণত branch অনুযায়ী cache-এর অধিকার আলাদা রাখে (লেখার সময়ে; platform-ভেদে আলাদা)। Release-এর build-এ cache-এর ওপর ভরসা না করা নিরাপদ অভ্যাস।

৬ · Pipeline লেখা — GitHub Actions আর GitLab CI-এর YAML

প্রায় সব আধুনিক CI platform pipeline-এর বিবরণ একটি YAML file-এ রাখে, আর file-টি থাকে repository-র ভেতরেই — “pipeline as code”. সুবিধা বড়: pipeline-এর প্রতিটি বদল একটি commit, review হয় PR-এ, আর পুরোনো commit-এ ফিরলে তখনকার pipeline-ও ফেরে। YAML-এ গঠন বোঝায় indentation (space, tab নয়); key: value হলো map, - দিয়ে শুরু লাইন হলো list-এর উপাদান।

নিচে কাল্পনিক bazar-cart-এর একটি workflow, GitHub Actions-এর জন্য। এটি চালানোর উদাহরণ নয় — এখানে চলবে না — কিন্তু লেখার সময়ের documentation অনুযায়ী বৈধ গঠন। Action-এর version (@v7, @v8) অক্টোবর ২০২৬-এ ওই repository-গুলোর সর্বশেষ major tag দেখে লেখা; আপনি ব্যবহারের আগে নিজে মিলিয়ে নিন।

# .github/workflows/ci.yml  (bazar-cart, a fictional Node.js shop app)
name: ci
on:
  pull_request:
  push:
    branches: [main]
  schedule:
    - cron: "0 20 * * *"          # 20:00 UTC = 02:00 in Dhaka
permissions:
  contents: read                  # least privilege for GITHUB_TOKEN
concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}  # PRs only

jobs:
  build:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v7
        with:
          persist-credentials: false
      - uses: actions/setup-node@v7
        with:
          node-version: 24
          cache: npm              # key = hash of package-lock.json
      - run: npm ci
      - run: npm run lint
      - run: npm run build
      - uses: actions/upload-artifact@v7
        with:
          name: dist
          path: dist/

  test:
    needs: build                  # waits for build; skipped if it fails
    strategy:
      fail-fast: true
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node: [22, 24, 26]
        exclude:
          - os: macos-latest
            node: 22
    runs-on: ${{ matrix.os }}
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v7
        with:
          persist-credentials: false
      - uses: actions/setup-node@v7
        with:
          node-version: ${{ matrix.node }}
          cache: npm
      - run: npm ci
      - uses: actions/download-artifact@v8
        with:
          name: dist
          path: dist/
      - run: npm test

লাইন ধরে পড়ুন:

  • on — trigger: যেকোনো PR, main-এ push, আর রাতের schedule (cron-এর সময় UTC-তে, তাই ঢাকার রাত ২টা = 20:00).
  • permissions — স্বয়ংক্রিয় token শুধু পড়তে পারে (least privilege, অংশ ৭)। concurrency — একই PR-এ নতুন push এলে পুরোনো run বাতিল; main-এ নয়, যাতে প্রতিটি commit-এর পূর্ণ ফল থাকে।
  • jobs-এ দুটি job; runs-on runner-এর ধরন; প্রতিটি step হয় uses (তৈরি action, owner/repo@version) নয়তো run (shell command).
  • actions/checkout কোড আনে (persist-credentials: false token disk-এ রাখে না); actions/setup-node-এর cache: npm lockfile-এর hash ধরে cache ফেরায়।
  • needs: build — DAG-এর তীর; strategy.matrix — ৩ × ৩ − ১ = ৮টি test job, প্রতিটিতে ${{ matrix.os }}-এর নিজের মান।
  • Artifact: build আপলোড করে, test ডাউনলোড করে — দুটি আলাদা যন্ত্র। লেখার সময়ে upload ও download action-এর major version আলাদা (v7, v8); documentation মিলিয়ে নিন। timeout-minutes আটকে যাওয়া job থামায়।

একই pipeline GitLab CI/CD-তে। শব্দ আলাদা, ধারণা একই: stages ক্রম ঠিক করে, প্রতিটি job-এ stage আর script, পরিবেশ আসে container image থেকে, আর কখন চলবে তা ঠিক করে rules. এটিও চালানোর জন্য নয়, তবে লেখার সময়ের GitLab documentation অনুযায়ী বৈধ গঠন:

# .gitlab-ci.yml  (the same fictional pipeline on GitLab CI/CD)
stages: [build, test, nightly]

workflow:
  rules:                                   # when does a pipeline exist at all?
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    - if: $CI_PIPELINE_SOURCE == "schedule"

default:
  image: node:24
  cache:
    key:
      files: [package-lock.json]           # new key only when the lockfile changes
    paths: [.npm/]

build:
  stage: build
  script:
    - npm ci --cache .npm --prefer-offline
    - npm run lint
    - npm run build
  artifacts:
    paths: [dist/]
    expire_in: 1 week

test:
  stage: test
  image: node:$NODE
  parallel:
    matrix:
      - NODE: ["22", "24", "26"]
  script:
    - npm ci --cache .npm --prefer-offline
    - npm test                             # dist/ arrives from build's artifacts

e2e:
  stage: nightly
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule"   # slow suite: only on the nightly run
  script:
    - npm ci --cache .npm --prefer-offline
    - npm run test:e2e

পার্থক্যগুলো খেয়াল করুন। GitLab-এ workflow:rules ঠিক করে pipeline আদৌ তৈরি হবে কি না — merge request, ডিফল্ট branch আর schedule-এ। default সব job-এ একই image আর cache দেয়; cache:key:files-এ key বদলায় শুধু lockfile বদলালে (documentation অনুযায়ী key-টি ওই file-এর শেষ পরিবর্তনের commit থেকে হিসাব হয়)। আগের stage-এর artifact পরের stage-এর job নিজে থেকেই পায় — needs লিখলে stage-এর অপেক্ষা ছাড়াই DAG. parallel:matrix তিনটি job বানায়, প্রত্যেকে আলাদা NODE নিয়ে। e2e-এর নিজের rules একে শুধু রাতের run-এ রাখে — ধীর suite PR আটকায় না, তবু প্রতিদিন চলে। Version নম্বর উদ্ধৃতিচিহ্নে লেখা ভালো অভ্যাস: YAML-এ 3.10 লিখলে অনেক parser একে সংখ্যা 3.1 পড়ে।

Jenkins (Jenkinsfile), Azure Pipelines, CircleCI — সবখানেই একই ধারণা: trigger, job বা stage, step, runner, cache, artifact. আসল কাজ রাখুন repository-র script-এ (npm test, ci/check.sh), আর YAML-এ শুধু “কখন, কোথায়, কোন ক্রমে” — অংশ ৮-এ দেখবেন কেন।

৭ · CI-এর নিরাপত্তা — secret, OIDC, fork-এর PR আর SHA pinning

CI runner এমন একটি যন্ত্র, যেখানে অন্যের লেখা কোড (dependency, action, PR-এর কোড) চলে আর প্রায়ই deploy-এর key থাকে। আক্রমণকারীর কাছে এটি সোনার খনি। পর্ব ৫৭-এ (Git-এ নিরাপত্তা) repository-তে secret ফাঁস ঠেকানো দেখেছেন; এখানে CI-এর দিকটি।

Secret রাখার নিয়ম

  • YAML-এ নয়, platform-এর secret store-এ: GitHub-এর repository বা environment secret, GitLab-এর CI/CD variable. Job চলার সময় environment variable হিসেবে আসে।
  • Masked: masked variable-এর মান log-এ ছাপা হলে platform তা *** দিয়ে ঢেকে দেয়। কিন্তু এটি শুধু হুবহু মান খোঁজে — base64 করে, ভেঙে বা উল্টে ছাপালে ঢাকা না-ও পড়তে পারে। তাই নিয়ম: secret কখনো echo নয়, debug-এর জন্যও নয়।
  • সীমিত পরিসর: GitLab-এর “protected” variable শুধু protected branch আর tag-এর pipeline পায়; GitHub-এর environment secret পাওয়া যায় নির্দিষ্ট environment-এর job-এ, চাইলে মানুষের অনুমোদনের পরে (platform-ভেদে আলাদা)। Deploy-এর key test job-এর হাতে থাকার দরকার নেই।
  • ন্যূনতম অনুমতি: workflow-এর স্বয়ংক্রিয় token-এর অনুমতি শুরুতে contents: read; যে job-এর লেখার দরকার, শুধু সেখানে বাড়ানো।

দীর্ঘমেয়াদি key-এর বদলে OIDC

পুরোনো পথ: cloud-এ একটি স্থায়ী access key বানিয়ে CI-এর secret-এ রাখা। ফাঁস হলে, কেউ খেয়াল না করা পর্যন্ত — মাসের পর মাস — সেটি কাজ করে। আধুনিক পথ OIDC: প্রতিটি job platform থেকে একটি স্বল্পমেয়াদি, স্বাক্ষরিত পরিচয়পত্র (token) চায়, যাতে লেখা থাকে “এটি example/bazar-cart repository-র main branch-এর একটি job”. Cloud আগে থেকে ঠিক করা নিয়মে এটি যাচাই করে কয়েক মিনিট বা ঘণ্টার জন্য অনুমতি দেয়। GitHub Actions-এ লেখার সময়ে এর জন্য job-কে id-token: write অনুমতি দিতে হয়; বড় cloud provider-গুলো এটি সমর্থন করে (বিস্তারিত platform আর provider-ভেদে আলাদা)।

  deploy:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production          # its own secrets and approval rules
    permissions:
      contents: read
      id-token: write                # may request a short-lived OIDC token
    steps:
      - run: echo "exchange the OIDC token for cloud credentials here"

Fork থেকে আসা PR

Open-source project-এ যে কেউ fork থেকে PR পাঠাতে পারেন, আর সেই PR-এর কোড আপনার CI-তে চলে। তাই GitHub Actions-এ লেখার সময়ের নিয়মে fork-এর PR-এর সাধারণ pull_request event-এ secret দেওয়া হয় না, আর স্বয়ংক্রিয় token শুধু পড়তে পারে; নতুন বাইরের অবদানকারীর run-এর আগে maintainer-এর অনুমোদনও লাগতে পারে। বিপদ pull_request_target-এ: এটি মূল repository-র প্রেক্ষাপটে secret-সহ চলে (লেবেল বসানোর মতো কাজের জন্য)। এমন workflow-এ PR-এর কোড checkout করে চালালে বাইরের কোড secret-এর নাগাল পায় — GitHub Security Lab একে “pwn request” বলেছে। অন্য platform-এর নিয়ম আলাদা; documentation দেখুন।

তৃতীয় পক্ষের action — tag নয়, commit SHA

uses: someone/tool@v3 লিখলে আপনি বলছেন “v3 tag এখন যে commit-এ, সেটি চালান”। কিন্তু Git-এ tag সরানো যায় (পর্ব ১৮)। কেউ সেই repository-র নিয়ন্ত্রণ পেলে v3-কে একটি ক্ষতিকর commit-এ সরিয়ে দিতে পারেন, আর পরের run থেকে হাজারো pipeline সেটি চালাবে — একটি supply-chain attack. ২০২৫ সালের মার্চে একটি জনপ্রিয় তৃতীয় পক্ষের action (tj-actions/changed-files)-এর ক্ষেত্রে প্রকাশিত বিবরণ অনুযায়ী ঠিক এমনটি ঘটে: tag-গুলো এমন commit-এ সরানো হয়, যা runner-এর secret log-এ ছাপিয়ে দিত। প্রতিরক্ষা SHA pinning: পুরো ৪০ অক্ষরের commit hash দিয়ে pin, পাশে comment-এ মানুষের পড়ার মতো version. Commit hash কনটেন্ট থেকে তৈরি (পর্ব ৫২, Git-এর ভেতরে), তাই নিঃশব্দে বদলানো যায় না।

      # mutable: whatever commit the tag points to today
      - uses: actions/checkout@v7
      # immutable: exactly this commit (v7.0.1, checked with git ls-remote, Oct 2026)
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # v7.0.1

SHA pin করলে হালনাগাদ নিজে থেকে আসে না — Dependabot বা Renovate-এর মতো tool নতুন version-এর PR খোলে, আর সেটি review হয়। Action-এর নিজের ভেতরের dependency অবশ্য SHA pin-এ ঢাকা পড়ে না। ২০২১ সালের Codecov ঘটনায় (প্রকাশিত বিবরণ অনুযায়ী) একটি জনপ্রিয় upload script বদলে বহু গ্রাহকের CI থেকে environment variable বাইরে পাঠানো হয়েছিল — শিক্ষা একই: CI-তে যা নামিয়ে চালান তা যাচাই করুন, আর secret রাখুন যতটা কম সম্ভব।

৮ · Main সবুজ রাখা — required check, merge queue আর আগে revert

লাল mainline-এর খরচ সবার: যিনি এইমাত্র main থেকে branch নিলেন, তিনি ভাঙা কোডে শুরু করলেন; যাঁর PR এখন test হচ্ছে, তিনি বুঝবেন না দোষ কার; আর লাল যত দীর্ঘ, তার ওপর তত পরিবর্তন জমে। তাই CI-এর প্রথম নিয়ম: broken build ঠিক করা দলের প্রথম অগ্রাধিকার। তিন স্তরের প্রতিরক্ষা:

  1. Required status check: branch protection-এ নির্দিষ্ট check-কে বাধ্যতামূলক — সবুজ না হলে PR merge হয় না, আর main-এ সরাসরি push বন্ধ (পর্ব ১৬)। খেয়াল রাখুন check-এর নাম বাধ্যতামূলক করা হয়; YAML-এ job-এর নাম বদলালে পুরোনো নামের check চিরকাল “অপেক্ষমাণ” থাকতে পারে।
  2. Merge queue: PR সবুজ, main সবুজ — তবু merge-এর পরে লাল হতে পারে: একজন function-এর নাম বদলালেন, অন্যজন পুরোনো নামে নতুন ডাক যোগ করলেন — Git-এর কোনো conflict নেই, অথচ একসাথে ভাঙা। Merge queue প্রতিটি PR-কে সারির আগেরগুলোর সাথে মিলিয়ে তখনকার main-এর ওপর আবার test করে, তারপর merge করে। পরিচয় পর্ব ১৬-এ; queue কীভাবে working tree ছাড়াই merge-এর ফল হিসাব করে (git merge-tree), তা পর্ব ৫৪-এ (Merge-এর ভেতরে)।
  3. লাল হলে আগে revert: চাপের মধ্যে তাড়াহুড়োর fix প্রায়ই নতুন ভুল আনে। প্রচলিত নিয়ম: কয়েক মিনিটে (ধরুন দশ) নিশ্চিত fix না হলে ভাঙা commit-টি git revert করে main সবুজ করুন, তারপর branch-এ শান্ত মাথায় ঠিক করুন। Revert ইতিহাস মোছে না — একটি নতুন উল্টো commit (পর্ব ১১)।

লাল main-এর সাথে flaky test-এর লাল গুলিয়ে ফেলবেন না: একই commit-এ আবার চালালে সবুজ হলে সমস্যাটি test-এর, commit-এর নয় — revert করে লাভ নেই। Flaky test শনাক্ত, quarantine আর ঠিক করার পদ্ধতি পর্ব ৫১-এ। আর কোন commit ভেঙেছে তা পরিষ্কার না হলে git bisect run দিয়ে স্বয়ংক্রিয়ভাবে খোঁজা যায় — CI-তেও — পর্ব ৫৫-এ (Git দিয়ে bug খোঁজা)।

স্থানীয় hook বনাম CI — একটি আসল transcript

অনেক দল push-এর আগেই নিজের কম্পিউটারে check চালায় একটি pre-push hook দিয়ে — .git/hooks/pre-push script, যা ব্যর্থ হলে push থামে (hook-এর বিস্তারিত পর্ব ৫৬-এ, Git-এর শক্তিশালী হাতিয়ার)। মূল কৌশলটি খেয়াল করুন: hook আর CI দুজনেই একই script চালায় — ci/check.sh. নিচের transcript এই যন্ত্রে Git ২.৫৩-এ একটি খালি scratch repository-তে তৈরি, একটি local bare repository-কে remote ধরে; লেখক Rahim, তারিখ নির্দিষ্ট করা, তাই hash আপনার যন্ত্রেও একই আসবে:

$ cat ci/check.sh
#!/bin/sh
# The ONE script both the pre-push hook and the CI job run.
set -e
python -m py_compile price.py
python test_price.py
$ cat .git/hooks/pre-push
#!/bin/sh
echo "pre-push: running ci/check.sh"
sh ci/check.sh || { echo "pre-push: checks failed, push blocked"; exit 1; }

Rahim coupon-এর হিসাব “সরল” করতে গিয়ে coupon // 100-কে ভুল করে coupon // 10 লিখলেন, commit করে push করলেন:

$ git commit -q -am "perf: simplify coupon maths"
$ git push origin main
pre-push: running ci/check.sh
ok   total([100, 200], 0) = 300 (want 300)
FAIL total([100, 200], 10) = 0 (want 270)
ok   total([], 50) = 0 (want 0)
tests: 3 failed: 1
pre-push: checks failed, push blocked
error: failed to push some refs to '../origin.git'
$ git push --no-verify origin main
To ../origin.git
   3b7b2d9..7521f2d  main -> main
$ git log --oneline
7521f2d perf: simplify coupon maths
3b7b2d9 feat: add price total with coupon

প্রথম push-এ hook test চালাল: ৩০০ টাকার ১০% coupon-এ আসা উচিত ২৭০, এল ০ (৩০০ × ১০ // ১০ = ৩০০, আর ৩০০ − ৩০০ = ০)। Hook শূন্য নয় এমন exit code দিল, Git push থামাল। কিন্তু --no-verify দিলে Git hook চালায়ই না, আর ভাঙা commit 7521f2d সোজা main-এ। Hook নিজের যন্ত্রে থাকে, clone-এর সাথে আসে না, এক flag-এ এড়ানো যায় — তাই এটি দ্রুত প্রথম পাহারা; আসল দারোয়ান server-এর CI আর branch protection. (এখানে local bare repository, কোনো protection নেই।)

সাত মিনিট পরে CI-তে main লাল। দল আগে revert করল:

$ git revert --no-edit HEAD
[main 3afa6a2] Revert "perf: simplify coupon maths"
 Date: Thu Jan 15 10:10:00 2026 +0600
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git push origin main
pre-push: running ci/check.sh
ok   total([100, 200], 0) = 300 (want 300)
ok   total([100, 200], 10) = 270 (want 270)
ok   total([], 50) = 0 (want 0)
tests: 3 failed: 0
To ../origin.git
   7521f2d..3afa6a2  main -> main
$ git log --oneline
3afa6a2 Revert "perf: simplify coupon maths"
7521f2d perf: simplify coupon maths
3b7b2d9 feat: add price total with coupon

Revert একটি নতুন commit 3afa6a2 বানাল, যা 7521f2d-এর বদলটির ঠিক উল্টো — ইতিহাসে তিনটি commit-ই থাকল, কেউ push --force করেননি, অন্যদের clone-এ কোনো গোলমাল নেই। এবার hook-ও সবুজ, push গেল। এখানে hook-এর ফল আর CI-এর ফল একই হবে — কারণ দুজনেই একই ci/check.sh চালায়। এটিই স্থানীয় পুনরুৎপাদনযোগ্যতা: CI-তে লাল হলে ডেভেলপার নিজের যন্ত্রে একটি command-এ হুবহু একই check চালাতে পারেন। YAML-এ লম্বা shell-এর যুক্তি লিখলে এটি হারায় — তখন “CI-তে ভাঙে, আমার কাছে চলে” বিতর্ক শুরু হয়।

bazar-cart · main (কাল্পনিক দল, আসল hash)
ঘড়ি
main-এর মাথা
CI
দল
মাপ
  1. সবুজ main. 3b7b2d9-এ সব check সবুজ। দলের সবাই এর ওপর কাজ করছেন।
  2. ভাঙা commit. Rahim hook এড়িয়ে (--no-verify) 7521f2d push করলেন। CI চলছে; এখনো কেউ জানে না।
  3. লাল। সাত মিনিটে CI লাল: total([100, 200], 10) ফেরত দিল ০. Chat-এ বার্তা, PR-এর check-এ লাল — “stop the line”: merge queue থামানো, কেউ লাল main-এর ওপর নতুন কিছু মেলাচ্ছেন না।
  4. Revert. কারণ অনুমান করা যায়, কিন্তু নিশ্চিত fix আর তার test-এ সময় লাগবে, আর Rahim একটি meeting-এ। দলের নিয়ম: অনিশ্চিত হলে আগে revert. যিনি দেখলেন, তিনিই git revert --no-edit HEAD → 3afa6a2, push. দোষ খোঁজা নয় — কাজ main সবুজ করা।
  5. সবুজ। ১০:১৭-এ CI সবুজ। লাল শুরু ১০:০৮, শেষ ১০:১৭ — time to fix ৯ মিনিট, দশের নিচে। Queue আবার চলছে।
  6. শান্ত মাথায় সমাধান। Rahim নিজের যন্ত্রে sh ci/check.sh চালিয়ে ভুলটি দেখলেন, branch-এ সঠিক বদল আর একটি বাড়তি boundary test সহ PR খুললেন — সাধারণ review আর CI পেরিয়ে merge. দলের পর্যালোচনার প্রশ্ন ব্যক্তি নিয়ে নয়, ব্যবস্থা নিয়ে: main-এ সরাসরি push কেন সম্ভব ছিল? উত্তর: branch protection চালু করা।

স্থানীয়ভাবে CI পুনরুৎপাদন

  • একই script আর version: make test বা ci/check.sh — YAML শুধু এদের ডাকে; lockfile আর .nvmrc-এর মতো version file CI-ও পড়ে।
  • একই পরিবেশ: CI-এর job যে container image-এ চলে, স্থানীয়ভাবেও সেটি চালানো যায় (docker run … node:24 sh ci/check.sh); মূল ধারণা Operating Systems কোর্সের Container পর্বে। Runner-এ আগে থেকে বসানো tool-এর ওপর লুকানো নির্ভরতা রাখবেন না।

৯ · CI-এর স্বাস্থ্য মাপা — মেট্রিক আর CI theatre

“আমাদের CI কেমন চলছে?” — অনুভূতিতে নয়, চারটি সংখ্যায় উত্তর দেওয়া যায়:

  • সময় (duration): গড় নয়, percentile — p50 (সাধারণ দিন) আর p90 (খারাপ দিন); মানুষ মনে রাখে খারাপ দিন।
  • Failure rate: main-এর কত শতাংশ build লাল। খুব বেশি মানে অস্থির কোড বা flaky test; শূন্যের কাছে হলে প্রশ্ন — test কি কিছু ধরছে?
  • Time to fix: লাল থেকে আবার সবুজ, প্রতিটি লাল ধারার জন্য একবার; এটি দলের অভ্যাস মাপে।
  • Queue time: runner পাওয়ার অপেক্ষা; বাড়লে কোড না বদলেও feedback ধীর।

নিচের প্রোগ্রাম main-এর ৪০টি build-এর একটি কাল্পনিক ইতিহাস তৈরি করে — seed-যুক্ত PRNG (mulberry32, seed 58) দিয়ে, তাই ফল প্রতিবার একই — আর চারটি মাপ হিসাব করে। Percentile-এর পদ্ধতি nearest-rank: n-টি মান সাজিয়ে p-তম percentile হলো ⌈p/100 × n⌉-তম মান। ইতিহাসে একটি ধারণা বসানো: আগের build লাল হলে পরেরটির লাল হওয়ার সম্ভাবনা বেশি (৫০%), কারণ প্রথম fix প্রায়ই অসম্পূর্ণ:

// CI health metrics from a SIMULATED build history of main (mulberry32 = seeded PRNG).
function mulberry32(a) {
  return () => { a |= 0; a = a + 0x6D2B79F5 | 0; let t = Math.imul(a ^ a >>> 15, 1 | a);
    t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; };
}
const rand = mulberry32(58);
const builds = []; let clock = 0;                       // minutes since Monday 09:00
for (let i = 0; i < 40; i++) {
  clock += 15 + Math.floor(rand() * 60);                // next push: 15..74 min later
  const queue = Math.floor(rand() * 4);                 // waits 0..3 min for a runner
  const dur = 6 + Math.floor(rand() * 7) + (rand() < 0.1 ? 15 : 0);  // 6..12 min, sometimes +15
  const red = builds.length && builds.at(-1).red ? rand() < 0.5 : rand() < 0.25;
  builds.push({ start: clock, queue, dur, red, end: clock + queue + dur });
}
const nearestRank = (xs, p) => { const s = [...xs].sort((a, b) => a - b);
  return s[Math.ceil(p / 100 * s.length) - 1]; };
// time to fix: from the end of the first red build to the end of the next green build
const fixes = [];
for (let i = 0; i < builds.length; i++) {
  if (!builds[i].red || (i && builds[i - 1].red)) continue;      // start of a red streak only
  const g = builds.findIndex((b, j) => j > i && !b.red);
  if (g > 0) fixes.push(builds[g].end - builds[i].end);
}
const reds = builds.filter(b => b.red).length;
const durs = builds.map(b => b.dur), queues = builds.map(b => b.queue);
console.log(`builds ${builds.length}, red ${reds} -> failure rate ${(100 * reds / builds.length).toFixed(1)}%`);
console.log(`red streaks ${fixes.length}, time to fix (min): ${fixes.join(", ")}`);
console.log(`mean time to fix ${(fixes.reduce((a, b) => a + b, 0) / fixes.length).toFixed(1)} min`);
console.log(`duration p50 ${nearestRank(durs, 50)} min, p90 ${nearestRank(durs, 90)} min, max ${Math.max(...durs)} min`);
console.log(`queue p50 ${nearestRank(queues, 50)} min, p90 ${nearestRank(queues, 90)} min`);

আউটপুট:

builds 40, red 7 -> failure rate 17.5%
red streaks 5, time to fix (min): 30, 42, 86, 55, 93
mean time to fix 61.2 min
duration p50 10 min, p90 24 min, max 25 min
queue p50 1 min, p90 3 min

৪০টির মধ্যে ৭টি লাল — ১৭.৫%. কিন্তু লাল ধারা ৫টি, কারণ দুবার লাল পরপর এসেছে (প্রথম fix কাজ করেনি)। Time to fix প্রতিটি ধারার জন্য একবার: প্রথম লাল build শেষ হওয়া থেকে পরের সবুজ build শেষ হওয়া পর্যন্ত; গড় (৩০ + ৪২ + ৮৬ + ৫৫ + ৯৩) ÷ ৫ = ৩০৬ ÷ ৫ = ৬১.২ মিনিট — অংশ ১-এর “দশ মিনিটের” পরীক্ষায় দলটি অনেক পিছিয়ে। এই মডেলে কেউ revert করছেন না; পরের push আসা পর্যন্ত লাল থাকছে — বাস্তবেও ঠিক এভাবেই time to fix ফুলে ওঠে। Duration: p50 ১০ মিনিট (সাজানো ৪০টির ২০-তম মান), p90 ২৪ মিনিট (৩৬-তম) — ৪০টির মধ্যে ৬টি build-এ ১৫ মিনিট বাড়তি (হয়তো cache miss), আর সেগুলোই p90 ঠিক করে। গড় দেখলে এটি চোখে পড়ত না।

DORA-এর delivery মেট্রিক এই মাপগুলোর বড় ভাই, পুরো delivery-র পথের জন্য — পর্ব ৫৯-এ। একটি সাবধানতা: মেট্রিককে লক্ষ্য বানালে মানুষ সংখ্যা ঠিক করে, ব্যবস্থা নয় (Goodhart-এর সূত্র) — যেমন failure rate কমাতে test মুছে ফেলা।

CI theatre — tool আছে, অনুশীলন নেই

CI theatre কথাটি শিল্পে চালু, আর ২০১৯ সালের একটি গবেষণাপত্র (“Continuous Integration Theater”, Felidré ও সহকর্মীরা, ESEM) বহু open-source project-এ এর লক্ষণ খুঁজেছে — অনিয়মিত commit, দীর্ঘ সময় ভাঙা build, খুব ধীর build. চেনা লক্ষণ:

লক্ষণকেন CI নয়প্রতিকার
দীর্ঘজীবী feature branch, প্রতিটিতে সুন্দর CImainline-এ মেলানো সপ্তাহে একবার; integration-এর ঝুঁকি branch-এ জমেছোট PR, দিনে merge; অসম্পূর্ণ কাজ feature flag-এর আড়ালে (পর্ব ১৭)
লাল build দিনের পর দিন, “ওটা সবসময় লাল থাকে”সংকেত মরে গেছে; নতুন ভুল পুরোনো লালে লুকায়stop the line, আগে revert; flaky test quarantine (পর্ব ৫১)
শুধু nightly buildভুল ধরা পড়ে ১২–২৪ ঘণ্টা পরে, সেদিনের সব commit সন্দেহেপ্রতি push-এ দ্রুত suite; রাতে শুধু ধীর অংশ
৪৫ মিনিটের buildকেউ অপেক্ষা করে না, commit জমে, feedback দেরিতেcritical path ছোট, cache, sharding, ধীর test নিচের স্তরে
npm test || true, continue-on-errorলাল test-ও সবুজ দেখায় — মিথ্যা সংকেতreview-এ ধরুন; exit code-কে সত্য বলতে দিন
Test প্রায় নেই, কিন্তু build সবুজself-testing build নয় — সবুজ মানে শুধু “compile হয়”ঝুঁকিভিত্তিক test, mutation testing দিয়ে test-এর মান যাচাই (পর্ব ৫০)

আরও গভীরে: মূল উৎস Martin Fowler-এর “Continuous Integration” লেখা (২০০০, ২০০৬-এ সংশোধিত, পরে আবার হালনাগাদ) আর Paul Duvall, Steve Matyas ও Andrew Glover-এর Continuous Integration: Improving Software Quality and Reducing Risk (২০০৭)। Humble ও Farley-এর Continuous Delivery (২০১০) CI-কে deployment pipeline-এর প্রথম ধাপ হিসেবে দেখায়; Software Engineering at Google (২০২০) বইয়ের CI অধ্যায়ে বিশাল monorepo-তে presubmit আর post-submit test-এর প্রকাশিত বিবরণ আছে। Build-এর নিরাপত্তা আর উৎস-প্রমাণের (provenance) জন্য OpenSSF-এর SLSA framework দেখুন।

পরীক্ষা ও ইন্টারভিউতে:

  • Continuous integration কী? — দলের সবাই ঘন ঘন (অন্তত দিনে একবার) mainline-এ ছোট পরিবর্তন মেলান, আর প্রতিটিতে স্বয়ংক্রিয় build ও test চলে; ভাঙলে সাথে সাথে ঠিক। এটি অনুশীলন, শুধু tool নয়।
  • CI, continuous delivery আর continuous deployment-এর পার্থক্য? — মেলানো ও যাচাই; সবসময় release-যোগ্য রাখা; প্রতিটি সবুজ পরিবর্তন স্বয়ংক্রিয়ভাবে production-এ।
  • Cache আর artifact-এর পার্থক্য? — cache গতির জন্য, না থাকলেও ফল একই (key = lockfile-এর hash); artifact এক job-এর ফল আরেক job বা মানুষের কাছে পাঠায়, তার ওপর পরের ধাপ নির্ভর করে।
  • CI-তে secret নিরাপদ রাখবেন কীভাবে? — platform-এর secret store, masked, ন্যূনতম অনুমতি, দীর্ঘমেয়াদি key-এর বদলে OIDC, fork-এর PR-এ secret নয়, তৃতীয় পক্ষের action SHA-তে pin.
  • Main লাল হলে কী করবেন? — সবাইকে জানানো, নতুন merge থামানো, কয়েক মিনিটে নিশ্চিত fix না হলে revert, তারপর branch-এ সমাধান আর ব্যবস্থার পর্যালোচনা।

১০ · AI-এর যুগে — AI-লেখা pipeline যাচাই

AI কয়েক সেকেন্ডে চমৎকার দেখতে CI YAML দেয়। কিন্তু YAML-এর ভুল চুপচাপ থাকে: বানানো input নীরবে উপেক্ষিত হয়, আর নিরাপত্তার ভুল কোনো test-এ ধরা পড়ে না।

AI-এর চেনা ভুলকীভাবে ধরবেন
বানানো action input বা পুরোনো major version (@v2), অস্তিত্বহীন actionপ্রতিটি action-এর repository-র action.yml আর README; git ls-remote --tags দিয়ে version দেখা
permissions: write-all, বা অনুমতির কোনো উল্লেখ নেইশুরু contents: read; প্রতিটি বাড়তি অনুমতির কারণ লিখে রাখা
লেবেল বা comment-এর জন্য pull_request_target, সাথে PR-এর কোড checkoutFork-এর কোড কখনো secret-এর পাশে নয়; platform-এর security documentation পড়া
Debug-এর জন্য echo $TOKEN বা env ছাপানোSecret কখনো ছাপানো নয়; masking-এর ওপর ভরসা নয়
লাল ঢাকতে || true, continue-on-error: true, অন্ধ retryইচ্ছে করে একটি test ভেঙে দেখুন pipeline সত্যিই লাল হয় কি না
Cache key-এ lockfile নেই (যেমন শুধু OS), বা install-এ npm installLockfile বদলালে key বদলায় কি না; npm ci-এর মতো lockfile-মানা command
CI “ঠিক” করতে git push --force বা reset --hard-এর পরামর্শ, সতর্কতা ছাড়াThrowaway clone-এ আগে চালানো; ভাগ করা branch-এ revert

AI-কে pipeline-এর নকশা আর review-এর সহকারী হিসেবে ব্যবহারের একটি prompt — এতে নিরাপত্তা, যাচাই আর অনিশ্চিত হলে তা খোলাখুলি জানানোর নির্দেশ আছে:

তুমি একজন সতর্ক build engineer. নিচে আমার কাল্পনিক project-এর বিবরণ আর বর্তমান CI YAML দিলাম (কোনো secret, token বা ভেতরের URL নেই)। [language, package manager, lockfile-এর নাম, test-এর command] [বর্তমান .github/workflows/ci.yml] ১. Pipeline-কে DAG হিসেবে আঁকো: কোন job কার ওপর নির্ভর করে, critical path কোনটি, আর কোথায় সমান্তরাল করা যায়। ২. প্রতিটি action-এর নাম আর version আলাদা তালিকায় দাও, আর লেখো "repository-র action.yml-এ input মিলিয়ে নিন" — নিশ্চিত না হলে কোনো input বানাবে না। ৩. নিরাপত্তা: permissions, secret-এর ব্যবহার, fork-এর PR, pull_request_target, তৃতীয় পক্ষের action-এর SHA pinning — প্রতিটির ঝুঁকি বলো। ৪. Cache key কী দিয়ে তৈরি, আর lockfile বদলালে বদলায় কি না বলো। ৫. কোনো || true, continue-on-error বা retry দিয়ে ব্যর্থতা ঢাকার পরামর্শ দেবে না; force push বা reset --hard প্রস্তাব করবে না।

AI-এর লেখা pipeline — চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১একটি দলে Jenkins আছে, প্রতিটি feature branch-এ পূর্ণ pipeline চলে, dashboard সবুজ। কিন্তু branch-গুলো গড়ে তিন সপ্তাহ পরে main-এ মেলে। দলটি কি continuous integration করছে?

পুরো অর্থে না — আর এই উত্তরটি CI-এর ইতিহাস থেকেই আসে। XP যখন ১৯৯০-এর দশকের শেষে continuous integration-কে একটি অনুশীলন হিসেবে দাঁড় করায়, তখন সমস্যাটি ছিল ঠিক এই: দল আলাদা আলাদা কাজ করত, আর শেষে “integration phase”-এ সপ্তাহের পর সপ্তাহ মেলানোর যুদ্ধ। সমাধান কোনো tool ছিল না; ছিল একটি অভ্যাস — কয়েক ঘণ্টা পরপর সবার কাজ এক জায়গায় মেলানো। CruiseControl-এর মতো server এসেছে পরে, সেই অভ্যাসকে সহজ করতে।

প্রশ্নের দলটি tool রেখেছে, অভ্যাস হারিয়েছে। প্রতিটি branch-এর pipeline প্রমাণ করে “এই branch নিজের ভেতরে ঠিক আছে”, কিন্তু CI-এর আসল প্রশ্ন — “সবার কাজ একসাথে চলে কি না” — তার উত্তর আসে তিন সপ্তাহে একবার, merge-এর দিনে। তত দিনে main অনেক এগিয়েছে, অন্য দশটি branch-ও বদলেছে; merge-এ conflict আর semantic ভুল দুটিই জমে আছে। Jez Humble-এর তিন প্রশ্নের প্রথমটিতেই দলটি ব্যর্থ: সবাই কি দিনে অন্তত একবার mainline-এ মেলান? ২০১৯ সালের “Continuous Integration Theater” গবেষণাপত্র ঠিক এই ব্যবধান মাপতে চেয়েছিল — CI tool ব্যবহার করা অনেক project-এ commit-এর ঘনত্ব, ভাঙা build-এর স্থায়িত্ব আর build-এর সময়ে অনুশীলনের ঘাটতি পাওয়া গেছে।

তাহলে branch-এ CI কি অর্থহীন? না — PR-এ test চালানো দারুণ অভ্যাস, merge-এর আগে ভুল ধরে। সমস্যা branch-এর আয়ু। সমাধানও জানা: কাজ ছোট টুকরোয় ভাঙা (একটি বড় feature-কে কয়েকটি এক-দুই দিনের PR-এ), অসম্পূর্ণ অংশ feature flag-এর আড়ালে main-এ পাঠানো, আর “branch by abstraction”-এর মতো কৌশলে বড় পুনর্গঠন ধাপে ধাপে করা। অনেক দল মাপে branch-এর গড় আয়ু আর PR-এর আকার — CI-এর স্বাস্থ্যের সবচেয়ে সৎ সংখ্যাগুলোর দুটি।

মূল উপলব্ধি: CI-এর “continuous” শব্দটি integration-এর ঘনত্বের কথা বলে, pipeline-এর অস্তিত্বের নয়। সবুজ dashboard নয়, ছোট আর ঘন ঘন mainline-এ মেলানোই CI.

প্র ০২PR-এর সব check সবুজ, main-ও সবুজ — তবু merge-এর পরে main লাল হলো। এটি কীভাবে সম্ভব, আর merge queue-এর ধারণা কোথা থেকে এল?

PR-এর check চলেছিল PR খোলার সময়ের main-এর ওপর (বা PR branch আর তখনকার main-এর মেলানো রূপে)। ততক্ষণে অন্য একটি PR merge হয়েছে। দুটি পরিবর্তন আলাদাভাবে ঠিক, Git-এর কাছেও কোনো conflict নেই — একজন applyCoupon-এর নাম বদলে applyDiscount করলেন আর সব ডাক হালনাগাদ করলেন; অন্যজন একটি নতুন file-এ পুরোনো নামে ডাক যোগ করলেন। Textual merge নিখুঁত, semantic merge ভাঙা। দুটি সবুজ যোগ করে একটি লাল।

এই সমস্যার পরিচিত সমাধান আছে বহুদিন ধরে। Rust ভাষার স্রষ্টা Graydon Hoare ২০১৪ সালের একটি blog লেখায় যাকে বলেছিলেন “the not rocket science rule of software engineering”: স্বয়ংক্রিয়ভাবে এমন একটি repository রাখা, যা সবসময় সব test পাস করে — অর্থাৎ কোনো পরিবর্তন main-এ ঢোকার আগে ঠিক সেই রূপে test করা, যে রূপে সে main হবে। তিনি লিখেছিলেন এই নিয়মের চর্চা তিনি আগের কাজেও দেখেছেন। Rust project-এর জন্য তৈরি bors bot এই কাজটি করত: approve হওয়া PR-কে সর্বশেষ main-এর সাথে মিলিয়ে test, সবুজ হলে তবেই merge. OpenStack-এর Zuul একই ধারণাকে “gating” নামে বড় আকারে চালায়, আর ধারাবাহিক PR-গুলোকে অনুমানমূলকভাবে (speculatively) একসাথে test করে। পরে platform-গুলো নিজেই এটি দিল — GitLab-এর merge train, আর GitHub-এর merge queue (লেখার সময়ের তথ্য অনুযায়ী ২০২৩ সালে সাধারণভাবে চালু)।

খরচও আছে। প্রতিটি PR-কে আবার test করলে সময় লাগে; queue-গুলো তাই কয়েকটি PR একসাথে batch করে, ব্যর্থ হলে ভাগ করে দোষী খোঁজে — অনেকটা bisect-এর মতো। Suite ধীর হলে queue-ই দলের bottleneck হয়ে ওঠে, তাই দ্রুত, স্থির test এখানে আরও জরুরি।

মূল উপলব্ধি: “সবুজ + সবুজ = সবুজ” সত্য নয়। Main-কে নিশ্চিতভাবে সবুজ রাখতে প্রতিটি পরিবর্তনকে ঠিক সেই রূপে test করতে হয়, যে রূপে সে main-এ বসবে — merge queue সেই নিয়মের যন্ত্র।

প্র ০৩“Build দশ মিনিটের মধ্যে” — এই নির্দেশিকা কোথা থেকে, আর একটি বড় project যার পুরো suite দুই ঘণ্টার, সে কী করবে?

সংখ্যাটির উৎস Extreme Programming. Kent Beck-এর বইয়ের দ্বিতীয় সংস্করণে (২০০৪) “Ten-Minute Build” আলাদা অনুশীলন, আর Fowler-ও তাঁর CI লেখায় একে XP-র নির্দেশিকা হিসেবে উল্লেখ করেছেন। এর পেছনে কোনো গবেষণার জাদুসংখ্যা নেই; আছে মানুষের মনোযোগের সাধারণ অভিজ্ঞতা। কয়েক মিনিট একজন মানুষ অপেক্ষা করেন — চা নেন, PR-এর বিবরণ লেখেন। আরও বেশি হলে অন্য কাজে ডুবে যান, আর ফল লাল এলে আবার প্রসঙ্গে ফিরতে বাড়তি সময় যায়। আরও খারাপ, ধীর build-এ মানুষ commit জমাতে শুরু করেন — “একবারে পাঠাই” — আর ঠিক তাতেই CI-এর ছোট ধাপের মূল সুবিধা হারায়।

দুই ঘণ্টার suite-এর project-এর জন্য উত্তর একটি সিদ্ধান্ত নয়, কয়েকটি স্তর। প্রথম, deployment pipeline-এর ধারণা (Humble ও Farley, ২০১০): একটি দ্রুত “commit stage” — static check, unit test আর কয়েকটি মূল integration test, মিনিট কয়েক — প্রতিটি push আর PR-এ; বাকি ধীর suite পরের stage-এ, merge-এর পরে বা সমান্তরালে, যার ব্যর্থতাও দল সাথে সাথে দেখে। দ্বিতীয়, শুধু প্রভাবিত অংশ: monorepo-তে dependency graph দেখে শুধু বদলানো অংশের test (পর্ব ১৯-এর affected build); Bazel-এর মতো build system remote cache দিয়ে অন্য কারও আগে চালানো একই test-এর ফল আবার ব্যবহার করে। তৃতীয়, sharding আর parallelism — কিন্তু critical path মাথায় রেখে, কারণ setup-এর খরচ প্রতিটি shard দেয়। চতুর্থ, test-এর আকার নিজেই: দুই ঘণ্টার বড় অংশ প্রায়ই অল্প কিছু ধীর E2E test, যাদের অনেকটা নিচের স্তরে নামানো যায় (পর্ব ৫১)।

আর একটি সৎ স্বীকারোক্তি: কিছু domain-এ — embedded, game, বিশাল C++ codebase — দশ মিনিট বাস্তবে অসম্ভব হতে পারে। তখন লক্ষ্য সংখ্যা নয়, প্রবণতা: build সময় মাপা, p90 দেখা, আর প্রতিটি বৃদ্ধিকে প্রশ্ন করা।

মূল উপলব্ধি: দশ মিনিট একটি মানবিক সীমার নাম, প্রকৃতির নিয়ম নয়। দ্রুত প্রথম সংকেত প্রতিটি push-এ, ধীর আস্থা পরে — এই স্তরভাগই বড় project-এর উত্তর।

প্র ০৪কেন বলা হয় CI system নিজেই আক্রমণের লক্ষ্য? প্রকাশিত কয়েকটি ঘটনা থেকে কী শেখা যায়, আর প্রতিরক্ষার স্তরগুলো কী?

CI এমন জায়গা, যেখানে তিনটি জিনিস একসাথে থাকে: অন্যের লেখা কোড (dependency, action, PR), শক্তিশালী অনুমতি (deploy-এর key, package প্রকাশের token), আর স্বয়ংক্রিয়তা — যা হাজারবার চলে, কেউ চোখে দেখে না। একটি ছোট ফাঁক মানে বহু system-এ একসাথে প্রবেশ।

প্রকাশিত ঘটনাগুলো এই ঝুঁকির বিভিন্ন দিক দেখায়। ২০২০ সালে প্রকাশিত SolarWinds ঘটনায়, প্রকাশিত বিশ্লেষণ অনুযায়ী, আক্রমণকারীরা প্রতিষ্ঠানটির build প্রক্রিয়ায় ঢুকে Orion software-এর build-এর সময় ক্ষতিকর কোড বসিয়েছিল — source code পরিষ্কার দেখালেও তৈরি হওয়া software ছিল দূষিত, আর তা স্বাক্ষরসহ গ্রাহকদের কাছে update হিসেবে গেছে। ২০২১ সালের Codecov ঘটনায় একটি জনপ্রিয় upload script পরিবর্তন করে বহু গ্রাহকের CI থেকে environment variable — অর্থাৎ secret — বাইরে পাঠানো হয়েছিল বলে জানানো হয়। আর ২০২৫ সালের মার্চে tj-actions/changed-files-এর tag-গুলো একটি ক্ষতিকর commit-এ সরানো হয়, যা runner-এর secret log-এ ছাপিয়ে দিত; যাঁদের pipeline পুরোনো, পরিষ্কার SHA-তে pin করা ছিল, তাঁরা প্রভাবিত হননি বলে বলা হয়। (প্রতিটির বিস্তারিত সংশ্লিষ্ট প্রতিষ্ঠান ও গবেষকদের প্রকাশিত বিবরণে দেখুন।)

প্রতিরক্ষা তাই স্তরে স্তরে। ন্যূনতম অনুমতি: token-এর ডিফল্ট শুধু পড়া, deploy-এর অনুমতি শুধু deploy job-এ, আর environment-এর অনুমোদন। দীর্ঘমেয়াদি key নয়, OIDC — ফাঁস হলেও কয়েক মিনিটে মেয়াদ শেষ। তৃতীয় পক্ষের কোড SHA-তে pin, হালনাগাদ review-এর মধ্য দিয়ে। অবিশ্বস্ত কোড (fork-এর PR) secret থেকে আলাদা। ক্ষণস্থায়ী runner — প্রতিটি job নতুন যন্ত্রে, যাতে একটি দূষিত job পরের job-এ কিছু রেখে যেতে না পারে। আর build-এর ফলের উৎস-প্রমাণ (provenance): কোন source, কোন pipeline থেকে কোন artifact তৈরি — OpenSSF-এর SLSA framework এর স্তরগুলো বর্ণনা করে।

মূল উপলব্ধি: আপনার software ততটুকুই নিরাপদ, যতটুকু নিরাপদ তাকে বানানোর পথ। CI-কে production-এর মতো গুরুত্ব দিয়ে সুরক্ষিত করুন — কারণ এটি production-এর দরজা।

প্র ০৫লাল main-এ “আগে revert” নিয়মটি কিছু ডেভেলপারের কাছে অপমানজনক লাগে — “আমার কাজ মুছে দিল!” এই নিয়মের পেছনের যুক্তি কী, আর কখন fix forward ভালো?

নিয়মটির শিকড় শিল্প-উৎপাদনে। Toyota Production System-এর একটি পরিচিত ধারণা “andon” — কারখানার যেকোনো কর্মী সমস্যা দেখলে দড়ি টেনে লাইন থামাতে পারেন, যাতে ত্রুটিপূর্ণ জিনিস লাইনের পরের ধাপে না গড়ায়। CI-তে “stop the line” একই ভাবনা: লাল main-এর ওপর আর কিছু জমতে দেবেন না। আর লাইন আবার চালু করার দ্রুততম, সবচেয়ে নিশ্চিত পথ সাধারণত revert — জানা-ভালো অবস্থায় ফেরা।

অপমানের অনুভূতিটি বোঝা যায়, তাই নিয়মের সাথে একটি সংস্কৃতিও লাগে। Revert কাজ মোছে না — Git-এ commit থাকে, branch-এ ফিরিয়ে এনে ঠিক করে আবার পাঠানো এক মিনিটের কাজ। Revert কোনো রায়ও নয়: যিনি revert করেন, তিনি লেখককে দোষ দিচ্ছেন না, দলের সময় বাঁচাচ্ছেন। ভালো দলে নিয়মটি আগেই লিখে সবাই মেনে নেন — “লাল হলে দশ মিনিট, তারপর revert, যে-ই দেখুক” — যাতে মুহূর্তের সিদ্ধান্ত ব্যক্তিগত না হয়। পর্যালোচনা হয় ব্যবস্থা নিয়ে (কেন test আগে ধরল না, কেন সরাসরি push সম্ভব হলো), মানুষ নিয়ে নয় — blameless postmortem-এর চর্চা।

তবু fix forward কখনো ভালো। যখন কারণ স্পষ্ট আর fix এক লাইনের, যার test এখনই চালানো যায়। যখন revert নিজেই ঝুঁকির — ধরুন commit-টি একটি database migration চালিয়েছে, যার উল্টো migration তথ্য হারাবে; বা অন্য অনেক commit ইতিমধ্যে এর ওপর দাঁড়িয়ে, আর revert-এ নতুন conflict. Feature flag থাকলে আরও ভালো তৃতীয় পথ: কোড রেখে flag বন্ধ। এখানেও DORA-র গবেষণার “পুনরুদ্ধারের সময়” মেট্রিকের শিক্ষা একই — দ্রুত ফেরার ক্ষমতা দলের মূল সক্ষমতা (পর্ব ৫৯)।

মূল উপলব্ধি: Revert-first একটি সময়ের নিয়ম, দোষের নয়: প্রথমে সবার জন্য লাইন চালু, তারপর একজনের জন্য শান্ত মাথায় সমাধান। নিয়মটি আগে থেকে সবাই মানলে কেউ অপমানিত হন না।

অনুশীলন

প্রথমে নিজে উত্তর লিখুন, তারপর মিলিয়ে দেখুন।

  1. হিসাব (parallelism আর cache): একটি কাল্পনিক project-এর CI-তে পাঁচটি কাজ: lint ১ মিনিট, build ৩, security scan ২, unit test ১২, integration ৬. Unit-এর আগে build লাগে, integration-এর আগে unit; lint আর scan স্বাধীন। প্রতিটি job নতুন runner-এ চলে আর আগে setup দেয় (checkout ০.৫ মিনিট + dependency install): cache ছাড়া install ৪ মিনিট, cache-এ hit হলে ০.৫. Runner যথেষ্ট আছে। (ক) সব কাজ একটি job-এ পরপর, cache ছাড়া — কত সময়? (খ) পাঁচটি আলাদা job, DAG মেনে সমান্তরাল, cache ছাড়া? (গ) একই, cache hit-সহ? (ঘ) তার ওপর unit test ৪টি সমান shard-এ? (ঙ) (ঘ)-এর মোট runner-মিনিট কত, (ক)-এর তুলনায়? (চ) অংশ ৪-এর matrix-এর ৯টি job (৪, ৬, ৪, ৭, ৭, ৭, ৬, ৬, ৫ মিনিট) ৪টি runner-এ সবচেয়ে ভালো সাজালে দেয়াল-ঘড়ি কত কম হতে পারে?

    Setup S = ০.৫ + ৪ = ৪.৫ মিনিট (cache ছাড়া), ০.৫ + ০.৫ = ১ মিনিট (cache hit).

    (ক) একটি job: ৪.৫ + ১ + ৩ + ২ + ১২ + ৬ = ২৮.৫ মিনিট.

    (খ) Critical path build → unit → integration, প্রতিটি job নিজের setup দেয়: (৪.৫ + ৩) + (৪.৫ + ১২) + (৪.৫ + ৬) = ৭.৫ + ১৬.৫ + ১০.৫ = ৩৪.৫ মিনিট — একটি job-এর চেয়েও ধীর! Lint (৫.৫) আর scan (৬.৫) critical path-এর বাইরে, তাই তাদের সমান্তরাল করে কিছু বাঁচেনি, অথচ setup তিনবার।

    (গ) Cache hit-সহ: (১ + ৩) + (১ + ১২) + (১ + ৬) = ৪ + ১৩ + ৭ = ২৪ মিনিট.

    (ঘ) Unit ৪ shard, প্রতিটি ১ + ৩ = ৪ মিনিট, সব একসাথে: ৪ + ৪ + ৭ = ১৫ মিনিট — (ক)-এর প্রায় অর্ধেক।

    (ঙ) lint ২ + build ৪ + scan ৩ + চার shard ৪ × ৪ = ১৬ + integration ৭ = ৩২ runner-মিনিট, (ক)-এর ২৮.৫-এর চেয়ে প্রায় ১২% বেশি — প্রায় দ্বিগুণ দ্রুত feedback, সামান্য বেশি খরচে।

    (চ) মোট ৫২ runner-মিনিট, ৪টি runner — নিচের সীমা ⌈৫২ ÷ ৪⌉ = ১৩, আর একক দীর্ঘতম job ৭, তাই ১৩-এর নিচে অসম্ভব। ১৩ পাওয়া যায়: (৭ + ৬), (৭ + ৬), (৭ + ৬), (৪ + ৪ + ৫) — প্রতিটি ঠিক ১৩। অর্থাৎ ১৩ মিনিট, সারির ক্রমের ১৬-র বদলে। “লম্বা job আগে” (LPT) নিয়মেও এখানে ১৫ আসে — ছোট সমস্যাতেও সেরা সাজানো সহজ নয়। (সংখ্যাগুলো একটি ছোট script চালিয়ে মিলিয়েছি।)

  2. Pipeline-এর YAML লিখুন: কাল্পনিক Python project result-portal-এর জন্য একটি GitHub Actions workflow লিখুন: (১) main-এ push, main-মুখী PR, আর প্রতি রাত ঢাকার সময় ৩:৩০-এ চলবে; (২) token শুধু পড়ার অনুমতি; (৩) ubuntu-তে Python 3.13 আর 3.14 — দুটি job; (৪) requirements.txt ধরে pip-এর cache; (৫) test: python -m unittest discover -s tests -v, আর তার আউটপুট report.txt-এ; (৬) test ব্যর্থ হলেও report একটি artifact হিসেবে থাকবে; (৭) job ১৫ মিনিটের বেশি চলবে না।

    একটি সম্ভাব্য উত্তর (লেখার সময়ের GitHub Actions documentation আর action-গুলোর action.yml-এর input মিলিয়ে লেখা; চালানোর আগে আপনার সময়ের version দেখে নিন):

    # .github/workflows/ci.yml  (result-portal, a fictional Python project)
    name: ci
    on:
      pull_request:
        branches: [main]
      push:
        branches: [main]
      schedule:
        - cron: "30 21 * * *"         # 21:30 UTC = 03:30 in Dhaka
    permissions:
      contents: read
    jobs:
      test:
        runs-on: ubuntu-latest
        timeout-minutes: 15
        strategy:
          matrix:
            python: ["3.13", "3.14"]  # quoted: 3.10 unquoted would become 3.1
        steps:
          - uses: actions/checkout@v7
            with:
              persist-credentials: false
          - uses: actions/setup-python@v7
            with:
              python-version: ${{ matrix.python }}
              cache: pip
              cache-dependency-path: requirements.txt
          - run: python -m pip install -r requirements.txt
          - name: unit tests, output also saved to report.txt
            shell: bash               # explicit bash adds -o pipefail
            run: python -m unittest discover -s tests -v 2>&1 | tee report.txt
          - uses: actions/upload-artifact@v7
            if: always()              # keep the report even when tests fail
            with:
              name: report-${{ matrix.python }}
              path: report.txt

    যাচাইয়ের বিন্দু: ঢাকা UTC+6, তাই ৩:৩০ = আগের দিনের UTC 21:30 — 30 21 * * *. Version উদ্ধৃতিচিহ্নে, নইলে 3.10 সংখ্যা হয়ে 3.1. সবচেয়ে সূক্ষ্ম ফাঁদ pipe: … | tee report.txt-এ shell শেষ command-এর (tee) exit code নেয়, যা সবসময় সফল — ব্যর্থ test-ও সবুজ দেখাত। GitHub Actions-এর documentation অনুযায়ী (লেখার সময়ে) shell: bash স্পষ্ট লিখলে -o pipefail যোগ হয়; না লিখলে Linux-এ ডিফল্ট bash -e, pipefail ছাড়া। if: always() ব্যর্থতার পরেও report আপলোড করে; দুটি matrix job-এর artifact-এর নাম আলাদা, নইলে সংঘাত। YAML-টি একটি parser দিয়ে পড়ে গঠন যাচাই করা হয়েছে; GitHub-এর নিজের যাচাই আসল repository-তে পাবেন।

  3. আউটপুট অনুমান করুন: অংশ ৩-এর pipeline_runner.js-এ শুধু দুটি বদল: unit-এর ok: false থেকে ok: true, আর security-এর ok: true থেকে ok: false. বাকি সব (needs, সময়) একই — checkout ৫ সেকেন্ড; install ৪০ (needs checkout); lint ২০, build ৬০, security ৪৫ (তিনটিই needs install); unit ৯০ (needs build); integration ১৮০ (needs unit); artifact ১৫ (needs lint, integration, security). প্রোগ্রাম ঠিক কী ছাপবে?

    আসলে চালালে আউটপুট:

    PASS checkout     0s -> 5s
    PASS install      5s -> 45s
    PASS lint         45s -> 65s
    PASS build        45s -> 105s
    PASS unit         105s -> 195s
    FAIL security     45s -> 90s
    PASS integration  195s -> 375s
    SKIP artifact     needs security
    wall clock 375s, serial would be 440s
    status: FAILED | failed: security | skipped: artifact

    ছাপার ক্রম topological (DFS), তাই security লেখা হয় unit-এর পরে, যদিও সে ৯০ সেকেন্ডেই শেষ। Security ব্যর্থ, কিন্তু integration security-র ওপর নির্ভর করে না — সে পুরো ১৮০ সেকেন্ড চলে ৩৭৫-এ শেষ করে। শুধু artifact বাদ, আর তার কারণ হিসেবে দেখায় শুধু security (lint আর integration সফল)। Serial সময় ৫ + ৪০ + ২০ + ৬০ + ৯০ + ৪৫ + ১৮০ = ৪৪০. বাস্তব শিক্ষা: ৯০ সেকেন্ডেই জানা গেছে pipeline লাল হবে, তবু runner আরও প্রায় পাঁচ মিনিট integration চালাল। অনেক দল তাই “যেকোনো job লাল হলে পুরো run বাতিল” নিয়ম যোগ করে — দ্রুত খবর আর কম খরচ, বিনিময়ে integration-এর ফল অজানা থাকে।

  4. সমালোচনা আর নকশা: একটি কাল্পনিক বিশ্ববিদ্যালয়ের ফলাফল পোর্টালের দল বলছে “আমাদের CI আছে”। বিবরণ: feature branch গড়ে ১৮ দিন বাঁচে; pipeline চলে শুধু রাত ২টায়; গত সপ্তাহে main চার দিন লাল ছিল, কেউ দেখেননি; build ৪৫ মিনিট; YAML-এ DB_PASSWORD: "…" সরাসরি লেখা; action-গুলো @main-এ; লেবেল বসানোর একটি workflow pull_request_target-এ PR-এর কোড checkout করে npm install চালায়; test step-এ npm test || true. সমস্যাগুলো চিহ্নিত করুন আর অগ্রাধিকারসহ একটি উন্নতির পরিকল্পনা দিন।

    এটি CI theatre-এর প্রায় সব লক্ষণ। অগ্রাধিকার — আগে নিরাপত্তা আর মিথ্যা সংকেত, তারপর অভ্যাস, তারপর গতি:

    • এখনই (নিরাপত্তা): YAML-এর password ফাঁস ধরে নিন — বদলান (rotate), secret store-এ রাখুন, ইতিহাস পরিষ্কার পর্ব ৫৭-এর পদ্ধতিতে। pull_request_target-এ PR-এর কোড checkout করে npm install মানে বাইরের যে কেউ secret-এর পাশে নিজের install script চালাতে পারেন — workflow বন্ধ করুন, লেবেলের কাজ PR-এর কোড না চালিয়ে। @main-এর বদলে পূর্ণ SHA, আর permissions: contents: read.
    • এই সপ্তাহে (সত্যিকারের সংকেত): || true সরান — অনেক test লাল দেখালে সেটিই সত্য; মালিক আর সময়সীমাসহ তালিকা (flaky হলে quarantine). Pipeline প্রতিটি push আর PR-এ, রাতে শুধু ধীর suite; required check আর branch protection.
    • অভ্যাস: লাল main প্রথম কাজ, দশ মিনিটে নিশ্চিত fix না হলে revert; কাজ এক-দুই দিনের PR-এ ভাঙা আর feature flag — branch-এর আয়ু ১৮ দিন থেকে ২ দিনের নিচে।
    • গতি আর মাপ: ৪৫ মিনিটের ভাঙন মাপুন; cache, critical path, sharding, ধীর E2E রাতে। প্রথম লক্ষ্য ১৫ মিনিট, তারপর ১০. প্রতি সপ্তাহে failure rate, time to fix, p50/p90 আর queue time.

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি সাধারণ CI pipeline-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. ⚡ Trigger: PR-এ নতুন commit push
  2. 📥 Checkout আর cache থেকে dependency
  3. 🧹 Lint আর build (সমান্তরাল)
  4. 🧪 Unit test (matrix)
  5. 🔍 Integration test আর security scan
  6. ✅ Artifact জমা আর PR-এ status check

একটি দলে প্রতিটি feature branch-এ স্বয়ংক্রিয় build আর test চলে, আর dashboard সবসময় সবুজ। কিন্তু branch-গুলো গড়ে তিন সপ্তাহ পরে main-এ মেলে। Continuous integration-এর মূল সংজ্ঞা অনুযায়ী কোন কথাটি সবচেয়ে ঠিক?

  • দলটি পুরোপুরি CI করছে, কারণ প্রতিটি commit-এ test চলে
  • দলটি CI করছে না, কারণ nightly build নেই
  • পুরো অর্থে CI নয় — mainline-এ মেলানো তিন সপ্তাহে একবার, তাই integration-এর ঝুঁকি branch-এ জমছে
  • CI নয়, কারণ CI-এর জন্য GitHub Actions লাগে

একটি pipeline-এ: install-এর পরে lint, build আর scan — তিনটিই শুধু install-এর ওপর নির্ভরশীল; unit নির্ভর করে build-এর ওপর; package নির্ভর করে lint, unit আর scan-এর ওপর। Build ব্যর্থ হলো। সাধারণ নিয়মে (নির্ভরশীল job বাদ পড়ে, স্বাধীন job চলতে থাকে) কী ঘটবে?

  • সব job সাথে সাথে বাতিল
  • Lint আর scan চলে; unit আর package বাদ পড়ে
  • Lint, scan আর package চলে; শুধু unit বাদ
  • Unit চলে, কিন্তু ফল উপেক্ষিত হয়

একটি GitHub Actions matrix-এ os-এর ৩টি মান আর python-এর ৪টি মান। exclude-এ ২টি জোড়া বাদ; include-এ একটি বস্তু, যার os আর python কোনো বিদ্যমান জোড়ার সাথে মেলে না। আরেকটি include বস্তু একটি বিদ্যমান জোড়ার সাথে মিলে শুধু একটি বাড়তি key যোগ করে। মোট কয়টি job?

  • ১২
  • ১০
  • ১২ (১০টির সাথে দুটি include-এর দুটি নতুন job)
  • ১১

একটি CI-তে dependency cache-এর key = linux-npm- + package-lock.json-এর hash. কোন ঘটনায় পরের run-এ key বদলাবে?

  • একটি dependency-র version বদলে lockfile হালনাগাদ হলে
  • App-এর source code-এর যেকোনো file বদলালে
  • প্রতিটি নতুন commit-এ, কারণ commit hash বদলায়
  • README.md বদলালে

একটি workflow-এ uses: someone/deploy-tool@v3 লেখা। একজন নিরাপত্তা-পর্যালোচক একে @ আর একটি পূর্ণ ৪০ অক্ষরের commit hash দিয়ে বদলাতে বললেন। এর প্রধান কারণ কী?

  • SHA দিয়ে action দ্রুত নামে
  • SHA দিলে নতুন version নিজে থেকেই আসে
  • Tag সরানো যায়, কিন্তু commit hash একটি নির্দিষ্ট কনটেন্টকে চিহ্নিত করে — কেউ tag ক্ষতিকর commit-এ সরালেও pipeline একই কোড চালাবে
  • SHA দিলে secret log-এ আরও ভালো ঢাকা পড়ে

সকাল ১০:০৮-এ একটি দলের main-এর CI লাল হলো একটি নির্দিষ্ট commit-এর পরে; একই commit-এ আবার চালালেও লাল। লেখক একটি meeting-এ, আর সঠিক fix কী, তা এখনো নিশ্চিত নয়। প্রচলিত CI অনুশীলনে সবচেয়ে ভালো প্রথম পদক্ষেপ কোনটি?

  • লেখক ফিরলে ঠিক করবেন; ততক্ষণ অন্যরা স্বাভাবিকভাবে merge চালিয়ে যান
  • নতুন merge থামিয়ে ভাঙা commit-টি git revert করে main সবুজ করা, তারপর branch-এ সমাধান
  • git reset --hard দিয়ে main আগের commit-এ নিয়ে git push --force
  • ব্যর্থ test-টি সাময়িকভাবে বন্ধ (skip) করে দেওয়া

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. CI = অনুশীলন: সবাই অন্তত দিনে একবার mainline-এ ছোট পরিবর্তন, প্রতিটিতে স্বয়ংক্রিয় build ও test, আর ভাঙলে সাথে সাথে ঠিক (Booch ১৯৯১-এর শব্দ, XP ১৯৯৯-এর অনুশীলন, Fowler ২০০০/২০০৬-এর তালিকা)। Tool একটি অংশ মাত্র।
  2. Pipeline = DAG: trigger, job, step, stage, runner; সস্তা check আগে; ব্যর্থ need-এর নির্ভরশীল job বাদ (fail fast); মোট সময় = critical path. প্রতিটি আলাদা job নিজের setup-এর খরচ দেয়।
  3. গতির কৌশল: matrix (OS × version, exclude/include), sharding, parallelism, lockfile-এর hash-এ cache key. Cache গতির জন্য, না থাকলেও ফল একই; artifact এক job-এর ফল পরের job-এ পাঠায়। নির্দেশিকা মোটামুটি দশ মিনিট।
  4. নিরাপত্তা: secret store আর masked variable (echo নয়), ন্যূনতম অনুমতি, দীর্ঘমেয়াদি key-এর বদলে OIDC, fork-এর PR আর pull_request_target-এ সাবধান, তৃতীয় পক্ষের action পুরো SHA-তে pin.
  5. Main সবুজ: required check, merge queue, লাল হলে stop the line আর আগে revert; hook আর CI একই script চালায়; মাপুন failure rate, time to fix, p50/p90, queue time — আর CI theatre চিনুন।

পরের পর্বে: উন্নত Git আর automation-এর মডিউল এখানে শেষ। পর্ব ৫৯-এ CI-এর পরের ধাপ — continuous delivery আর DevOps: সবুজ build কীভাবে deployment pipeline বেয়ে environment থেকে environment-এ যায়, আর DORA-র চারটি মেট্রিক দিয়ে delivery-র স্বাস্থ্য কীভাবে মাপা হয়।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান