পাঠ ৫৩ · ৫৭-এর মধ্যে · মডিউল ১২

মোবাইল অ্যাপের জন্য CI/CD

CI/CD for mobile apps
৯ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • মোবাইল CI/CD-এর স্টেজ ক্রম এবং ওয়েব CI/CD (FSWF L53) থেকে এর নির্দিষ্ট পার্থক্য — দ্বৈত আর্টিফ্যাক্ট
  • early-stop-on-failure আচরণ একটি সত্যিকারের পাইপলাইন-রানারে বাস্তবায়ন করা
  • call-count যাচাইয়ের মাধ্যমে প্রমাণ করা skip হওয়া স্টেজ সত্যিই একবারও চলে না
  • সব স্টেজ সফল হওয়া এবং একটি স্টেজে ইনজেক্টেড ব্যর্থতা — দুটো পরিস্থিতির প্রকৃত ফলাফলের পার্থক্য পড়া

১ · মোবাইল CI/CD স্টেজ ক্রম

FSWF কোর্সের L53-এ শেখানো হয়েছে কেন CI/CD পাইপলাইনের স্টেজগুলো একটি নির্দিষ্ট ক্রমে চলে এবং একটি ব্যর্থতায় বাকি স্টেজ কেন সম্পূর্ণ এড়িয়ে যাওয়া উচিত — সেই সাধারণ নীতিটি এখানেও অপরিবর্তিত। মোবাইলের নির্দিষ্ট পার্থক্যটা build স্টেজে — ওয়েবে সাধারণত একটি একক ডিপ্লয়যোগ্য আর্টিফ্যাক্ট (একটি কন্টেইনার ইমেজ বা বান্ডেল) তৈরি হয়, কিন্তু মোবাইলে একই কোডবেস থেকে দুটো সম্পূর্ণ আলাদা প্ল্যাটফর্ম-নির্দিষ্ট আর্টিফ্যাক্ট তৈরি করতে হয় — iOS-এর জন্য একটি .ipa ফাইল ও Android-এর জন্য একটি .apk ফাইল, যাদের সাইনিং সার্টিফিকেট ও বিল্ড টুলচেইনও আলাদা।

lint unit_test build_ipa_apk beta_distribute কোড স্টাইল চেক ইউনিট টেস্ট স্যুট .ipa + .apk তৈরি TestFlight/Play বেটা
unit_test ব্যর্থ হলে build_ipa_apk ও beta_distribute সম্পূর্ণ skip হয়ে যায় -- অর্থাৎ কোনো iOS/Android আর্টিফ্যাক্টই তৈরি হয় না, এবং টেস্টারদের কাছে কিছুই বিতরণ করা হয় না।
lint
কোড স্টাইল ও সাধারণ ভুল দ্রুত ধরে — সবচেয়ে সস্তা চেক, প্রথমে চলে।
unit_test
দ্রুত ইউনিট টেস্ট স্যুট চালিয়ে বিজনেস লজিক আচরণগতভাবে সঠিক কি না যাচাই করে (L50/L51-এর ধারাবাহিকতা)।
build_ipa_apk
একই কোডবেস থেকে দুটো আলাদা সাইনড আর্টিফ্যাক্ট তৈরি করে — iOS .ipa ও Android .apk।
beta_distribute
বৈধ আর্টিফ্যাক্ট দুটো বেটা-টেস্টারদের কাছে পাঠায় (যেমন TestFlight/Play বেটা ট্র্যাক) — শেষ স্টেজ, সব আগের স্টেজ সফল হলেই এখানে পৌঁছায়।

২ · একটি সত্যিকারের পাইপলাইন রানার — early-stop ও call-count যাচাই

নিচের কোড সেলে প্রতিটি স্টেজ একটি real ফাংশন — কল হলে নিজের call_counts কাউন্টার বাড়ায়, এবং একটি ইনজেক্ট করা fail_stages সেটে নিজের নাম থাকলে ব্যর্থ হয়। build_ipa_apk সফল হলে এটি সত্যিই artifacts ডিকশনারিতে দুটো আর্টিফ্যাক্ট-নাম লিখে দেয় — যদি এই স্টেজ কখনো না চলে, সেই ডিকশনারিও খালি থেকে যাবে, যা আমরা সরাসরি যাচাই করব।

