পাঠ ৫৩ · ৫৮-এর মধ্যে · মডিউল ১২
Home / Courses / Software Engineering Principles & Git / DevOps ও CI/CD প্রসেস

কন্টিনিউয়াস ইন্টিগ্রেশন প্রিন্সিপল

Continuous integration principles
৮ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • কন্টিনিউয়াস ইন্টিগ্রেশন কী, এবং কেন "ঘন-ঘন ইন্টিগ্রেশন + স্বয়ংক্রিয় ফিডব্যাক" এত শক্তিশালী একটি নীতি
  • বিল্ড স্ট্যাটাস ধারণা এবং "stop the line" দলগত শৃঙ্খলা
  • একটি CIPipeline সিমুলেশন যা প্রকৃতপক্ষে একটি রিয়েল রিগ্রেশন ধরে ফেলে
  • git bisect-স্টাইল বাইনারি সার্চ দিয়ে ঠিক কোন কমিট বিল্ড ভাঙল তা খুঁজে বের করা

১ · Continuous Integration কী

Continuous IntegrationCIকোড পরিবর্তন খুব ঘন-ঘন একটি শেয়ার্ড ব্রাঞ্চে ইন্টিগ্রেট করার প্র্যাক্টিস, প্রতিটি ইন্টিগ্রেশনে স্বয়ংক্রিয়ভাবে বিল্ড ও পুরো টেস্ট স্যুট চালিয়ে। (CI) হলো M9/L42-এ শেখা ট্রাংক-বেসড ডেভেলপমেন্ট দর্শনের প্রত্যক্ষ প্রযুক্তিগত বাস্তবায়ন — কোড পরিবর্তন খুব ঘন-ঘন একটি শেয়ার্ড ব্রাঞ্চে ইন্টিগ্রেট (মার্জ) করা, আর প্রতিটি ইন্টিগ্রেশনেই স্বয়ংক্রিয়ভাবে একটি বিল্ড ও পুরো অটোমেটেড টেস্ট স্যুট (M10) চালানো। ../cloud-devops/Cloud Computing & DevOpsআসল পাইপলাইন-কনফিগারেশন ফাইল (YAML), নির্দিষ্ট CI প্ল্যাটফর্ম (GitHub Actions, Jenkins ইত্যাদি) ও কন্টেইনার/অর্কেস্ট্রেশন — এই কোর্সে গভীরভাবে কভার হয়। কোর্স CI-এর আসল পাইপলাইন-কনফিগারেশন মেকানিক্স গভীরভাবে কভার করে — এই পাঠ শুধু "কেন" ও মূলনীতি কভার করে।

২ · মূল নীতি — ঘন-ঘন ইন্টিগ্রেশন কেন কাজ করে

কোড পরিবর্তন যত বেশি সময় একটি আলাদা, অ-ইন্টিগ্রেটেড ব্রাঞ্চে পড়ে থাকে, তত বেশি সম্ভাবনা থাকে একটি বেদনাদায়ক মার্জ কনফ্লিক্ট (M8/L36) তৈরি হওয়ার — এবং দুইজন ডেভেলপারের পরিবর্তনের মধ্যে একটি লুকানো, ক্ষতিকর interaction তত বেশি সময় ধরে অজানা থাকে। CI ঠিক এই দুটো সমস্যাই সরাসরি আক্রমণ করে — ঘন-ঘন ইন্টিগ্রেশন বাধ্য করার মাধ্যমে, সাথে অবিলম্বে স্বয়ংক্রিয় ফিডব্যাক দিয়ে (M10/L48-এর অটোমেটেড টেস্টিং-এর গতি ও পুনরাবৃত্তিযোগ্যতার সুবিধার প্রত্যক্ষ ব্যবহার)। বিল্ড/টেস্ট ইন্টিগ্রেশনের পর ফেইল করলে, টিম প্রায়ই মিনিটের মধ্যে জানতে পারে — সপ্তাহ পরে আবিষ্কার হওয়ার বদলে, যখন কারণ খুঁজে বের করা অনেক কঠিন হয়ে যায়।

বিল্ড স্ট্যাটাস ও "stop the line"