Python
STAGE_ORDER = ["lint", "unit_test", "build_ipa_apk", "beta_distribute"]

def make_stage(stage_name, fail_stages, call_counts, artifacts):
    """একটি স্টেজ ফাংশন তৈরি করে দেয়। কল হলে call_counts[stage_name] বাড়ায় --
    তাই এটি কতবার কল হয়েছে তা পরে যাচাইযোগ্য। build_ipa_apk সফল হলে
    সত্যিই দুটো প্ল্যাটফর্ম-নির্দিষ্ট আর্টিফ্যাক্ট artifacts-এ লিখে দেয়।"""
    def stage_fn():
        call_counts[stage_name] += 1
        succeeded = stage_name not in fail_stages
        if stage_name == "build_ipa_apk" and succeeded:
            artifacts["ios"] = "app-release.ipa"
            artifacts["android"] = "app-release.apk"
        return succeeded
    return stage_fn

def run_pipeline(fail_stages):
    """STAGE_ORDER অনুযায়ী প্রতিটি স্টেজ ক্রমানুসারে চালায়। কোনো স্টেজ ব্যর্থ হলে
    সাথে সাথে থেমে যায় -- পরের স্টেজগুলোর ফাংশন একবারও কল করা হয় না।
    রিটার্ন করে: (results, call_counts, failed_at, artifacts)।"""
    call_counts = {name: 0 for name in STAGE_ORDER}
    artifacts = {}
    stages = {name: make_stage(name, fail_stages, call_counts, artifacts) for name in STAGE_ORDER}

    results = {}
    failed_at = None
    for name in STAGE_ORDER:
        if failed_at is not None:
            results[name] = "SKIPPED"
            continue                      # এই স্টেজের ফাংশন কখনো কল করা হচ্ছে না
        succeeded = stages[name]()
        results[name] = "SUCCESS" if succeeded else "FAILED"
        if not succeeded:
            failed_at = name

    return results, call_counts, failed_at, artifacts

def print_report(label, fail_stages):
    results, call_counts, failed_at, artifacts = run_pipeline(fail_stages)
    print(f"=== {label} ===")
    for name in STAGE_ORDER:
        print(f"  {name:16s} -> {results[name]:8s}  (call_count={call_counts[name]})")
    print(f"  আর্টিফ্যাক্ট: iOS={artifacts.get('ios', '--')}, Android={artifacts.get('android', '--')}")
    if failed_at is None:
        print("  ফলাফল: পাইপলাইন সবুজ -- সব স্টেজ সফল, বেটা টেস্টারদের কাছে বিতরণ সম্পন্ন\n")
    else:
        print(f"  ফলাফল: '{failed_at}' স্টেজে ব্যর্থতার কারণে থেমে গেছে -- পরের স্টেজগুলো কখনো চালানো হয়নি\n")
    return call_counts, artifacts

# রান ১ -- সব স্টেজ পাস
print_report("রান ১ -- সব স্টেজ পাস", fail_stages=set())

# রান ২ -- 'unit_test' স্টেজে ইচ্ছাকৃতভাবে ব্যর্থতা ইনজেক্ট করা হলো
call_counts_run2, artifacts_run2 = print_report(
    "রান ২ -- 'unit_test' স্টেজে ইনজেক্টেড ব্যর্থতা", fail_stages={"unit_test"}
)