একটি CI সিস্টেম সবসময় শেয়ার্ড ব্রাঞ্চের একটি বর্তমান পাস/ফেইল বিল্ড স্ট্যাটাস ধরে রাখে — একটি গুরুত্বপূর্ণ, বাস্তব দলগত শৃঙ্খলা হলো: ফেইল হওয়া বিল্ডকে জরুরি অগ্রাধিকার হিসেবে ঠিক করা, যাকে উৎপাদন-শিল্প থেকে ধার করা "stop the line" বলা হয় — কারণ ভাঙা বিল্ডের উপর প্রতিটি নতুন কমিট ঠিক কোন পরিবর্তনটি সমস্যা তৈরি করেছে তা আলাদা করা ক্রমেই কঠিন করে তোলে।

৩ · CIPipeline সিমুলেশন — একটি রিয়েল রিগ্রেশন ধরা

নিচের কোড সেলে একটি ছোট শপিং-কার্ট টোটাল-ক্যালকুলেশন ফাংশনের ৪টি সংস্করণ (৪টি কমিট) সিমুলেট করা হয়েছে — c1 সঠিক বেস ইমপ্লিমেন্টেশন, c2 একটি ডিসকাউন্ট ফিচার যোগ করে (এখনও সঠিক), c3 রিফ্যাক্টর করতে গিয়ে ভুলবশত *-এর বদলে + ব্যবহার করে ফেলে (একটি রিয়েল রিগ্রেশন), c4 সেটি ঠিক করে। প্রতিটি কমিটে CIPipeline ক্রমবর্ধমান টেস্ট স্যুট চালায় (M10/L48-এর রিগ্রেশন-সুইট প্যাটার্নের সরাসরি পুনর্ব্যবহার) এবং একটি সত্যিকারের পাস/ফেইল স্ট্যাটাস রেকর্ড করে।

Python
class CIPipeline:
    def __init__(self):
        self.history = []  # প্রতিটি কমিটের (commit_label, status, failing_tests) রেকর্ড

    def run_build(self, commit_label, build_fn, tests):
        failing = []
        for test in tests:
            actual = build_fn(test["cart"])
            if actual != test["expected"]:
                failing.append((test["name"], test["expected"], actual))
        status = "FAILED" if failing else "PASSED"
        self.history.append({"commit": commit_label, "status": status, "failing": failing})
        passed_count = len(tests) - len(failing)
        print(f"  CI [{commit_label}]: বিল্ড {status}  ({passed_count}/{len(tests)} টেস্ট পাস)")
        for name, expected, actual in failing:
            print(f"      ✗ {name} -- expected={expected}, actual={actual}")
        return status

# ---- চারটি কমিটের বিল্ড ফাংশন ----
def build_v1(cart):  # c1: সঠিক বেস ইমপ্লিমেন্টেশন
    return sum(item["price"] * item["quantity"] for item in cart)

def build_v2(cart):  # c2: ১০০-এর উপরে অর্ডারে ১০ টাকা ডিসকাউন্ট ফিচার যোগ (এখনও সঠিক)
    total = sum(item["price"] * item["quantity"] for item in cart)
    if total > 100:
        total -= 10
    return total

def build_v3(cart):  # c3: রিফ্যাক্টরে ভুলবশত * এর বদলে + লেখা হয়েছে -- REGRESSION
    total = sum(item["price"] + item["quantity"] for item in cart)
    if total > 100:
        total -= 10
    return total

def build_v4(cart):  # c4: ফিক্স -- সঠিক গুণ পুনরুদ্ধার
    total = sum(item["price"] * item["quantity"] for item in cart)
    if total > 100:
        total -= 10
    return total

test_basic_total = {"name": "test_basic_total", "cart": [{"price": 20, "quantity": 3}, {"price": 15, "quantity": 2}], "expected": 90}
test_discount_applied = {"name": "test_discount_applied", "cart": [{"price": 50, "quantity": 3}], "expected": 140}

ci = CIPipeline()
ci.run_build("c1", build_v1, [test_basic_total])
ci.run_build("c2", build_v2, [test_basic_total, test_discount_applied])
ci.run_build("c3", build_v3, [test_basic_total, test_discount_applied])
ci.run_build("c4", build_v4, [test_basic_total, test_discount_applied])

print()
print("বিল্ড হিস্ট্রি:", [h["status"] for h in ci.history])

# ---- git bisect-স্টাইল বাইনারি সার্চ: ঠিক কোন কমিট প্রথম ভাঙল তা খোঁজা ----
def find_first_regression(statuses, good_index, bad_index):
    while bad_index - good_index > 1:
        mid = (good_index + bad_index) // 2
        if statuses[mid] == "PASSED":
            good_index = mid
        else:
            bad_index = mid
    return bad_index

statuses = [h["status"] for h in ci.history]
culprit_index = find_first_regression(statuses, good_index=0, bad_index=2)
print(f"\ngit bisect-স্টাইল সার্চ: known-good=c1 (index 0), known-bad=c3 (index 2)")
print(f"বাইনারি সার্চে চিহ্নিত প্রথম রিগ্রেশন: c{culprit_index + 1}  (স্ট্যাটাস: {statuses[culprit_index]})")

    
হাতে-যাচাই: c1-এ 20×3 + 15×2 = 90, প্রত্যাশিত ৯০ — পাস। c2-তে একই কার্ট + ডিসকাউন্ট কার্ট 50×3=150 > 100 তাই 150-10=140, প্রত্যাশিত ১৪০ — পাস। c3-এ ভুল সূত্র 20+3 + 15+2 = 40 (প্রত্যাশিত ৯০-এর সাথে মেলে না) এবং 50+3=53 (প্রত্যাশিত ১৪০-এর সাথে মেলে না) — দুটোই ফেইল, বিল্ড সঠিকভাবে FAILED হিসেবে চিহ্নিত হয়। c4 আবার সঠিক গুণ ফিরিয়ে আনায় দুটোই পাস করে। বাইনারি সার্চ: mid = (0+2)//2 = 1 (c2), স্ট্যাটাস PASSED, তাই good_index=1; এখন bad_index(2) - good_index(1) = 1, লুপ থামে, ফলাফল index 2 = c3 — সঠিকভাবে দোষী কমিট শনাক্ত হয়েছে।
মূল কথা · Key takeaway

CI-এর মূল শক্তি হলো ঘন-ঘন ইন্টিগ্রেশন + তাৎক্ষণিক স্বয়ংক্রিয় ফিডব্যাক — এই সংমিশ্রণ একটি রিগ্রেশনকে মিনিটের মধ্যে ধরে ফেলে, সপ্তাহ পরে নয়। আর যখন বিল্ড ভাঙে, git bisect-স্টাইল বাইনারি সার্চ দ্রুত ঠিক কোন কমিট দোষী তা খুঁজে বের করতে সাহায্য করে — লিনিয়ার সার্চের বদলে লগারিদমিক সময়ে।

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

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

প্র ০১ যদি একটি টিম মাসে একবার সব ব্রাঞ্চ একসাথে মার্জ করে (CI না ব্যবহার করে), রিগ্রেশন ধরতে কেমন সমস্যা হতে পারে?

মাসে একবার মার্জ করলে, একসাথে অনেক ডেভেলপারের বহু পরিবর্তন একবারে ইন্টিগ্রেট হয় — যদি কোনো রিগ্রেশন দেখা দেয়, ঠিক কোন পরিবর্তনটি দায়ী তা খুঁজে বের করা অনেক কঠিন হয়ে যায় (উপরের উদাহরণে মাত্র ৪টি কমিটেই বাইনারি সার্চ দরকার হলো — শত শত কমিট একসাথে ইন্টিগ্রেট হলে সমস্যা আরও গুরুতর)। এছাড়া মার্জ কনফ্লিক্টও (M8/L36) অনেক বড় ও জটিল হওয়ার সম্ভাবনা থাকে, কারণ ব্রাঞ্চগুলো দীর্ঘ সময় ধরে ডাইভার্জ করেছে।

প্র ০২ "stop the line" নিয়মটি বাস্তবে কেন গুরুত্বপূর্ণ — একটি ভাঙা বিল্ডের উপর কাজ চালিয়ে যাওয়া কেন ঝুঁকিপূর্ণ?