# --- যাচাই: skip করা build_ipa_apk ও beta_distribute সত্যিই কখনো কল হয়নি ---
assert call_counts_run2["build_ipa_apk"] == 0, "build_ipa_apk স্টেজ ভুলবশত কল হয়ে গেছে!"
assert call_counts_run2["beta_distribute"] == 0, "beta_distribute স্টেজ ভুলবশত কল হয়ে গেছে!"
assert artifacts_run2 == {}, "কোনো আর্টিফ্যাক্ট তৈরি হওয়ার কথা ছিল না, কিন্তু artifacts dict খালি নয়!"
print(
    f"যাচাই সফল: রান ২-এ build_ipa_apk এর call_count={call_counts_run2['build_ipa_apk']}, "
    f"beta_distribute এর call_count={call_counts_run2['beta_distribute']} -- অর্থাৎ এই দুটো স্টেজের "
    f"ফাংশন সত্যিই একবারও চালানো হয়নি, এবং কোনো iOS/Android আর্টিফ্যাক্টও তৈরি হয়নি।"
)

    
রান ১-এ fail_stages খালি, তাই প্রতিটি স্টেজ কল হয়ে সফল হয় — build_ipa_apk চলার সময় সত্যিই artifacts["ios"] ও artifacts["android"] সেট হয়ে যায়, এবং শেষ রিপোর্টে দুটো আর্টিফ্যাক্টের নামই দেখা যায়। রান ২-এ unit_test কল হওয়ার সময় (তার call_count ১-এ যায়) এটি False রিটার্ন করে, সাথে সাথে failed_at = "unit_test" সেট হয়ে যায় — লুপের পরের ধাপগুলোতে (build_ipa_apk, beta_distribute) if failed_at is not None শর্ত সত্যি হওয়ায় সেই স্টেজের ফাংশন একবারও কল করা হয় না, তাই artifacts-এ লেখার কোড লাইনটিও কখনো চলার সুযোগ পায় না — artifacts_run2 খালি dict থেকে যায়, যা তৃতীয় assert-টি নিশ্চিত করে।
মূল কথা · Key takeaway

মোবাইল CI/CD-এর গঠনগত নীতি ওয়েবের মতোই — early-stop-on-failure — কিন্তু build স্টেজে দুটো আলাদা প্ল্যাটফর্ম-নির্দিষ্ট আর্টিফ্যাক্ট (iOS .ipa, Android .apk) তৈরি করাটাই মোবাইলের নির্দিষ্ট সংযোজন। উপরের কোডে দেখানো হয়েছে যে early-stop নিছক প্রিন্ট করা বার্তা নয় — skip হওয়া স্টেজের ফাংশন সত্যিই কল হয় না, এবং সেই স্টেজ যা তৈরি করার কথা ছিল (এখানে: দুটো আর্টিফ্যাক্ট) তাও কখনো তৈরি হয় না — real call-count ও real dict state দিয়ে যাচাইযোগ্য।

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

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

প্র ০১ যদি build_ipa_apk-এর ভেতরের কোড ভুলবশত iOS আর্টিফ্যাক্ট তৈরির আগেই artifacts["android"] সেট করে দিত এবং তারপর ব্যর্থ হতো, তাহলে বাস্তবে কী ঝুঁকি হতে পারত?

beta_distribute স্টেজ (যদি চলার সুযোগ পেত) হয়তো একটি অসম্পূর্ণ Android আর্টিফ্যাক্ট বেটা টেস্টারদের কাছে পাঠিয়ে দিত, যখন iOS সংস্করণ কখনো তৈরিই হয়নি — একটি অসামঞ্জস্যপূর্ণ রিলিজ। এই কোর্সের পাইপলাইনে যেহেতু early-stop beta_distribute-কে সম্পূর্ণ skip করে দেয়, এই ঝুঁকিটা এড়ানো যায় — কিন্তু এটি মনে করিয়ে দেয় যে একটি স্টেজের ভেতরের আংশিক-সফল কাজ পরিষ্কারভাবে হ্যান্ডল করাও গুরুত্বপূর্ণ।

প্র ০২ যদি fail_stages = {"lint", "beta_distribute"} দিয়ে পাইপলাইন চালানো হতো, তাহলে beta_distribute-এর call_count কত হতো, আর কেন?

0 হতো — কারণ lint নিজেই প্রথম স্টেজ, আর "lint" in fail_stages সত্যি হওয়ায় এটি প্রথম কলেই ব্যর্থ হবে (call_count["lint"] = 1) এবং failed_at = "lint" সেট হয়ে যাবে। এরপর unit_test, build_ipa_apk, beta_distribute — সবগুলো "SKIPPED" হবে এবং কখনো কল হবে না — যদিও beta_distribute নিজে fail_stages-এ ছিল, তার ফাংশন কল হওয়ারই সুযোগ পায়নি।

প্র ০৩ কেন build_ipa_apk স্টেজে iOS ও Android আর্টিফ্যাক্ট একই ফাংশনে একসাথে তৈরি করা হচ্ছে, আলাদা build_ios/build_android স্টেজ না রেখে?