একটি ভাঙা বিল্ডের উপর যদি নতুন কমিট চলতে থাকে, তাহলে প্রতিটি নতুন কমিট সেই ভাঙা অবস্থার উপর ভিত্তি করে কাজ করে — ফলে পরে যখন সমস্যাটি ঠিক করতে বসা হয়, ঠিক কোন কমিট আসল কারণ তা আলাদা করা কঠিন হয়ে যায় (উপরের উদাহরণে c3 রিগ্রেশন আনার পরপরই বাইনারি সার্চ চালানো সহজ ছিল কারণ মাত্র একটি নতুন কমিট ছিল)। তাই বিল্ড ভাঙলে অবিলম্বে ঠিক করাকে অগ্রাধিকার দেওয়া, বাকি সব কাজ সাময়িকভাবে থামিয়ে হলেও, দীর্ঘমেয়াদে সময় বাঁচায়।

প্র ০৩ উপরের বাইনারি সার্চে known-good হিসেবে c1 আর known-bad হিসেবে c3 ব্যবহার করা হলো, c4 নয় কেন?

কারণ বাইনারি সার্চ কাজ করে ধরে নিয়ে যে known-good ও known-bad-এর মাঝে ঠিক একটি পয়েন্টে অবস্থা PASSED থেকে FAILED-এ পরিবর্তিত হয়েছে (মনোটোনিক রূপান্তর)। c1→c2→c3-এর মধ্যে এই ধারা সত্যি (PASS, PASS, FAIL)। কিন্তু c4 আবার PASSED-এ ফিরে গেছে (ফিক্স করা হয়েছে বলে) — c1 থেকে c4 পর্যন্ত সার্চ করলে ধারাটি PASS,PASS,FAIL,PASS হয়ে যেত, যা মনোটোনিক নয়, তাই বাইনারি সার্চের অনুমান ভেঙে যেত। বাস্তব git bisect-এও ঠিক এই কারণে known-bad হিসেবে সবচেয়ে সাম্প্রতিক নিশ্চিতভাবে-ভাঙা কমিটই বেছে নেওয়া হয়।

অনুশীলন

  1. চিন্তা করুন: ধরুন আরও একটি কমিট c0 ছিল c1-এরও আগে, যেখানে টেস্ট স্যুটই লেখা হয়নি (তাই বিল্ড চালানো যেত না)। এটি CI-এর দৃষ্টিকোণ থেকে কেন সমস্যাজনক হতে পারে?

    টেস্ট স্যুট ছাড়া CI পাইপলাইন কার্যত অন্ধ — বিল্ড হয়তো "সফলভাবে" চলবে, কিন্তু কোনো লজিক আসলে সঠিক কিনা তা যাচাই করার কোনো উপায় থাকবে না। CI-এর পুরো মূল্য নির্ভর করে একটি অর্থপূর্ণ, ব্যাপক অটোমেটেড টেস্ট স্যুটের (M10) উপর — শুধু "বিল্ড কম্পাইল হয়েছে" যথেষ্ট নয়, "বিল্ড সঠিক আচরণ করছে" নিশ্চিত করতে হয়।

  2. পরীক্ষা করুন: উপরের কোড সেলে build_v3-এ নতুন করে একটি দ্বিতীয় বাগ যোগ করুন (যেমন ডিসকাউন্টের শর্ত total > 100-কে total > 200 করে দিন) এবং Run চেপে দেখুন c3-এর ফেইলিং টেস্টের তালিকা কীভাবে বদলায়।

    দ্বিতীয় বাগ যোগ করলে test_discount_applied-এর ব্যর্থতার কারণও বদলাবে (এখন ডিসকাউন্টই প্রয়োগ হবে না, তাই actual মান আরও ভিন্ন হবে), কিন্তু বিল্ড স্ট্যাটাস আগে থেকেই FAILED ছিল বলে সামগ্রিক ফলাফলে (PASSED/FAILED) কোনো পরিবর্তন হবে না — শুধু failing তালিকায় দেখানো actual মান বদলাবে। এটি দেখায় কেন "stop the line" গুরুত্বপূর্ণ — একটি ভাঙা বিল্ডের উপর একাধিক বাগ জমা হতে দিলে প্রতিটি আলাদা করে চিহ্নিত করা ক্রমেই কঠিন হয়।

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

আগের পাঠ
কোড রিভিউ বেস্ট প্র্যাক্টিস