বাস্তব CI সিস্টেমে দুটো পদ্ধতিই দেখা যায় — এই পাঠে সরলতার জন্য একটি স্টেজে একত্রিত করা হয়েছে, কিন্তু বড় টিমে প্রায়ই দুটো প্ল্যাটফর্মের বিল্ড আলাদা করা হয় যাতে তারা সমান্তরালভাবে চলতে পারে (একটি iOS রানার ও একটি Android রানার একই সাথে কাজ করে, সিরিয়ালি নয়) — এটি মোট পাইপলাইন সময় কমায়। তবে মূল নীতি অপরিবর্তিত থাকে: আগের সব স্টেজ সফল না হলে কোনো বিল্ড স্টেজই শুরু হবে না।

অনুশীলন

  1. চিন্তা করুন: কেন lint সাধারণত পাইপলাইনের প্রথম স্টেজ, আর beta_distribute সবসময় শেষ স্টেজ হয় বলে মনে হয়?

    lint সবচেয়ে সস্তা ও দ্রুততম চেক — কোনো কম্পাইল/বিল্ড ছাড়াই কোড স্ক্যান করা যায়, তাই এটি প্রথমে চালালে ব্যর্থতা দ্রুততম সময়ে ও কম খরচে ধরা পড়ে। beta_distribute সবচেয়ে ঝুঁকিপূর্ণ ও অপরিবর্তনীয় কাজ — একবার টেস্টারদের কাছে একটি বিল্ড পাঠানো হলে সেটি প্রত্যাহার করা কঠিন, তাই এটি তখনই চলা উচিত যখন lint, unit_test, ও build_ipa_apk — সবকটি স্টেজ নিশ্চিত করেছে বিল্ডটি বৈধ।

  2. পরীক্ষা করুন: উপরের কোড সেলে fail_stages={"build_ipa_apk"} দিয়ে print_report(...) একটি নতুন কল যোগ করুন এবং দেখুন artifacts ডিকশনারি খালি থাকে কি না, যদিও build_ipa_apk স্টেজ নিজেই একবার কল হয়েছে।

    হ্যাঁ, artifacts খালি থাকবে — কারণ make_stage()-এর ভেতর if stage_name == "build_ipa_apk" and succeeded: শর্তটি সত্যি হয় শুধুমাত্র যখন succeeded সত্যি (অর্থাৎ স্টেজ ব্যর্থ না হলে)। যেহেতু এই কলে "build_ipa_apk" নিজেই fail_stages-এ আছে, succeeded = False হবে, তাই আর্টিফ্যাক্ট লেখার লাইনটি কখনো চলবে না — স্টেজটি কল হওয়া (call_count=1) আর স্টেজটির কাজ সম্পূর্ণ হওয়া দুটো ভিন্ন জিনিস, এটাই এই উদাহরণ দেখায়।

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

  • কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ অ্যাপ লাইফসাইকেল, মোবাইল UI/UX, MVVM/MVI আর্কিটেকচার, নেটিভ বনাম ক্রস-প্ল্যাটফর্ম, নেভিগেশন, স্টেট ম্যানেজমেন্ট, লোকাল স্টোরেজ, ডিভাইস ফিচার ও ডিপ্লয়মেন্ট — বাকি পাঠগুলো শীঘ্রই যুক্ত হবে।
  • FSWF · CI/CD Pipelines for Full-Stack Projects সহোদর পাঠ CI/CD পাইপলাইনের সাধারণ ধারণা ও ওয়েব-প্রেক্ষাপটের বিস্তারিত এই পাঠেই তৈরি হয়েছে।
  • সব Courses দেখুন ABCL TECH C, C++, Python, Java, JavaScript, DSA, DBMS, Discrete Mathematics, System Design, Cybersecurity, Cloud Computing & DevOps, Computer Networks, Operating Systems, Computer Architecture, Programming Languages & Compiler Design, Software Engineering & Git, Theory of Computation, Engineering Economics, Full-Stack Web Frameworks ও Mobile App Development — সব এক জায়গায়।
আগের পাঠ
মোবাইল অ্যাপ সিকিউরিটি ফান্ডামেন্টাল