সরাসরি পর্বে যান
পর্ব ৮১ · ৯৩-এর মধ্যে · মডিউল ১২

CI/CD — lint, test, build, preview আর migration

CI/CD for a Full-Stack App — GitHub Actions, Caching, Tests with PostgreSQL, Playwright Sharding, Preview Deployments, Migrations, Secrets and Rollback

Taught in Bangla, this lesson builds a GitHub Actions pipeline for the Bazarghar app: triggers, permissions and job order, npm ci with lockfile caching, lint, type check and unit tests first, integration tests with a PostgreSQL service container, builds and artifacts, sharded Playwright with merged reports, required checks and previews, migrations once before traffic, secrets, forks and pinned actions, and deploys with approval and rollback.

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

  • Next.js আর Prisma অ্যাপের জন্য একটি পূর্ণ GitHub Actions workflow পড়া ও লেখা, আর actionlint দিয়ে তা যাচাই করা
  • npm ci, lockfile আর cache key; job-এর needs, concurrency, PostgreSQL service container, artifact আর Playwright shard
  • Required check, branch protection, প্রতিটি PR-এর preview আর preview database কীভাবে কাজ করে
  • Pipeline-এ migration-এর সঠিক জায়গা, expand and contract, deploy gate আর আগের build দিয়ে rollback
  • Secret, fork, pull_request_target, OIDC, SHA pinning, npm audit, আর CI দ্রুত রাখার মাপা কৌশল

১ · বাজারঘরের pipeline — প্রতিটি পরিবর্তনের একই পরীক্ষা

মডিউল ১১-এ বাজারঘরের জন্য চার স্তরের test লেখা হয়েছে: পর্ব ৭৩-এ Vitest-এর unit test, পর্ব ৭৪-এ component test, পর্ব ৭৫-এ test database-সহ integration test, আর পর্ব ৭৬-এ Playwright-এর E2E. কিন্তু test লেখা থাকলেই হয় না; কেউ সেগুলো চালাতে ভুলে গেলে, বা নিজের ল্যাপটপে পাস করে অন্যের কম্পিউটারে ফেল করলে, test-এর মূল্য অর্ধেক। এই পর্বের কাজ সেই ফাঁক বন্ধ করা: প্রতিটি পরিবর্তনে একটি পরিষ্কার মেশিন নিজে থেকে সব ধাপ চালাবে, আর ফল দেখাবে pull request-এর পাশে।

একটি গার্মেন্টস কারখানার শেষ লাইনের কথা ভাবুন। প্রতিটি শার্ট কয়েকটি টেবিল পেরোয়: সুতো কাটা, সেলাই মাপা, বোতাম টানা, ইস্ত্রি, প্যাকিং। কোনো টেবিলে ত্রুটি ধরা পড়লে শার্টটি সেখানেই থামে, পরের টেবিলে যায় না। সবচেয়ে সস্তা পরীক্ষা (সুতো কাটা) আগে, সবচেয়ে দামি (পুরো পোশাক পরিয়ে দেখা) শেষে। আর কোনো শার্ট বায়ারের কাছে যায় না, যতক্ষণ না কোয়ালিটি ম্যানেজার সই করেন। CI/CD pipeline ঠিক এই লাইন: প্রতিটি commit শার্ট, প্রতিটি job একটি টেবিল, আর production-এ যাওয়ার আগে একজন মানুষের অনুমোদন।

শব্দ দুটি আলাদা। Continuous integration (CI) মানে প্রত্যেকের পরিবর্তন ঘন ঘন মূল branch-এ মেলানো, আর প্রতিবার স্বয়ংক্রিয় build ও test. Continuous delivery বা deployment (CD) মানে সবুজ build যেকোনো সময় production-এ পাঠানোর মতো প্রস্তুত রাখা, আর পাঠানোর পথটিও স্বয়ংক্রিয় করা। Stage, job, DAG, runner, cache poisoning আর Amdahl-এর সীমার তত্ত্ব আছে Cloud ও DevOps কোর্সের pipeline নকশার পর্বে; rolling, blue-green আর canary আছে deployment strategy পর্বে; branch, pull request আর review-এর নিয়ম আছে Software Engineering-এর pull request পর্বে আর continuous integration পর্বে। এখানে তত্ত্ব আবার নয়; এখানে একটি নির্দিষ্ট অ্যাপের একটি নির্দিষ্ট pipeline, শুরু থেকে শেষ।

বাজারঘরের CI/CD — কোন job কার পরে

Jobs, needs, service container, artifacts, required checks and the two exits

বাজারঘরের CI/CD pipeline-এর job আর নির্ভরতা pull request / push পুরোনো run বাতিল quality audit · lint · tsc · vitest integration 🐘 service · migrate deploy build next build → artifact e2e 1/2 Playwright shard e2e 2/2 Playwright shard e2e-report merge-reports → HTML ✓ required checks সবুজ PR: preview নিজস্ব URL + preview DB main: deploy migrate → অনুমোদন → release
ছবি ৮১.১ — quality আর integration একসাথে শুরু হয়; build অপেক্ষা করে শুধু quality-র জন্য; দুটি E2E shard অপেক্ষা করে build আর integration দুটোর জন্য (সবুজ ড্যাশ রেখা)। শেষে একই সবুজ ফল দুই দিকে যায়: PR-এ preview, main-এ deploy.

এই পর্বের প্রতিটি আউটপুট একটি সত্যিকারের ল্যাব থেকে, Windows 10-এর একটি কম্পিউটারে, কোনো CI সেবা ছাড়া। লেখার সময়ের version: Node 24.15.0, npm 11.12.1, create-next-app 16.4.4 দিয়ে বানানো Next.js 16.4.4 আর React 19.3.0, TypeScript 5.9.3, ESLint 9.39.5 (flat config), Prisma 7.10.1 (npm-এর latest একটি 8.0 release candidate, তাই stable 7.10.1 বেছে নেওয়া), Vitest 5.1.0, jsdom 30.1.3, Testing Library 16.3.3, Playwright 1.64.1, আর embedded-postgres 18.6.0-beta.19 প্যাকেজের PostgreSQL 18, যা চলেছে শুধু 127.0.0.1-এ, port 47823. Workflow ফাইল GitHub-এ চালানো হয়নি; সেটি যাচাই হয়েছে actionlint দিয়ে, আর তার প্রতিটি ধাপ একটি script একই ক্রমে নিজের কম্পিউটারে চালিয়েছে।

“Continuous integration” কথাটি Grady Booch ১৯৯১ সালে তাঁর object-oriented design-এর বইয়ে ব্যবহার করেন বলে সাধারণত বলা হয়। ১৯৯০-এর দশকের শেষে Kent Beck-এর Extreme Programming একে দিনে বহুবার মেলানোর অভ্যাসে পরিণত করে, আর Martin Fowler ২০০০ সালে এ নিয়ে একটি প্রভাবশালী প্রবন্ধ লেখেন (২০০৬ সালে হালনাগাদ)। Jez Humble আর David Farley-র “Continuous Delivery” বই প্রকাশ হয় ২০১০ সালে। GitHub Actions ঘোষিত হয় ২০১৮ সালে, আর CI/CD-সহ সাধারণভাবে উন্মুক্ত হয় ২০১৯ সালের নভেম্বরে। GitLab CI বা Jenkins-এর মতো বিকল্পেও ধারণাগুলো একই।

২ · Workflow ফাইল — trigger, permission, concurrency আর needs

GitHub Actions-এ pipeline লেখা হয় YAML ফাইলে, .github/workflows/ folder-এ। একটি ফাইল একটি workflow; তার ভেতরে কয়েকটি job, প্রতিটি job একটি আলাদা, নতুন runner-এ চলে, আর job-এর ভেতরে ধাপ বা step একটির পর একটি চলে। নিচে বাজারঘরের ci.yml-এর মাথা আর প্রথম job. পুরো ফাইলে পাঁচটি job: quality, integration, build, e2e (matrix-এ দুটি shard, তাই run-এ ছয়টি job দেখায়) আর e2e-report; deploy আলাদা ফাইলে (১০ নম্বর অংশে)।

name: ci

on:
  pull_request:
  push:
    branches: [main]

permissions:
  contents: read            # GITHUB_TOKEN শুধু পড়তে পারবে

concurrency:
  group: ci-${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
  quality:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
        with:
          node-version: 24
          cache: npm
      - run: npm ci
      - run: npm audit --omit=dev --audit-level=high
      - run: npx prisma generate
      - run: npx eslint .
      - run: npx tsc --noEmit
      - run: npx vitest run

অংশগুলো একে একে। on: কখন চলবে। প্রতিটি pull request-এ, আর main-এ push হলে (merge-এর পরে)। permissions: প্রতিটি job একটি স্বয়ংক্রিয় GITHUB_TOKEN পায়; এখানে বলা হলো সেটি শুধু repository পড়তে পারবে। কম অধিকার দিয়ে শুরু, দরকার হলে নির্দিষ্ট job-এ বাড়ানো, এটি least privilege. concurrency: একই branch-এ নতুন push এলে পুরোনো run চালিয়ে লাভ নেই, তাই একই group-এর আগের run বাতিল হয়। লক্ষ করুন, শর্তটি শুধু pull request-এর জন্য সত্য; main-এর run বাতিল না করাই নিরাপদ, কারণ প্রতিটি merge-এর ফল আলাদা করে জানা দরকার। এই group-কে বলে concurrency group.

Action-গুলোর @-এর পরে একটি লম্বা hex সংখ্যা: এটি action-এর repository-র একটি নির্দিষ্ট commit, আর পাশের মন্তব্যে মানুষের পড়ার জন্য version. লেখার সময়ে GitHub-এর release পাতা থেকে নেওয়া: actions/checkout v7.0.1, actions/setup-node v7.1.0, actions/upload-artifact v7.0.2, actions/download-artifact v8.0.2 (আর actions/cache v6.1.0, যা এখানে আলাদা করে লাগেনি)। কেন tag-এর বদলে commit, তা ৯ নম্বর অংশে। Version বদলায়, তাই নিজের workflow লেখার দিন আবার release পাতা দেখুন।

Job-এর নির্ভরতা: needs

Job-গুলো সাধারণভাবে একসাথে শুরু হয়। কোনো job আগে অন্য job শেষ হওয়ার অপেক্ষা করবে, তা বলে needs: build-এ needs: [quality], e2e-তে needs: [build, integration]. এভাবে job-গুলো একটি DAG তৈরি করে (directed acyclic graph, অর্থাৎ দিকওয়ালা, চক্রহীন নির্ভরতার ছক)। GitHub-এর docs অনুযায়ী কোনো job ব্যর্থ বা skip হলে যে job তাকে needs করে, সেটিও skip হয়, যদি না তার if-এ always() বা !cancelled()-এর মতো শর্ত থাকে। আর job-এর ভেতরে ধাপগুলো fail-fast: একটি step ফেল করলে পরের step চলে না। নিচের ব্লক এই দুই নিয়মের একটি ছোট মডেল, একটি নকল ঘড়িতে। সংখ্যাগুলো ল্যাবের স্থানীয় সময়ের কাছাকাছি করে বানানো, আসল CI-এর মাপ নয়।

// Teaching model: jobs with "needs", a simulated clock, fail-fast steps.
const jobs = {
  quality:     { needs: [], steps: [['install', 20], ['lint', 2], ['typecheck', 2], ['unit', 2]] },
  integration: { needs: [], steps: [['install', 20], ['migrate', 1], ['int-tests', 1]] },
  build:       { needs: ['quality'], steps: [['install', 20], ['next build', 5]] },
  e2e_1:       { needs: ['build', 'integration'], steps: [['install', 20], ['shard 1/2', 4, 'FAIL'], ['upload', 1]] },
  e2e_2:       { needs: ['build', 'integration'], steps: [['install', 20], ['shard 2/2', 4]] },
  report:      { needs: ['e2e_1', 'e2e_2'], always: true, steps: [['merge-reports', 3]] },
  deploy:      { needs: ['e2e_1', 'e2e_2'], steps: [['migrate deploy', 2], ['release', 5]] },
};
const done = {};
let work = 0; // runner-seconds actually used
while (Object.keys(done).length < Object.keys(jobs).length) {
  for (const [name, job] of Object.entries(jobs)) {
    if (done[name] || job.needs.some((n) => !done[n])) continue; // wait for needs
    const start = Math.max(0, ...job.needs.map((n) => done[n].end));
    if (!job.always && job.needs.some((n) => done[n].status !== 'success')) {
      done[name] = { status: 'skipped', end: start };
      console.log(`${name.padEnd(12)} skipped (a needed job did not succeed)`);
      continue;
    }
    let t = start, status = 'success', ran = [];
    for (const [step, secs, result] of job.steps) {      // fail-fast inside a job
      t += secs; work += secs; ran.push(step);
      if (result === 'FAIL') { status = 'failure'; break; }
    }
    done[name] = { status, end: t };
    console.log(`${name.padEnd(12)} ${status.padEnd(8)} t=${start}s→${t}s  ran: ${ran.join(', ')}`);
  }
}
const wall = Math.max(...Object.values(done).map((d) => d.end));
console.log(`wall clock ${wall}s; runner time used ${work}s (the steps that really ran)`);

quality আর integration দুটোই t=0s-এ শুরু, কারণ কারও needs নেই। build শুরু হয় t=26s-এ, ঠিক যখন quality শেষ। দুটি shard শুরু t=51s-এ: build শেষ ৫১-তে, integration আগেই ২২-এ, দুটোর বড়টি। e2e_1 failure ... ran: install, shard 1/2: shard ফেল করায় upload step আর চলেনি, এটি job-এর ভেতরের fail-fast. report success: এর always আছে, তাই একটি shard ফেল করলেও report তৈরি হয়, ঠিক যেমন আসল workflow-এ if: ${{ !cancelled() }}. আর deploy skipped (a needed job did not succeed): লাল E2E-র পরে deploy হয় না। শেষ লাইন wall clock 78s; runner time used 124s: parallel job ঘড়ির সময় কমায়, কিন্তু runner-এর মোট সময় বাড়ায়, কারণ প্রতিটি job আলাদা মেশিনে আবার install করে। ১০ নম্বর অংশে এই হিসাব আবার আসবে।

Workflow-ও কোড — actionlint দিয়ে যাচাই

YAML-এ বানান ভুল হলে GitHub push-এর পরেই তা জানায়, আর তখন একটি ভুলের জন্য একটি পুরো round trip. actionlint নামের open-source টুল workflow ফাইল নিজের কম্পিউটারেই পড়ে ভুল ধরে: অজানা key, ভুল needs, expression-এর type, পুরোনো action, আর কিছু নিরাপত্তার ঝুঁকি। ল্যাবে এর Windows binary নামানো হয়েছে GitHub release থেকে, আর release-এর checksum ফাইলের সাথে মিলিয়ে দেখা হয়েছে। সত্যিকারের run:

$ curl -sSLO …/v1.7.12/actionlint_1.7.12_windows_amd64.zip
$ curl -sSLO …/v1.7.12/actionlint_1.7.12_checksums.txt
$ sha256sum -c --ignore-missing actionlint_1.7.12_checksums.txt
actionlint_1.7.12_windows_amd64.zip: OK
$ actionlint -version
1.7.12
installed by downloading from release page
built with go1.26.1 compiler for windows/amd64

$ actionlint .github/workflows/ci.yml .github/workflows/deploy.yml
$ echo $?
0

কোনো আউটপুট নেই, exit code 0: দুটি ফাইলই পরিষ্কার। (Folder-টি git repository না হলে actionlint নিজে workflow folder খুঁজে পায় না, তাই ফাইলের নাম দিয়ে চালানো হয়েছে।) এবার ইচ্ছা করে চারটি ভুলসহ একটি ছোট ফাইল: পুরোনো checkout@v3, pull_request_target-এ PR-এর শিরোনাম সরাসরি shell-এ, needs-এ বানান ভুল, আর matrix-এর ভুল নাম।

$ actionlint bad.yml
bad.yml:9:15: the runner of "actions/checkout@v3" action is too old to run on GitHub Actions. update the action's version to fix this issue [action]
bad.yml:10:33: "github.event.pull_request.title" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https://docs.github.com/en/actions/reference/security/secure-use#good-practices-for-mitigating-script-injection-attacks for more details [expression]
bad.yml:12:3: job "e2e" needs job "biuld" which does not exist in this workflow [job-needs]
bad.yml:19:46: property "shrd" is not defined in object type {shard: number} [expression]
$ echo $?
1

চারটিই আসল ধরনের ভুল। দ্বিতীয়টি সবচেয়ে গুরুত্বপূর্ণ: PR-এর শিরোনাম যে কেউ লিখতে পারেন, আর ${{ … }} shell চলার আগে লেখার ভেতরে বসে যায়। শিরোনামে "; কোনো কমান্ড; " থাকলে সেটি runner-এ চলবে। একে বলে script injection; প্রতিকার actionlint নিজেই বলছে: মানটি env: দিয়ে environment variable-এ দিন, তারপর shell-এ "$TITLE" লিখুন। বাজারঘরের deploy.yml-এ একই কারণে inputs.build_sha সরাসরি না বসিয়ে env দিয়ে পাঠানো হয়েছে। একটি ভালো অভ্যাস: workflow ফাইল বদলানো প্রতিটি PR-এ actionlint চালানো, CI-তেই বা commit-এর আগে।

৩ · Install আর cache — npm ci, lockfile আর cache key

প্রতিটি job একটি খালি মেশিনে শুরু হয়, তাই প্রথম কাজ dependency বসানো। CI-তে npm install নয়, npm ci. পার্থক্য পর্ব ০৫-এ এসেছিল; সংক্ষেপে: npm ci node_modules পুরো মুছে lockfile-এ (package-lock.json) লেখা ঠিক version-গুলোই বসায়, package.json-এর সাথে lockfile না মিললে থেমে যায়, আর lockfile কখনো বদলায় না। ফলে আজকের CI আর গতকালের CI একই dependency পায়, আর কেউ চুপচাপ নতুন version ঢোকাতে পারে না।

Install-এর একটি আসল অভিজ্ঞতা দিয়ে শুরু। create-next-app-এর বানানো project-এ Vitest 5.1 যোগ করতে গিয়ে npm থামল:

$ npm i -D vitest@5.1.0 …
npm error code ERESOLVE
npm error While resolving: vitest@5.1.0
npm error Found: @types/node@20.19.43
…
npm error Could not resolve dependency:
npm error peerOptional @types/node@"^22.0.0 || >=24.0.0" from vitest@5.1.0

Template-এ ছিল "@types/node": "^20", অথচ Vitest 5.1 চায় 22 বা 24 থেকে। সমাধান --force নয়, বরং চলমান Node-এর সাথে মেলানো @types/node@^24. ঠিক এই কারণেই lockfile commit করা হয়: dependency মেলানোর এই সিদ্ধান্ত একবার নিজের কম্পিউটারে নেওয়া হয়, তারপর CI শুধু অনুসরণ করে।

Cache — কী রাখা হয়, কোন চাবিতে

actions/setup-node-এ cache: npm দিলে action-টি npm-এর global cache folder (Linux-এ সাধারণত ~/.npm) রেখে দেয়, node_modules নয়; docs-এও তা স্পষ্ট লেখা। Cache-এর key তৈরি হয় lockfile-এর hash থেকে, তাই lockfile না বদলালে পরের run একই cache ফিরে পায়। Docs আরও বলে, v6 থেকে package.json-এ packageManager (বা devEngines.packageManager) npm হলে cache নিজে থেকেই চালু হয়; না চাইলে package-manager-cache: false. বাজারঘরে cache: npm স্পষ্ট লেখা, যাতে পাঠক বুঝতে পারেন কী হচ্ছে।

Cache-এর মূল্য কত, তা মাপা হয়েছে। একটি খালি cache folder দিয়ে npm ci (cache miss-এর মতো), তারপর একই folder দিয়ে আবার দুবার। সত্যিকারের run, একটি স্থানীয় run, সাধারণ ফল নয়:

$ node measure-install.mjs
cache miss (empty)     100.4 s   added 565 packages in 2m
cache hit (same dir)    17.3 s   added 565 packages in 17s
cache hit again         17.0 s   added 565 packages in 17s

৫৬৫টি package, cache ছাড়া ১০০.৪ সেকেন্ড, cache থেকে ১৭ সেকেন্ডের মতো: প্রায় ছয় গুণ পার্থক্য। Cache থাকলেও ১৭ সেকেন্ড লাগে, কারণ npm ci তবুও node_modules নতুন করে লেখে; ল্যাবে সেই folder ৮৫০ MB. নিচের ব্লকে lockfile-এর hash থেকে key বানানোর একটি মডেল। আসল setup-node-এর key-তে কী কী থাকে, তা action-এর version অনুযায়ী আলাদা হতে পারে; ধারণাটি একই।

// Teaching model: a dependency cache keyed by the hash of package-lock.json.
const { createHash } = require('node:crypto');
const hash = (text) => createHash('sha256').update(text).digest('hex').slice(0, 12);
const keyFor = (os, lock) => `node-cache-${os}-npm-${hash(lock)}`;

const lockV1 = JSON.stringify({ packages: { next: '16.4.4', prisma: '7.10.1' } });
const lockV2 = JSON.stringify({ packages: { next: '16.4.4', prisma: '7.10.1', zod: '4.6.7' } });
const store = new Map(); // what the cache service already holds

function restore(os, lock) {
  const key = keyFor(os, lock);
  const hit = store.has(key);
  if (!hit) store.set(key, 'npm cache saved after a successful job');
  return `${hit ? 'HIT ' : 'MISS'} ${key}`;
}
console.log('run 1, linux, lock v1     ', restore('Linux', lockV1));
console.log('run 2, linux, lock v1     ', restore('Linux', lockV1));
console.log('run 3, only a .tsx changed', restore('Linux', lockV1));
console.log('run 4, zod added, lock v2 ', restore('Linux', lockV2));
console.log('run 5, windows, lock v2   ', restore('Windows', lockV2));
console.log('run 6, linux, lock v2     ', restore('Linux', lockV2));
console.log('one extra space in the lockfile, same key?', hash(lockV1) === hash(lockV1 + ' '));
console.log('entries in the cache store:', store.size);

প্রথম run MISS node-cache-Linux-npm-de60d24ddf7b: cache খালি, তাই পুরো download, তারপর সংরক্ষণ। দ্বিতীয় আর তৃতীয় run HIT: একটি .tsx ফাইল বদলালে lockfile বদলায় না, তাই একই key. চতুর্থ run MISS …728a860e1b6e: zod যোগ হতেই lockfile-এর hash বদলেছে। পঞ্চম MISS node-cache-Windows-…: একই lockfile, কিন্তু অন্য operating system-এর জন্য আলাদা key, কারণ কিছু package-এর ভেতরে OS-নির্দিষ্ট binary থাকে। ষষ্ঠ আবার HIT. one extra space in the lockfile, same key? false: hash একটি অক্ষরের পার্থক্যও ধরে। শেষে entries in the cache store: 3. অর্থাৎ cache hit নির্ভর করে lockfile কত ঘন ঘন বদলায় তার ওপর; প্রতিদিন dependency বদলানো দলে cache-এর লাভ কম।

Cache-এ বিশ্বাস, কিন্তু সীমিত। Cache-এ কখনো build-এর ফল বা secret রাখবেন না যা পরে যাচাই ছাড়া ব্যবহার হবে। PR-এর কোডের লেখা cache পরে অন্য run পড়লে দূষিত cache ঢুকতে পারে; এই “cache poisoning”-এর তত্ত্ব দেখুন pipeline নকশার পর্বে। Dependency-র ক্ষেত্রে npm ci নিজেই lockfile-এর integrity hash মিলিয়ে নেয়, তাই নষ্ট package ধরা পড়ে।

৪ · দ্রুত check — lint, type check, unit আর component test

quality job-এ চারটি সস্তা পরীক্ষা, প্রতিটি কয়েক সেকেন্ডের। Lint: Next.js 16-এ next lint কমান্ড আর নেই, আর next build নিজে lint চালায় না; সরকারি upgrade guide-এ এটি স্পষ্ট লেখা। তাই ESLint সরাসরি চলে, create-next-app-এর বানানো flat config eslint.config.mjs দিয়ে, যা eslint-config-next-এর core-web-vitals আর typescript নিয়ম জোড়া দেয়। ল্যাবে এর globalIgnores-এ generated/** (Prisma-র বানানো client) আর Playwright-এর report folder যোগ করা হয়েছে, নইলে ESLint বানানো কোডেও অভিযোগ করে। Type check: tsc --noEmit, অর্থাৎ শুধু type যাচাই, কোনো JavaScript লেখা নয়। এর আগে prisma generate লাগে, কারণ Prisma 7-এর বানানো client ফাইল সাধারণত repository-তে রাখা হয় না (.gitignore-এ থাকে), তাই প্রতিবার schema থেকে বানাতে হয়। Unit ও component test: vitest run, jsdom-এ।

এই চারটির জন্য পুরো CI-তে অপেক্ষা না করে নিজের কম্পিউটারে push-এর আগেই চালানো যায়। ল্যাবের একটি git repository-তে (শুধু scratch folder-এ, আলাদা git config দিয়ে) একটি git hook বসানো হয়েছে: .githooks/pre-push, যা node scripts/ci-local.mjs lint typecheck unit+component চালায়। তারপর একটি commit-এ ইচ্ছা করে formatTaka('13490') লেখা হলো, যদিও function সংখ্যা চায়। সত্যিকারের run:

$ git push -u origin main
▶ lint: npx eslint .

▶ typecheck: npx tsc --noEmit
lib/price.ts(13,34): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
✖ typecheck failed after 2.1s — stopping (fail-fast)

step             seconds  result
lint                 2.6  ok
typecheck            2.1  FAILED
total                4.7
error: failed to push some refs to '../remote.git'

৪.৭ সেকেন্ডে ভুল ধরা, push হয়নি। সংশোধনের পরে একই hook: lint ২.০, typecheck ১.৭, unit ও component ১.৮, মোট ৫.৫ সেকেন্ড, তারপর * [new branch] main -> main. কিন্তু hook একটি সুবিধা, পাহারা নয়: git push --no-verify দিয়ে যে কেউ এটি এড়াতে পারেন, আর নতুন সদস্যের কম্পিউটারে hook বসানোই না থাকতে পারে। সিদ্ধান্তের কর্তৃত্ব থাকে CI-র হাতে।

প্রথম লাল run — একটি সত্যিকারের ব্যর্থতা

পুরো pipeline প্রথমবার চালাতেই একটি component test ফেল করল, আর fail-fast পরের সব ধাপ থামিয়ে দিল। Test-টি একটি <p role="status">স্টক শেষ</p> খুঁজছিল getByRole('status') দিয়ে। সত্যিকারের run, অংশ:

 FAIL  components/AddToCart.test.tsx > স্টকের বেশি যোগ করা যায় না
TestingLibraryElementError: Found multiple elements with the role "status"

Here are the matching elements:
<output
  aria-label="মসুর ডাল পরিমাণ"
>
  2
</output>
<p
  role="status"
>
  স্টক শেষ
</p>
…
✖ unit+component failed after 1.9s — stopping (fail-fast)

কারণটি শিক্ষণীয়। HTML-এর <output> element-এর নিজস্ব implicit ARIA role হলো status, অর্থাৎ role না লিখলেও স্ক্রিন রিডার আর Testing Library একে status হিসেবেই দেখে। তাই একই নামের দুটি element. সমাধান test-এ: getByText('স্টক শেষ'), আর E2E-তেও একই বদল, নইলে Playwright-এর strict mode একই কারণে থামত (পর্ব ৭৬)।

৫ · Database-সহ test আর build — service container, migrate deploy আর artifact

Integration test-এর জন্য আসল PostgreSQL লাগে, mock নয় (পর্ব ৭৫)। GitHub Actions-এ সবচেয়ে সহজ উপায় service container: job শুরুর আগে runner একটি container-এ database চালু করে, health check সফল হওয়া পর্যন্ত অপেক্ষা করে, আর job শেষে মুছে দেয়। প্রতিটি run তাই একটি সম্পূর্ণ খালি database পায়। নিচের YAML চলে GitHub-এর Linux runner-এ, যেখানে Docker আছে; এখানে, এই ল্যাবে, এটি চালানো হয়নি।

  integration:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    services:
      postgres:
        image: postgres:18
        env:
          POSTGRES_PASSWORD: example-secret     # শুধু এই ক্ষণস্থায়ী container-এর
          POSTGRES_DB: bazarghar_ci
        ports: ['5432:5432']
        options: >-
          --health-cmd "pg_isready -U postgres"
          --health-interval 5s --health-timeout 5s --health-retries 10
    env:
      DATABASE_URL: postgresql://postgres:example-secret@localhost:5432/bazarghar_ci
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
        with: { node-version: 24, cache: npm }
      - run: npm ci
      - run: npx prisma generate
      - run: npx prisma migrate deploy
      - run: npx vitest run -c vitest.integration.config.mts

এখানে password secret নয়: container-টি job-এর সাথে জন্মায় আর মরে, বাইরে থেকে পৌঁছানো যায় না। তবু স্পষ্ট নকল মান রাখা ভালো অভ্যাস। লক্ষ করুন, test-এর আগে prisma migrate deploy: খালি database-এ ঠিক সেই migration ফাইলগুলো চলে, যা production-এ চলবে। ফলে migration-এর ভুল (SQL-এর ভুল, ক্রমের ভুল) এখানেই ধরা পড়ে।

ল্যাবে Docker চালানো হয়নি। Container-এর জায়গায় embedded-postgres একটি আলাদা folder থেকে 127.0.0.1:47823-এ চলেছে, আর pipeline script প্রতিবার DROP DATABASE … WITH (FORCE) আর CREATE DATABASE দিয়ে খালি database বানিয়েছে, যেন নতুন container. কাজের আগে নিশ্চিত হওয়া গেছে যে সংযোগ নিজের server-এই: database bazarghar_ci81, port 47823, আর data_directory ল্যাবের নিজের folder. সত্যিকারের run:

▶ fresh-db

▶ migrate: npx prisma migrate deploy
Loaded Prisma config from prisma.config.ts.
Prisma schema loaded from prisma\schema.prisma.
Datasource "db": PostgreSQL database "bazarghar_ci81_run", schema "public" at "127.0.0.1:47823"

1 migration found in prisma/migrations

Applying migration `20261103112012_init`

The following migration(s) have been applied:

migrations/
  └─ 20261103112012_init/
    └─ migration.sql

All migrations have been successfully applied.

▶ integration: npx vitest run -c vitest.integration.config.mts
 Test Files  1 passed (1)
      Tests  2 passed (2)
   Duration  360ms (tests 52%, import 24%, transform 22%, worker 2%)

দুটি integration test: স্টক শূন্য পণ্য তালিকায় আসে না, আর একই sku দুবার দিলে database নিজেই Unique constraint দিয়ে আটকায়। দ্বিতীয় নিয়মটি database-এর index-এ, তাই mock তা প্রমাণ করতে পারত না।

Build — যা lint আর tsc ধরেনি

একটি অদ্ভুত সত্য: lint সবুজ, type check সবুজ, সব test সবুজ, তবু দ্বিতীয় run-এ next build ফেল করল। সত্যিকারের run:

▶ build: npx next build
▲ Next.js 16.4.4 (Turbopack)
- Cache Components enabled
- Partial Prefetching enabled

  Creating an optimized production build ...

> Build error occurred
Error: Turbopack build failed with 1 error:
./app/page.tsx:6:14
Error: Route segment config "dynamic" is not compatible with `nextConfig.cacheComponents`. Please remove it.
> 6 | export const dynamic = 'force-dynamic';
✖ build failed after 2.5s — stopping (fail-fast)

create-next-app 16.4.4-এর বানানো next.config.ts-এ cacheComponents: true চালু ছিল, আর পুরোনো অভ্যাসের export const dynamic = 'force-dynamic' তার সাথে চলে না। TypeScript-এর চোখে লাইনটি বৈধ, তাই tsc চুপ ছিল; নিয়মটি framework-এর build জানে। সমাধান: পণ্যের তালিকা একটি আলাদা async component-এ, await connection() দিয়ে চিহ্নিত যে এটি প্রতি request-এ চলবে, আর বাইরে <Suspense>. Rendering-এর এই মডেল পর্ব ৫৫-এ। সংশোধনের পরে build সফল, আর route-এর তালিকায় ◐ /: “prerendered as static HTML with dynamic server-streamed content”. শিক্ষা: CI-তে build একটি আলাদা, আবশ্যিক ধাপ; lint আর tsc সবুজ মানে build সবুজ নয়।

Build-এর ফল, অর্থাৎ .next/ folder, একটি artifact হিসেবে রাখা হয়, যাতে E2E job একই build পরীক্ষা করে, আবার বানায় না। একবার build, তারপর সেটিই পরীক্ষা ও deploy, এই নীতির তত্ত্ব আছে Cloud ও DevOps-এর artifact আর promotion পর্বে; build-এর ভেতরের কাজ পর্ব ৭৯-এ। একটি সতর্কতা: NEXT_PUBLIC_ দিয়ে শুরু variable build-এর সময় JavaScript-এ বসে যায়, তাই একই artifact ভিন্ন environment-এ নিলে সেগুলো বদলায় না (পর্ব ৭৮)।

      - run: npx next build
      - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
        with:
          name: next-build-${{ github.sha }}
          path: .next/
          include-hidden-files: true   # নাম বিন্দু দিয়ে শুরু, নিরাপদ দিকে থাকা
          retention-days: 7

৬ · E2E — Playwright shard, blob report আর একটি HTML report

E2E সবচেয়ে ধীর স্তর। পর্ব ৭৬-এ দেখা sharding এখানে কাজে লাগে: একটি matrix দুটি job বানায়, প্রতিটি --shard=1/2 বা --shard=2/2 নিয়ে অর্ধেক test চালায়, আলাদা runner-এ, একসাথে। প্রতিটি shard তার ফল লেখে একটি blob report-এ, যা মানুষের পড়ার জন্য নয়, জোড়া দেওয়ার জন্য। শেষে একটি আলাদা job সব blob নামিয়ে merge-reports দিয়ে একটি HTML report বানায়। Playwright-এর config-এ CI-তে reporter: [['blob'], ['github']], আর নিজের কম্পিউটারে list.

  e2e:
    needs: [build, integration]
    strategy:
      fail-fast: false        # এক shard ফেল করলেও অন্যটি শেষ হোক
      matrix:
        shard: [1, 2]
    # … postgres service, npm ci, prisma generate …
    steps:
      - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
        with: { name: 'next-build-${{ github.sha }}', path: .next/ }
      - run: npx prisma migrate deploy
      - run: node scripts/seed-e2e.mjs
      - run: npx playwright install --with-deps chromium
      - run: npx playwright test --shard=${{ matrix.shard }}/2
      - if: ${{ !cancelled() }}
        uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
        with: { name: 'blob-report-${{ matrix.shard }}', path: blob-report/, retention-days: 7 }

  e2e-report:
    needs: [e2e]
    if: ${{ !cancelled() }}
    # … download-artifact with pattern: blob-report-* and merge-multiple: true
    #     npx playwright merge-reports --reporter html ./all-blob-reports
    #     upload-artifact: playwright-report

এখানে fail-fast: false ইচ্ছাকৃত। Matrix-এর default-এ একটি job ফেল করলে বাকিগুলো বাতিল হয়; E2E-তে তা চাই না, কারণ অন্য shard-এর ফলও জানা দরকার, নইলে প্রতিবার একটি করে ব্যর্থতা দেখে সারাতে হবে। Upload-এর if: ${{ !cancelled() }} নিশ্চিত করে test ফেল করলেও report ওঠে; ব্যর্থতার সময়েই report সবচেয়ে দরকারি। CI-র runner-এ Playwright-এর নিজের Chromium বসানো হয়, আর ল্যাবে config CI না থাকলে কম্পিউটারের Microsoft Edge (channel: 'msedge') ব্যবহার করে।

ল্যাবে দুই shard পরপর চালানো হয়েছে, প্রতিটি নিজের blob folder-এ, তারপর জোড়া দেওয়া। E2E-র আগে একটি seed script তিনটি পণ্য বসায়: দুটি স্টকে, একটি স্টক শূন্য। সত্যিকারের run:

▶ seed-e2e: node scripts/seed-e2e.mjs
seeded 3 products

▶ e2e 1/2: npx playwright test --shard=1/2 --reporter=list,blob
Running 2 tests using 2 workers, shard 1 of 2
  ok 1 [chromium] › e2e\cart.spec.ts:9:5 › স্টক ফুরালে বোতাম বন্ধ (704ms)
  ok 2 [chromium] › e2e\cart.spec.ts:3:5 › ডাল কার্টে যোগ হয় (796ms)
  2 passed (2.6s)

▶ e2e 2/2: npx playwright test --shard=2/2 --reporter=list,blob
Running 2 tests using 2 workers, shard 2 of 2
  ok 1 [chromium] › e2e\home.spec.ts:9:5 › health route 200 দেয় (22ms)
  ok 2 [chromium] › e2e\home.spec.ts:3:5 › হোমপেজে শুধু স্টকে থাকা পণ্য (674ms)
  2 passed (2.4s)

$ npx playwright merge-reports --reporter list ./all-blob
Running 4 tests using 4 workers
  ok 1 [chromium] › e2e\cart.spec.ts:9:5 › স্টক ফুরালে বোতাম বন্ধ (508ms)
  ok 2 [chromium] › e2e\cart.spec.ts:3:5 › ডাল কার্টে যোগ হয় (808ms)
  ok 4 [chromium] › e2e\home.spec.ts:9:5 › health route 200 দেয় (21ms)
  ok 3 [chromium] › e2e\home.spec.ts:3:5 › হোমপেজে শুধু স্টকে থাকা পণ্য (701ms)
  4 passed (6.0s)

দুই shard-এর অংশ একটি run থেকে, আর merge-reports চালানো হয়েছে পরের run-এর blob-এর ওপর, তাই millisecond-এর সংখ্যা সামান্য আলাদা। merge-reports দুই shard-এর চারটি test এক report-এ এনেছে, যেন একটি run. প্রতিটি shard-এর মাথায় Playwright নিজে next start চালু করেছে (webServer), /api/health-এ 200 পাওয়ার পরে test শুরু, শেষে বন্ধ। এখানে প্রতিটি shard ৩.৪–৩.৫ সেকেন্ড, তার বেশির ভাগ server চালু হওয়া; এত ছোট suite-এ shard লাভজনক নয়।

৭ · Pull request — required check, branch protection আর preview

Pipeline তখনই পাহারা দেয়, যখন তার ফল merge আটকাতে পারে। GitHub-এ এটি করে branch protection-এর নিয়ম বা নতুন ধাঁচের ruleset: main-এ সরাসরি push বন্ধ, merge শুধু pull request দিয়ে, অন্তত একজনের review, আর নির্দিষ্ট কিছু required check সবুজ হতে হবে। বাজারঘরে required check: quality, integration, build, দুটি e2e job (matrix-এর job-এর নাম সাধারণত shard-এর মানসহ দেখায়)। Review আর merge-এর নিয়মের তত্ত্ব pull request পর্বে, branching-এর কৌশল branching strategy পর্বে।

Skip হওয়া job-এর ফাঁদ। GitHub-এর docs অনুযায়ী if-এর শর্তে skip হওয়া job “Success” হিসেবে রিপোর্ট হয়, আর required check হলেও merge আটকায় না। তাই যে job required, তাকে শর্তসাপেক্ষে skip করবেন না; দরকার হলে job চালিয়ে ভেতরের step-এ শর্ত দিন। Required check-এর নাম বদলালে (job-এর নাম বদলালে) পুরোনো নামের check কখনো আসবে না, আর PR চিরকাল “Expected — waiting” দেখাতে পারে; job-এর নাম বদলানোর সাথে নিয়মও বদলান।

Preview deployment আর preview database

Review-এ কোড পড়া এক জিনিস, পরিবর্তনটি চোখে দেখা আরেক। অনেক hosting platform-এর docs অনুযায়ী তারা repository-র সাথে যুক্ত হলে প্রতিটি pull request-এর জন্য একটি আলাদা URL-এ অ্যাপের একটি কপি চালায়, যাকে বলে preview deployment; PR বন্ধ হলে সেটি মুছে যায়। এখানে কোনো platform সুপারিশ করা হচ্ছে না, আর কোথাও সত্যিকারের deploy করা হয়নি। কোথায় অ্যাপ চালাবেন, তার তুলনা পর্ব ৮২-এ।

Full-stack অ্যাপের preview-এর কঠিন অংশ database. তিনটি সাধারণ পথ, প্রতিটির দাম আছে:

পথকীভাবেসুবিধাঝুঁকি বা খরচ
প্রতি PR-এ নতুন খালি databasemigrate deploy, তারপর seed scriptপরিষ্কার, প্রতিবার একই অবস্থাআসল ডেটার আকার আর অদ্ভুত ঘটনা দেখা যায় না
Database branchingকিছু managed PostgreSQL সেবার docs অনুযায়ী একটি branch কয়েক সেকেন্ডে তৈরি হয়, copy-on-write পদ্ধতিতেবাস্তবের মতো ডেটা, দ্রুতসেবা-নির্ভর; production-এর ব্যক্তিগত ডেটা preview-তে চলে আসতে পারে
একটি ভাগ করা staging databaseসব preview একই database-এসস্তা, সহজএক PR-এর migration অন্য PR-এর preview ভাঙে

Branching ব্যবহার করলে প্রশ্নটি নিরাপত্তার: ক্রেতার নাম, ফোন নম্বর, ঠিকানা preview-র URL-এ, যা হয়তো দলের বাইরের কেউও দেখতে পান। নিরাপদ ভিত্তি হলো production-এর বদলে একটি masked বা নকল ডেটার “parent” থেকে branch. আর fork থেকে আসা PR-এর preview-তে কোনো database-এর চাবি যাবে না; কেন, তা ৯ নম্বর অংশে। এবার একটি PR-এর পুরো পথ ধাপে ধাপে।

  1. Pushfeat/stockPR #42
  2. Concurrencycancelপুরোনো run
  3. JobsneedsDAG
  4. লালe2e (1)merge বন্ধ
  5. সবুজchecksনতুন commit
  6. PreviewURLreview
  7. Mergemainpreview মুছে
রিনার branchনমুনা
feat/stock: "স্টক শেষ" দেখানো
commit a1b2c3d → push
PR #42  feat/stock → main
event: pull_request (synchronize)
concurrency groupনমুনা
group: ci-ci-refs/pull/42/merge
আগের commit 9f8e7d6-এর run চলছিল
→ cancelled (cancel-in-progress: true)
নতুন run শুরু: a1b2c3d
Job-এর ক্রমছবি ৮১.১-এর মতো
quality      ─┐
integration  ─┼→ build → e2e (1), e2e (2)
              └────────→
e2e-report   (!cancelled)
PR-এর check তালিকানমুনা
✓ quality   ✓ integration   ✓ build
✗ e2e (1)   ✓ e2e (2)   ✓ e2e-report
Merging is blocked:
required status check failed
নতুন commit-এর পরেনমুনা
report artifact থেকে কারণ: locator-এ দুটি status
fix → commit c4d5e6f → push
সব required check: ✓
review: 1 approval
Previewনমুনা, provider-নিরপেক্ষ
URL: pr-42.preview.bazarghar.example
DB:  খালি DB + migrate deploy + seed
secret: শুধু preview-র নকল চাবি
reviewer হাতে চেপে “স্টক শেষ” দেখলেন
Merge-এর পরেনমুনা
squash merge → main@7a8b9c0
event: push (main) → ci আবার চলে
preview আর তার DB মুছে ফেলা
deploy: আলাদা workflow, অনুমোদনসহ
  1. Push. রিনা feat/stock branch-এ একটি commit push করলেন, যার PR #42 আগেই খোলা। এতে pull_request event ঘটে, আর ci.yml চালু হয়।
  2. পুরোনো run বাতিল। একই PR-এর আগের commit-এর run তখনো চলছিল। একই concurrency group, আর PR-এ cancel-in-progress সত্য, তাই সেটি বাতিল হলো; runner-এর সময় নষ্ট হলো না।
  3. Job-গুলো DAG ধরে। quality আর integration একসাথে, তারপর build, তারপর দুটি E2E shard, শেষে report. প্রতিটি job নতুন runner-এ।
  4. একটি shard লাল। e2e (1) ফেল করল। Branch protection-এ এটি required check, তাই GitHub merge বোতাম আটকে দিল। e2e-report তবু চলেছে, কারণ তার শর্ত !cancelled().
  5. Report পড়ে সংশোধন। HTML report-এর artifact থেকে কারণ পাওয়া গেল, নতুন commit push, আবার পুরো pipeline, এবার সব সবুজ। একজন reviewer অনুমোদন দিলেন।
  6. Preview দেখা। এই PR-এর নিজস্ব URL-এ অ্যাপের কপি, নিজের খালি database-এ migration আর seed. Reviewer নিজে স্টক শূন্য পণ্যে “স্টক শেষ” দেখলেন। Preview-তে শুধু preview-র নিজস্ব নকল চাবি।
  7. Merge. PR merge হলো, main-এ push event-এ CI আবার চলে, preview আর তার database মুছে যায়। Production-এ যাওয়া আলাদা workflow-এর কাজ, অনুমোদনসহ (১০ নম্বর অংশ)।

৮ · Migration pipeline-এ — একবার, traffic-এর আগে, কখনো migrate dev নয়

পর্ব ৪০-এ Prisma-র কমান্ডগুলোর কাজ ভাগ করা হয়েছিল: migrate dev নিজের কম্পিউটারে নতুন migration বানায় আর চালায়; migrate deploy শুধু আগে থেকে commit করা migration ফাইলগুলো ক্রমে চালায়, নতুন কিছু বানায় না, কিছু মোছে না। Pipeline-এর নিয়ম তিনটি। (১) Production-এ শুধু prisma migrate deploy. (২) এটি চলে একবার, একটি আলাদা job-এ, নতুন version traffic পাওয়ার আগে; প্রতিটি app instance-এর startup-এ নয়, কারণ দশটি instance একসাথে চালু হলে দশজনই migration চালাতে চাইবে। (৩) Migration এমন হবে যাতে পুরোনো আর নতুন দুই version একসাথে চলতে পারে, অর্থাৎ expand and contract.

migrate dev pipeline-এ চালালে কী হয় — দুটি সত্যিকারের run

ল্যাবের একটি আলাদা database-এ (শুধু পরীক্ষার জন্য) ইচ্ছা করে এমন অবস্থা বানানো হলো, যেখানে database-এ title নামের একটি কলাম আছে কিন্তু schema.prisma-তে নেই, যেমনটি ঘটে যখন migration চলেছে কিন্তু schema ফাইল পুরোনো। প্রথমবার টেবিল খালি ছিল। npx prisma migrate dev --name x কোনো প্রশ্ন না করেই একটি নতুন migration বানাল আর চালিয়ে দিল। তার ভেতরের SQL, সত্যিকারের ফাইল থেকে:

/*
  Warnings:

  - You are about to drop the column `title` on the `Product` table. All the data in the column will be lost.

*/
-- AlterTable
ALTER TABLE "Product" DROP COLUMN "title";

দ্বিতীয়বার টেবিলে একটি সারি ছিল। এবার migrate dev থামল, সত্যিকারের run:

$ npx prisma migrate dev --name sync
⚠️  Warnings for the current datasource:
  • You are about to drop the column `title` on the `Product` table, which still contains 1 non-null values.
Error: Prisma Migrate has detected that the environment is non-interactive, which is not supported.
`prisma migrate dev` is an interactive command designed to create new migrations and evolve the database in development.
To apply existing migrations in deployments, use prisma migrate deploy.
$ echo $?
1

দুটি ফলই বিপদের ইঙ্গিত। প্রথমটিতে টেবিল খালি থাকায় কোনো প্রশ্নই আসেনি, আর CI নিজে একটি DROP COLUMN লিখে চালিয়ে দিয়েছে, যা কেউ review করেনি। দ্বিতীয়টিতে Prisma নিজেই বলছে: deploy-এ migrate deploy. Pipeline-এ migration-এর একমাত্র উৎস হলো PR-এ review হওয়া migration.sql ফাইল।

Expand and contract — পুরোনো version ভাঙার সত্যিকারের প্রমাণ

বাজারঘরে Product.name কলামের নাম বদলে title করতে হবে। একবারে RENAME COLUMN করলে, deploy-এর কয়েক মিনিট যখন পুরোনো আর নতুন instance একসাথে চলে (rolling deployment), পুরোনোগুলো সাথে সাথে ভাঙবে। তাই তিন ধাপ: expand (নতুন কলাম যোগ আর backfill), কোড বদল, তারপর অনেক পরে contract (পুরোনো কলাম মোছা)। তত্ত্ব আর lock-এর সতর্কতা পর্ব ৪০-এ আর deployment strategy পর্বের database অংশে। এখানে দেখা যাক pipeline-এর ক্রম ভুল হলে কী হয়। ল্যাবের script একটি খালি database-এ ধাপগুলো চালিয়েছে, আর প্রতি ধাপের পরে “পুরোনো version”, অর্থাৎ যে Prisma Client শুধু name চেনে, পণ্য পড়েছে। সত্যিকারের run:

### 1. production today: only init
Applying migration `20261103112012_init`
All migrations have been successfully applied.
old version OK: মসুর ডাল
### 2. new release, step A: migrate status, then EXPAND
2 migrations found in prisma/migrations
Following migration have not yet been applied:
20261103120000_expand_add_title
To apply migrations in development run prisma migrate dev.
To apply migrations in production run prisma migrate deploy.
Applying migration `20261103120000_expand_add_title`
All migrations have been successfully applied.
old version OK: মসুর ডাল
### 3. too early: CONTRACT while the old version still runs
Applying migration `20261110120000_contract_drop_name`
All migrations have been successfully applied.
old version FAILED: P2022 - The column `Product.name` does not exist in the current database.
### 4. running deploy again changes nothing
3 migrations found in prisma/migrations
No pending migrations to apply.

Expand-এর পরে পুরোনো version দিব্যি চলেছে: নতুন কলাম সে চেনে না, কিন্তু তাতে তার কিছু আসে যায় না। Contract আগেভাগে চালাতেই একই পুরোনো version P2022 দিয়ে ভাঙল, আর migration নিজে “successfully applied” বলেছে; অর্থাৎ migration সফল হওয়া মানে অ্যাপ নিরাপদ নয়। শেষ অংশে No pending migrations to apply: migrate deploy আবার চালালে কিছু বদলায় না, তাই retry নিরাপদ। আরেকটি মাপা তথ্য: pending migration থাকা অবস্থায় prisma migrate status-এর exit code ছিল ১, আর সব চলার পরে ০, তাই এটি একটি gate-এ ব্যবহার করা যায়।

1# Release R12job migrate: migrate deploy  (expand: ADD title, backfill)3job release: v2  (title পড়ে, name ও title লেখে)4# Release R25job release: v3  (শুধু title; migration নেই)6# Release R37job migrate: migrate deploy  (contract: DROP name)8job release: v4
Production-এর অবস্থা
DB-র কলাম
চলমান version
name পড়ে কে
name লেখে কে
  1. আজকের অবস্থা। Production-এ শুধু name কলাম, চলছে v1. ল্যাবের পুরোনো version পণ্য পড়ছে: old version OK: মসুর ডাল.
  2. R1, migrate job আগে। নতুন version traffic পাওয়ার আগে একবার migrate deploy: title যোগ, পুরোনো সারিতে name থেকে মান বসানো। v1 এখনো চলছে, আর ল্যাবেও expand-এর পরে সে ঠিক আছে।
  3. R1, release job. Rolling deploy-এর সময় কিছুক্ষণ v1 আর v2 একসাথে। v2 পড়ে title, কিন্তু লেখে দুটোতেই, যাতে v1-এর পড়া name হালনাগাদ থাকে।
  4. R1 শেষ। সব instance এখন v2. কেউ আর name পড়ে না, তবে v2 এখনো লেখে, কারণ সমস্যা হলে v1-এ rollback করার পথ খোলা রাখা হয়েছে।
  5. R2: শুধু কোড। v3 আর name লেখে না। কোনো migration নেই। কিছুদিন দেখা হয়; v2-এ ফেরার দরকার থাকলে name এখনো আছে।
  6. R3, contract. এখন name কেউ ছোঁয় না, তাই DROP COLUMN নিরাপদ। R3-এর migrate job আগে চলে, তখন v3 চলছে, যে কলামটি ব্যবহারই করে না। এই ধাপ R1-এ করলে কী হতো, তা ল্যাবে দেখা গেছে: P2022.
  7. R3, release. v4 চালু। Migrate deploy আবার চালালে No pending migrations to apply. নিয়মটি এক বাক্যে: contract সবসময় সেই release-এ, যার আগের release-এর কোড পুরোনো কলাম আর ছোঁয় না।

Deploy gate — সব শর্ত একসাথে

Deploy-এর আগে pipeline যে প্রশ্নগুলো করে, তা একটি ছোট function-এ লেখা যায়: সব required check সবুজ কি না, deploy-এর build সেই commit কি না যা test হয়েছে, pending migration-এর ধরন কী আর migrate job সফল হয়েছে কি না, আর production-এর অনুমোদন আছে কি না। নিচে তার একটি শিক্ষণ মডেল; আসল pipeline-এ এই নিয়মগুলো ছড়িয়ে থাকে needs, environment-এর নিয়ম আর release-এর পরিকল্পনায়।

// Teaching model: may this build go to production now?
function gate(r) {
  const why = [];
  if (r.checks.some((c) => c.result !== 'success')) why.push('a required check is not green');
  if (r.buildSha !== r.testedSha) why.push('the build is not the commit that was tested');
  if (r.pendingMigrations.some((m) => m.kind === 'contract' && r.oldVersionLive))
    why.push('a contract migration while the old version still serves traffic');
  if (r.pendingMigrations.length && !r.migrateJobPassed) why.push('migrate deploy has not succeeded yet');
  if (r.env === 'production' && !r.approvedBy) why.push('production needs a reviewer approval');
  return why.length ? `BLOCK: ${why.join('; ')}` : 'DEPLOY';
}
const green = [{ name: 'quality', result: 'success' }, { name: 'e2e', result: 'success' }];
const base = { env: 'production', checks: green, buildSha: 'a1b2c3d', testedSha: 'a1b2c3d',
  pendingMigrations: [], migrateJobPassed: true, oldVersionLive: true, approvedBy: 'rina' };

const cases = {
  'all good': base,
  'e2e failed': { ...base, checks: [green[0], { name: 'e2e', result: 'failure' }] },
  'expand pending, migrate ran': { ...base, pendingMigrations: [{ kind: 'expand' }] },
  'expand pending, migrate failed': { ...base, pendingMigrations: [{ kind: 'expand' }], migrateJobPassed: false },
  'contract too early': { ...base, pendingMigrations: [{ kind: 'contract' }] },
  'contract after old is gone': { ...base, pendingMigrations: [{ kind: 'contract' }], oldVersionLive: false },
  'rebuilt from main, no approval': { ...base, buildSha: 'f9e8d7c', approvedBy: null },
};
for (const [name, r] of Object.entries(cases)) console.log(name.padEnd(31), gate(r));

all good DEPLOY আর expand pending, migrate ran DEPLOY: expand migration সফল হলে নতুন version যেতে পারে। expand pending, migrate failed BLOCK: migrate deploy has not succeeded yet: schema তৈরি না হলে নতুন কোড নতুন কলাম খুঁজে ভাঙবে, তাই release থামে; এজন্যই আসল workflow-এ release job-এর needs: [migrate]. contract too early BLOCK: …old version still serves traffic হলো ল্যাবের P2022-এর নিয়ম, আর contract after old is gone DEPLOY. শেষ লাইনে দুটি কারণ একসাথে: the build is not the commit that was tested; production needs a reviewer approval. Deploy-এর সময় নতুন করে build করলে যা যাচ্ছে তা test হওয়া জিনিস নয়; তাই SHA মেলানো।

৯ · Secret, fork আর supply chain — pipeline-এর নিরাপত্তা

CI-র runner production database-এর চাবি, cloud-এর অধিকার আর package প্রকাশের token ছুঁতে পারে, তাই এটি আক্রমণের আকর্ষণীয় লক্ষ্য। নিচের সব পরামর্শ রক্ষার দিক থেকে; threat model-এর পূর্ণ তত্ত্ব DevSecOps পর্বে আর Cybersecurity-র supply chain পর্বে। App-এর environment আর secret কীভাবে ভাগ করবেন, তা পর্ব ৭৮-এ।

Secret কোথায় থাকে, কে পায়

GitHub-এ secret রাখা যায় তিন স্তরে: organization, repository, আর environment. Environment secret শুধু সেই job পায়, যে environment: production ঘোষণা করে, আর সেই environment-এর সুরক্ষার নিয়ম (যেমন reviewer-এর অনুমোদন) পূরণ হওয়ার পরে। তাই production database-এর DATABASE_URL repository secret নয়, environment secret: quality বা e2e job সেটি কখনো দেখে না। Log-এ secret-এর মান GitHub মুছে *** দেখায়, কিন্তু মানটি বদলে (যেমন base64 করে) ছাপলে সেই পাহারা কাজ করে না; secret কখনো ছাপবেন না।

আরও ভালো হলো দীর্ঘস্থায়ী চাবি না রাখা। OIDC federation-এ job permissions: id-token: write নিয়ে GitHub থেকে একটি স্বাক্ষর করা, স্বল্পমেয়াদি পরিচয়পত্র নেয়, যাতে লেখা থাকে কোন repository, কোন branch বা environment থেকে job চলছে। Cloud provider আগে থেকে সেই শর্তে বিশ্বাস ঠিক করে রাখে, আর কয়েক মিনিটের অস্থায়ী অধিকার দেয়। চুরি করার মতো স্থায়ী key কোথাও থাকে না। বড় cloud provider-রা এটি সমর্থন করে বলে তাদের docs-এ লেখা; ঠিক কোন শর্ত লিখতে হয়, তা provider অনুযায়ী আলাদা, আর এখানে কোনো cloud-এ চালানো হয়নি। Protocol-এর ভিত্তি OpenID Connect, যা পর্ব ৪৭-এ এসেছিল।

Fork থেকে pull request — কেন secret দেওয়া হয় না

Open-source repository-তে যে কেউ fork করে PR খুলতে পারেন, আর সেই PR-এর কোড CI-তে চলে। কোডটি অচেনা মানুষের লেখা, তাই GitHub-এর docs অনুযায়ী fork থেকে আসা pull_request run-এ repository-র secret দেওয়া হয় না (GITHUB_TOKEN ছাড়া), আর সেই token-ও শুধু পড়ার অধিকার পায়। ফলে fork-এর PR-এ যে job secret ছাড়া চলে না (preview deploy, cloud-এ upload), সেটি ফেল বা skip হওয়াই প্রত্যাশিত, আর এটি ভালো খবর।

এখান থেকে একটি বিপজ্জনক “সমাধান” জন্মায়: pull_request_target. এই event-এ workflow চলে base repository-র প্রেক্ষাপটে, base branch-এর workflow ফাইল দিয়ে, secret আর লেখার অধিকারসহ। নকশাটি তৈরি PR-এ label দেওয়া বা মন্তব্য করার মতো কাজের জন্য, যেখানে PR-এর কোড চালানো হয় না। কেউ যদি এখানে PR-এর head checkout করে npm ci বা test চালান, তাহলে অচেনা কোড secret-সহ চলবে: PR-এর package.json-এর একটি script-ই যথেষ্ট। GitHub Security Lab ২০২১ সালের দিকে এর নাম দিয়েছিল “pwn request”. লেখার সময়ের একটি পরিবর্তন: actions/checkout-এর v7 (জুন ২০২৬) আর পরে backport হওয়া পুরোনো major version, GitHub-এর changelog অনুযায়ী, pull_request_target-এ fork-এর PR-এর head বা merge commit checkout করতে নিজে থেকেই অস্বীকার করে; ইচ্ছা করে খুলতে allow-unsafe-pr-checkout দিতে হয়। একই changelog বলে, SHA-তে পিন করা workflow এই backport নিজে থেকে পায় না, হাতে বা Dependabot দিয়ে হালনাগাদ করতে হয়। আর এটি run step-এ git দিয়ে নিজে PR-এর কোড আনা ঠেকায় না।

নিয়ম

অচেনা কোড চলবে শুধু pull_request-এ, secret ছাড়া, পড়ার অধিকারে। pull_request_target বা workflow_run-এ PR-এর কোড checkout বা চালানো নয়; PR-এর লেখা (শিরোনাম, branch-এর নাম) shell-এ সরাসরি নয়, env দিয়ে।

Supply chain — action, package আর lockfile

Workflow-এর প্রতিটি uses: অন্যের কোড, যা আপনার secret-এর পাশে চলে। @v7-এর মতো tag একটি সরানো যায় এমন নাম: action-এর মালিকের account বা repository দখল হলে tag-টি নতুন, ক্ষতিকর commit-এ সরানো যায়, আর আপনার পরের run সেটিই চালাবে। এটি তাত্ত্বিক নয়। মার্চ ২০২৫-এ একটি জনপ্রিয় action, tj-actions/changed-files, আপস হয়, আর প্রকাশিত বিবরণ অনুযায়ী তার বহু version tag একটি ক্ষতিকর commit-এ সরানো হয়, যা runner-এর memory থেকে secret পড়ে build log-এ ছাপত; ঘটনাটি CVE-2025-30066 নামে নথিভুক্ত। যাঁরা পূর্ণ commit SHA-তে পিন করেছিলেন, tag সরানো তাঁদের workflow-এ পৌঁছায়নি।

তাই SHA pinning: uses: actions/checkout@3d3c42e5…ba90b1 # v7.0.1. Git-এ commit-এর পরিচয় তার বিষয়বস্তুর hash, তাই একই SHA মানে একই কোড। এর দাম: নিরাপত্তা-সংশোধন আর নিজে থেকে আসে না (উপরের checkout-এর উদাহরণই তা দেখায়)। সমাধান Dependabot বা অনুরূপ টুল, যা নতুন version-এর SHA-সহ PR খোলে, আর মানুষ তা review করেন। নিচের ব্লক workflow-এর লেখা থেকে পিন না করা action খোঁজে; এটি একটি শিক্ষণ মডেল, আসল কাজে যাচাই করা টুল ব্যবহার করুন।

// Teaching model: flag "uses:" lines that are not pinned to a full 40-character commit SHA.
const workflow = `
jobs:
  ci:
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - uses: actions/setup-node@v7
      - uses: some-org/deploy-action@main
      - uses: actions/upload-artifact@cf430e0
      - uses: ./.github/actions/seed-db
      - uses: docker://alpine:3.22
      - run: npx playwright test
`;
const FULL_SHA = /^[0-9a-f]{40}$/;
let problems = 0;
workflow.split('\n').forEach((line, i) => {
  const m = line.match(/uses:\s*([^\s#]+)/);
  if (!m) return;
  const ref = m[1];
  if (ref.startsWith('./')) return console.log(`line ${i}: local action, lives in this repo  ${ref}`);
  if (ref.startsWith('docker://')) return console.log(`line ${i}: container image, pin by @sha256 digest  ${ref}`);
  const [, version] = ref.split('@');
  if (FULL_SHA.test(version ?? '')) return console.log(`line ${i}: ok, pinned  ${ref.slice(0, 32)}…`);
  problems++;
  const why = !version ? 'no version' : /^[0-9a-f]{7,39}$/.test(version) ? 'short SHA, not a full pin' : `"${version}" is a movable tag or branch`;
  console.log(`line ${i}: NOT PINNED (${why})  ${ref}`);
});
console.log(`${problems} action(s) to pin`);

line 4: ok, pinned actions/checkout@3d3c42e5aac5ba8…: পূর্ণ ৪০ অক্ষরের SHA. line 5: NOT PINNED ("v7" is a movable tag or branch) আর line 6: … ("main" …): একটি tag আর একটি branch, দুটোই সরানো যায়; branch-এ পিন সবচেয়ে বিপজ্জনক, কারণ প্রতিটি নতুন commit সাথে সাথে আপনার CI-তে চলে। line 7: NOT PINNED (short SHA, not a full pin): ছোট SHA দেখতে নিরাপদ, কিন্তু পূর্ণ পরিচয় নয়। line 8: local action: নিজের repository-র কোড, PR-এর review-ই তার পাহারা। line 9: container image, pin by @sha256 digest: image-এর tag-ও সরানো যায়, তাই digest. শেষে 3 action(s) to pin. লাইন নম্বর template-এর শুরুর খালি লাইন থেকে গোনা।

Package-এর দিকে তিনটি অভ্যাস। (১) npm ci আর commit করা lockfile: install-এ নতুন version ঢোকে না, আর প্রতিটি package-এর integrity hash মেলানো হয়। (২) npm audit একটি step হিসেবে, একটি স্পষ্ট সীমাসহ। ল্যাবে দুটি সত্যিকারের ফল পাশাপাশি:

$ npm audit --omit=dev --audit-level=high
found 0 vulnerabilities
$ echo $?
0

$ npm audit --audit-level=moderate
# npm audit report

minimatch  <11.0.1
Severity: moderate
minimatch has potential denial of service via globparts amplification - https://github.com/advisories/GHSA-g3hv-75wg-m249
fix available via `npm audit fix --force`
Will install eslint@4.0.0, which is a breaking change
…
16 moderate severity vulnerabilities
$ echo $?
1

Production-এর dependency-তে কিছু নেই; ১৬টি moderate সতর্কতা সব ESLint-এর tool chain-এর ভেতরে, যা শুধু development-এ চলে। আর npm-এর প্রস্তাবিত npm audit fix --force ESLint-কে 9 থেকে 4.0.0-এ নামিয়ে দিত, যা পুরো lint ভেঙে দিত। তাই pipeline-এ সিদ্ধান্তটি সচেতন: production dependency-তে high বা তার বেশি হলে লাল, development tool-এর moderate আলাদাভাবে, মানুষের বিচারে। Dependency-র নিরাপত্তার তালিকা পর্ব ৫০-এ। (৩) নতুন package যোগের PR-এ নাম আর প্রকাশক যাচাই, কারণ কাছাকাছি বানানের ক্ষতিকর package (typosquatting) একটি পরিচিত কৌশল।

১০ · Deploy, অনুমোদন, rollback আর দ্রুত CI

বাজারঘরের deploy একটি আলাদা workflow, হাতে চালানো (workflow_dispatch), যাতে input হিসেবে দেওয়া হয় কোন commit-এর build যাবে। দুটি job: migrate আর release, দুটোই environment: production-এ, আর release-এর needs: [migrate]. Environment-এর নিয়মে reviewer-এর অনুমোদন চালু থাকলে job শুরু হওয়ার আগে GitHub থেমে একজন নির্দিষ্ট মানুষের সম্মতি চায়; কোন plan-এ, কোন ধরনের repository-তে এই সুবিধা পাওয়া যায়, তা GitHub-এর বর্তমান docs-এ দেখে নিন। Concurrency-তে cancel-in-progress: false: অর্ধেক হওয়া deploy বাতিল করা অর্ধেক হওয়া migration-এর মতোই বিপজ্জনক, তাই নতুন deploy সারিতে অপেক্ষা করে।

on:
  workflow_dispatch:
    inputs:
      build_sha:
        description: 'Commit SHA whose build to deploy (rollback = an older SHA)'
        required: true
concurrency:
  group: deploy-production
  cancel-in-progress: false
jobs:
  migrate:
    environment: production          # অনুমোদন, তারপর environment secret
    steps:
      # checkout ${{ inputs.build_sha }}, setup-node, npm ci
      - run: npx prisma migrate deploy
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
  release:
    needs: [migrate]
    environment: production
    permissions: { contents: read, id-token: write }   # OIDC
    steps:
      - name: Deploy the stored build of this SHA
        env:
          BUILD_SHA: ${{ inputs.build_sha }}
        run: ./scripts/deploy.sh "$BUILD_SHA"     # provider-নিরপেক্ষ, এখানে চালানো হয়নি

deploy.sh ইচ্ছা করে কোনো provider-এর নয়: কোথাও এটি একটি container image বদলায়, কোথাও একটি PaaS-এর CLI চালায়, কোথাও VPS-এ ফাইল পাঠিয়ে process manager restart করে (পর্ব ৮২)। Health check-এর জন্য পর্ব ৩১-এর /health কাজে লাগে, আর deploy-এর পরে error বাড়ছে কি না দেখা পর্ব ৮৩-এর (Monitoring) বিষয়।

Rollback — আগের build আবার, database নয়

Deploy-এর পরে কিছু ভাঙলে প্রথম কাজ ক্ষতি থামানো। সবচেয়ে দ্রুত আর নিরাপদ rollback হলো আগের, জানা-ভালো commit-এর build আবার deploy করা: একই workflow, input-এ পুরোনো SHA. এখানে আবার build নয়, কারণ নতুন build মানে নতুন, পরীক্ষা না করা জিনিস। Database পেছনে নেওয়া হয় না: down migration ডেটা হারাতে পারে, আর expand-only migration-এর পরে পুরোনো কোড এমনিই চলে, ল্যাবে যেমন দেখা গেছে। যেখানে পুরোনো build চালানো সম্ভব নয় (যেমন contract-এর পরে), সেখানে পথ হলো দ্রুত সংশোধন করে নতুন release, অর্থাৎ roll forward.

  1. চলছে7a8b9c0v41
  2. Deployc4d5e6fv42 + expand
  3. Error বাড়ে5xxcheckout
  4. Redeploy7a8b9c0migrate: কিছু নেই
  5. স্বাভাবিক/healthv41
  6. FixPRv43
Productionনমুনা
build: next-build-7a8b9c0  (v41)
DB migrations: init
error rate: স্বাভাবিক
deploy.yml, build_sha=c4d5e6fনমুনা
migrate (অনুমোদনের পরে):
  Applying `…_expand_add_title`   ✓
release: next-build-c4d5e6f  (v42)
Monitoringনমুনা
POST /checkout → 500 বাড়ছে
কারণ: v42-এর একটি বাগ (DB নয়)
সিদ্ধান্ত: আগে ক্ষতি থামানো, পরে খোঁজা
deploy.yml, build_sha=7a8b9c0নমুনা
migrate: No pending migrations to apply.
  (title কলাম থেকে যায়; v41 তা ছোঁয় না)
release: next-build-7a8b9c0  (নতুন build নয়)
যাচাইনমুনা
GET /health → 200
checkout-এর error rate আগের মতো
ঘটনার নোট: কখন, কী, কে, কেন
Roll forwardনমুনা
বাগের জন্য আগে একটি ফেল করা test
সংশোধন → PR → সব check সবুজ
deploy build_sha=d7e8f90  (v43)
  1. স্বাভাবিক অবস্থা। Production-এ চলছে commit 7a8b9c0-এর build, অর্থাৎ v41, CI-তে পরীক্ষিত।
  2. নতুন release. c4d5e6f deploy: অনুমোদনের পরে migrate job expand migration চালায়, তারপর release job v42-এর সংরক্ষিত build চালু করে।
  3. কিছু ভাঙল। Checkout-এ 500 বাড়ছে। কারণ কোডের একটি বাগ, database-এর নয়। প্রথম লক্ষ্য খোঁজা নয়, ক্রেতার ক্ষতি থামানো।
  4. আগের build আবার। একই deploy workflow, input-এ 7a8b9c0. Migrate job-এ কিছু চালানোর নেই; expand-এর title কলাম থেকে যায়, v41 তা ব্যবহার করে না, তাই কোনো ক্ষতি নেই। Release-এ সংরক্ষিত পুরোনো build, নতুন করে build নয়।
  5. যাচাই। /health-এ 200, error rate আগের জায়গায়। ঘটনার একটি ছোট নোট লেখা হয়, দোষারোপ ছাড়া।
  6. সামনে এগোনো। বাগটি ধরার জন্য আগে একটি ফেল করা test, তারপর সংশোধন, PR, সব check সবুজ, নতুন release v43. পুরোনো build-এ ফেরা ছিল সাময়িক ব্যবস্থা, সমাধান নয়।

CI দ্রুত রাখা — মাপা সময় থেকে সিদ্ধান্ত

ল্যাবের script (scripts/ci-local.mjs) workflow-এর সব ধাপ একই ক্রমে, fail-fast-সহ, একটি কম্পিউটারে চালিয়ে প্রতিটির সময় লিখেছে। শেষ পূর্ণ run-এর সত্যিকারের সারণি, একটি স্থানীয় run, CI-র মেশিনে সংখ্যা আলাদা হবে:

$ node scripts/ci-local.mjs
…
step             seconds  result
install             20.0  ok
audit                0.8  ok
generate             1.4  ok
lint                 2.1  ok
typecheck            1.7  ok
unit+component       1.8  ok
fresh-db             0.3  ok
migrate              1.3  ok
integration          1.1  ok
build                5.1  ok
seed-e2e             0.2  ok
e2e 1/2              3.5  ok
e2e 2/2              3.4  ok
total               42.7

সংখ্যাগুলো থেকে কয়েকটি শিক্ষা। Install-ই সবচেয়ে বড়: ৪২.৭-এর ২০.০, প্রায় ৪৭%; আর খালি cache-এ সেটি ১০০ সেকেন্ড। তাই cache-ই প্রথম অগ্রাধিকার। প্রতিটি job install-এর দাম আবার দেয়: pipeline-এ পাঁচটি job মানে পাঁচবার install; ছোট project-এ সব একটি job-এ রাখলেই হয়তো দ্রুত, ২ নম্বর অংশের মডেলের মতো। সস্তা আগে: lint, typecheck, unit মিলে ৫.৬ সেকেন্ডে বেশির ভাগ সাধারণ ভুল ধরে; build আর E2E তার পরে। পুরোনো run বাতিল (concurrency) আর সময়সীমা (timeout-minutes), যাতে আটকে থাকা job ঘণ্টার পর ঘণ্টা runner না খায়। Shard তখনই, যখন E2E-র সময় প্রতি shard-এর স্থির খরচের চেয়ে অনেক বেশি।

আরও গভীরে: GitHub Actions-এর প্রতিটি job আসলে একটি নতুন virtual machine বা container, যেখানে runner নামের একটি agent GitHub-এর সেবা থেকে কাজ নেয়, step চালায় আর log পাঠায়; ${{ }} expression runner-এ পৌঁছানোর আগেই মূল্যায়ন হয়, এজন্যই তা shell-এ বসালে injection হয়। Supply chain-এর নিরাপত্তার মান হিসেবে SLSA (Supply-chain Levels for Software Artifacts) build-এর provenance, অর্থাৎ কোন source থেকে কোন builder-এ কী তৈরি হলো তার স্বাক্ষরিত প্রমাণ, স্তরে স্তরে চায়; npm-এর provenance আর Sigstore-এর মতো উদ্যোগ এর অংশ। আর DORA-র গবেষণা deployment frequency, lead time, change failure rate আর recovery time মেপে দেখায় যে ঘন ঘন ছোট release আর স্থিতিশীলতা একে অন্যের শত্রু নয় (Continuous delivery পর্ব)।

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

  • CI-তে npm install নয় কেন? npm ci lockfile-এর হুবহু version বসায়, না মিললে থামে, lockfile বদলায় না।
  • Migration pipeline-এর কোথায়? একটি আলাদা job-এ migrate deploy, নতুন version traffic পাওয়ার আগে, একবার।
  • Fork-এর PR-এ secret নেই কেন? কোডটি অচেনা; secret দিলে যে কেউ তা পড়ে নিতে পারত।
  • Action-কে SHA-তে পিন কেন? Tag সরানো যায়, commit SHA বিষয়বস্তুর hash, তাই বদলায় না।
  • Rollback-এ database? সাধারণত না; expand-only migration-এর পরে আগের build আবার deploy, প্রয়োজনে roll forward.

AI-এর যুগে CI/CD

AI টুল একটি পুরো workflow কয়েক সেকেন্ডে লিখে দেয়, আর সেখানেই বিপদ। সাধারণ ভুলগুলো: পুরোনো action version (@v3, যা actionlint ল্যাবে ধরেছে), npm install, CI-তে prisma migrate dev বা db push, next lint (Next.js 16-এ নেই), সব secret repository স্তরে আর সব job-এ permissions: write-all, fork-এর PR-এ preview চালাতে pull_request_target আর তার মধ্যে PR-এর head checkout, ব্যর্থতা ঢাকতে continue-on-error: true, আর কখনো কখনো অস্তিত্বহীন action বা option. যাচাইয়ের পথ: action-এর release পাতা আর framework-এর version-মিলানো docs পড়া, actionlint চালানো, প্রতিটি step নিজের কম্পিউটারে একবার চালানো, diff লাইন ধরে পড়া, আর কখনো আসল secret prompt-এ না দেওয়া।

AI-কে version, নিরাপত্তার শর্ত আর migration-এর নিয়ম স্পষ্ট বলুন, আর ফল actionlint দিয়ে যাচাই করুন।

আমার অ্যাপ Next.js 16, Prisma 7 (PostgreSQL), Vitest আর Playwright দিয়ে। GitHub Actions-এর জন্য একটি CI workflow লিখে দাও। শর্ত: ১. শুরুতে permissions: contents: read; কোনো job-এ বাড়তি অধিকার লাগলে শুধু সেই job-এ, কারণসহ মন্তব্য। ২. প্রতিটি uses: একটি পূর্ণ commit SHA-তে, পাশে মন্তব্যে version. কোন SHA দিচ্ছ তা না জানলে বানিয়ে লিখবে না, "SHA_HERE" রেখে বলবে কোথা থেকে নিতে হবে। ৩. Install শুধু npm ci; lint হবে npx eslint . (next lint নয়); তারপর tsc --noEmit, vitest run. ৪. Integration test-এর আগে postgres service container আর npx prisma migrate deploy; migrate dev বা db push কোথাও নয়। ৫. Playwright দুটি shard-এ, blob report, আর একটি merge job; fail-fast: false. ৬. Workflow চলবে শুধু pull_request আর main-এ push-এ; pull_request_target ব্যবহার করবে না। PR-এর শিরোনাম বা branch-এর নাম run-এর ভেতরে সরাসরি বসাবে না। ৭. কোনো secret-এর আসল মান লিখবে না। শেষে বলো কোন অংশ তুমি নিশ্চিত নও; "এটি চলবে" বলবে না, কারণ তুমি চালাওনি।

AI-এর লেখা pipeline মেলানোর চেকলিস্ট

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

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

প্র ০১CI-এর নামে “integration” কেন? শুধু test চালানোর যন্ত্র হিসেবে CI-কে দেখলে কী হারায়?

১৯৯০-এর দশকে বড় সফটওয়্যার প্রকল্পে একটি পরিচিত দুঃস্বপ্ন ছিল “integration phase”: কয়েক মাস ধরে প্রতিটি দল নিজের অংশ আলাদা লিখত, তারপর শেষে সব জোড়া দিতে গিয়ে সপ্তাহের পর সপ্তাহ conflict আর অপ্রত্যাশিত বাগ। Grady Booch-এর বইয়ে (১৯৯১) আর পরে Kent Beck-এর Extreme Programming-এ প্রস্তাবটি ছিল উল্টো: জোড়া দেওয়াকে বড় ঘটনা না বানিয়ে প্রতিদিন, বারবার করা, যাতে প্রতিবারের পার্থক্য ছোট থাকে। Martin Fowler-এর ২০০০ সালের প্রবন্ধে CI-এর অনুশীলনগুলো তালিকাভুক্ত হয়: একটি repository, স্বয়ংক্রিয় build, নিজে-পরীক্ষা-করা build, প্রত্যেকে প্রতিদিন mainline-এ commit, প্রতিটি commit-এ build, ভাঙা build দ্রুত সারানো।

অর্থাৎ CI-এর মূল কথা একটি অভ্যাস: ছোট পরিবর্তন, ঘন ঘন মেলানো, আর মেলানোর প্রতিটি ফল সবার কাছে দৃশ্যমান। স্বয়ংক্রিয় test সেই অভ্যাসকে নিরাপদ করে। কেউ যদি তিন সপ্তাহ একটি feature branch-এ কাজ করে শেষে একটি বিশাল PR খোলেন, আর সেই PR-এ চমৎকার pipeline সবুজ হয়, তবু সেটি অনেকাংশে CI নয়: integration-এর ঝুঁকি জমেছে তিন সপ্তাহ ধরে, শুধু pipeline সেটি শেষে একবার দেখেছে। Software Engineering-এর branching পর্বে এই ঝুঁকির হিসাব আছে।

বাজারঘরের অভিজ্ঞতাও একই কথা বলে। <output>-এর implicit role-এর ভুলটি ছোট PR-এ সাথে সাথে ধরা পড়েছে, কারণটি এক নজরে বোঝা গেছে। একই ভুল পঞ্চাশটি ফাইলের PR-এ থাকলে খোঁজা কঠিন হতো। আবার cacheComponents-এর build error দেখায়, “আমার কম্পিউটারে চলে” আর “পরিষ্কার পরিবেশে চলে” এক নয়।

মূল উপলব্ধি: CI একটি যন্ত্র নয়, একটি অভ্যাস: ছোট ধাপে ঘন ঘন মেলানো। Pipeline সেই অভ্যাসের পাহারাদার; বড়, দেরিতে মেলানো branch-কে সে নিরাপদ করতে পারে না।

প্র ০২অনেক টিউটোরিয়াল অ্যাপ চালু হওয়ার সময় নিজেই migration চালায়। বাজারঘর আলাদা job-এ চালায় কেন? ছোট অ্যাপে কি এতটা দরকার?

Startup-এ migration সুবিধাজনক: একটি কমান্ডে সব হয়ে যায়, আলাদা ধাপ মনে রাখতে হয় না। সমস্যা দেখা দেয় যখন instance একাধিক। Rolling deploy-এ দশটি নতুন instance প্রায় একসাথে চালু হয়, আর প্রত্যেকে migration চালাতে চায়। Prisma Migrate একটি advisory lock নেয়, তাই একসাথে দুজন migration লিখবে না (পর্ব ৪০), কিন্তু বাকিরা lock-এর জন্য অপেক্ষা করে, startup ধীর হয়, আর health check-এর সময়সীমা পেরিয়ে গেলে orchestrator instance-কে খারাপ ধরে নতুন করে চালাতে পারে। আর migration ব্যর্থ হলে সেটি দশটি instance-এর log-এ ছড়িয়ে থাকে, একটি স্পষ্ট লাল job হিসেবে নয়।

দ্বিতীয় যুক্তি অধিকারের। Migration-এর জন্য টেবিল বানানো আর মোছার অধিকার লাগে; চলমান অ্যাপের দরকার শুধু সারি পড়া আর লেখা। আলাদা job থাকলে migration-এর জন্য একটি শক্তিশালী database user, আর অ্যাপের জন্য সীমিত user রাখা যায়, এবং শক্তিশালী চাবিটি শুধু অনুমোদিত migrate job-এর environment secret-এ থাকে। এটি least privilege-এর সরাসরি প্রয়োগ।

তৃতীয় যুক্তি ক্রমের। আলাদা job-এ “migration সফল হলে তবেই release” নিয়মটি needs দিয়ে লেখা যায়, আর ব্যর্থ হলে পুরোনো version অক্ষত থাকে। কিছু PaaS-এর docs-এ একটি “release phase” বা “pre-deploy command”-এর ধারণা আছে, যা ঠিক এই কাজটিই করে: নতুন version চালুর আগে একবার একটি কমান্ড।

তবে সবকিছুর দাম আছে। একটি shared hosting বা একটিমাত্র VPS-এ একটিমাত্র process চলা ছোট অ্যাপে startup-এ migrate deploy চালানোও যুক্তিসঙ্গত হতে পারে, যতক্ষণ সেটি migrate deploy, migration-গুলো review করা, আর ব্যর্থ হলে অ্যাপ চালুই হয় না। প্রশ্নটি তাই “কোনটি সবসময় ঠিক” নয়, বরং “আমার কতগুলো instance, আর migration ব্যর্থ হলে কে জানবে”।

মূল উপলব্ধি: Migration একটি একক ঘটনা, instance-এর সংখ্যার সাথে বাড়ে না। তাকে pipeline-এর একটি স্পষ্ট, একবারের, অধিকার-সীমিত ধাপ বানালে ব্যর্থতা দৃশ্যমান আর release নিরাপদ হয়।

প্র ০৩সব action-কে commit SHA-তে পিন করা কি নিখুঁত সমাধান? এর বিনিময়ে কী দিতে হয়?

পিন করার পক্ষে যুক্তি শক্ত। Git-এর commit SHA তার বিষয়বস্তুর hash, তাই একই SHA মানে হুবহু একই কোড; কেউ action-এর repository দখল করলেও পুরোনো commit বদলাতে পারে না। মার্চ ২০২৫-এর tj-actions/changed-files-এর ঘটনায় প্রকাশিত বিবরণ অনুযায়ী আক্রমণকারী version tag-গুলো একটি ক্ষতিকর commit-এ সরিয়ে দিয়েছিল, আর যাঁরা tag ব্যবহার করতেন, তাঁদের পরের run-এ secret log-এ ছাপা হয়েছিল। SHA-তে পিন করা workflow সেই পরিবর্তন পায়নি।

কিন্তু একই বৈশিষ্ট্য উল্টো দিকেও কাটে। পিন মানে ভালো পরিবর্তনও নিজে আসে না। লেখার সময়ের একটি উদাহরণ এখানে খুব স্পষ্ট: GitHub-এর changelog অনুযায়ী actions/checkout ২০২৬ সালে pull_request_target-এর একটি বিপজ্জনক ধরন নিজে থেকে আটকাতে শুরু করে, আর পুরোনো major version-এও backport করে। @v4-এর মতো চলমান tag ব্যবহারকারীরা সুরক্ষাটি নিজে থেকে পেয়েছেন; পুরোনো SHA-তে পিন করা workflow পায়নি, যতক্ষণ না কেউ হালনাগাদ করেছেন।

তাই পিন একা যথেষ্ট নয়, তার সাথে একটি হালনাগাদের প্রক্রিয়া চাই। Dependabot বা অনুরূপ টুল নতুন release এলে SHA বদলের PR খোলে; সেই PR-এ release notes পড়া, পরিবর্তনের diff দেখা, আর CI সবুজ হলে merge. আরও কিছু স্তর আছে: কম action ব্যবহার করা (অনেক কাজ একটি run লাইনেই হয়), শুধু বিশ্বস্ত প্রকাশকের action, permissions ন্যূনতম, যাতে কোনো action দখল হলেও ক্ষতি সীমিত থাকে। GitHub-এর organization স্তরে “শুধু পিন করা action অনুমোদিত” ধরনের নীতিও আছে বলে docs-এ উল্লেখ আছে।

মূল উপলব্ধি: পিন করা মানে নিয়ন্ত্রণ নিজের হাতে নেওয়া, আর নিয়ন্ত্রণের সাথে আসে হালনাগাদের দায়িত্ব। পিন আর নিয়মিত, review-করা হালনাগাদ একসাথে হলেই তা নিরাপত্তা।

প্র ০৪প্রতিটি PR-এর জন্য preview deployment আর preview database: লাভ কোথায়, আর কোন জায়গায় এটি বিপদ ডেকে আনে?

Preview-এর বড় লাভ যোগাযোগে। Designer, product-এর মালিক বা ক্রেতার প্রতিনিধি কোড পড়েন না, কিন্তু একটি URL খুলে পরিবর্তনটি দেখতে পারেন। “স্টক শেষ” বার্তা কোথায় বসেছে, মোবাইলে কেমন দেখায়, এসব প্রশ্নের উত্তর merge-এর আগেই আসে। E2E test-ও একটি preview URL-এর বিরুদ্ধে চালানো যায়, যা production-এর পরিবেশের কাছাকাছি।

Database যোগ হলেই প্রশ্ন জটিল। প্রতি PR-এ নতুন খালি database সবচেয়ে নিরাপদ আর পরিষ্কার, কিন্তু আসল ডেটার আকার, পুরোনো অদ্ভুত সারি, বা ধীর query দেখায় না। Database branching, যা কিছু managed PostgreSQL সেবা copy-on-write পদ্ধতিতে দেয় বলে তাদের docs-এ লেখা, বাস্তবের মতো ডেটা দেয় প্রায় সাথে সাথে। কিন্তু production থেকে branch নিলে ক্রেতার নাম, ফোন, ঠিকানা preview-তে চলে আসে, যার URL হয়তো সহজে শেয়ার হয়। বাংলাদেশসহ অনেক দেশে ব্যক্তিগত তথ্য সুরক্ষা নিয়ে আইন আর নীতি তৈরি হচ্ছে; ঠিক কী বাধ্যতামূলক, সেই আইনি প্রশ্ন আইনজ্ঞের, কিন্তু প্রকৌশলের দিক থেকে নিরাপদ অভ্যাস স্পষ্ট: masked বা নকল ডেটার parent থেকে branch, preview-তে login বাধ্যতামূলক, আর PR বন্ধ হলে সব মুছে ফেলা।

দ্বিতীয় বিপদ secret. Preview চালাতে কোনো না কোনো চাবি লাগে। Fork থেকে আসা PR-এ সেই চাবি দিলে অচেনা কোড তা পড়তে পারে; তাই fork-এর PR-এ preview হয় বন্ধ, নয়তো একজন maintainer কোড পড়ে হাতে চালু করেন। তৃতীয় বিপদ খরচ আর জঞ্জাল: খোলা PR-এর সংখ্যা যত, চলমান preview আর database তত; বন্ধ PR-এর preview মুছে না ফেললে খরচ আর আক্রমণের জায়গা দুটোই বাড়ে।

মূল উপলব্ধি: Preview একটি ছোট production, তাই production-এর সব প্রশ্ন তার জন্যও: কোন ডেটা, কোন চাবি, কে দেখতে পায়, আর কখন মুছে যায়।

প্র ০৫“দ্রুত CI” আর “পূর্ণ CI” কি পরস্পরবিরোধী? কোন check কোথায় চালানো উচিত?

Feedback-এর গতি মানুষের মনোযোগের সাথে জড়িত। পাঁচ মিনিটে ফল এলে ডেভেলপার অপেক্ষা করে সাথে সাথে সারান; চল্লিশ মিনিট লাগলে তিনি অন্য কাজে চলে যান, ফিরে এসে প্রসঙ্গ মনে করতে সময় যায়, আর অনেকে একসাথে কয়েকটি পরিবর্তন জমিয়ে push করতে শুরু করেন, যা CI-এর মূল অভ্যাসের উল্টো। DORA-র গবেষণাও ছোট batch আর কম lead time-কে ভালো delivery-র লক্ষণ হিসেবে দেখায়।

সমাধান স্তর ভাগ করা। সবচেয়ে দ্রুত স্তর নিজের কম্পিউটারে: editor-এর lint, আর ল্যাবের মতো একটি pre-push hook, যা ৫.৫ সেকেন্ডে lint, type check আর unit test চালায়। কিন্তু hook --no-verify দিয়ে এড়ানো যায়, তাই এটি সুবিধা, প্রমাণ নয়। দ্বিতীয় স্তর প্রতিটি PR-এ: সেই একই check, সাথে integration, build আর জরুরি পথের E2E, cache আর parallel job-সহ, যাতে কয়েক মিনিটে শেষ হয়। তৃতীয় স্তর কম ঘন ঘন: অনেক browser-এ পূর্ণ E2E, দীর্ঘ performance test, পূর্ণ dependency scan, যা হয়তো main-এ merge-এর পরে বা প্রতি রাতে চলে।

এতে একটি ঝুঁকি আছে: রাতের check ফেল করলে তখন main-এ ইতিমধ্যে কয়েকটি পরিবর্তন, কোনটি দায়ী খুঁজতে হয়। তাই রাতের স্তরে যা ধরা পড়ে, তার একটি ছোট সংস্করণ PR স্তরে আনার চেষ্টা থাকা উচিত। আরেকটি ভুল হলো দ্রুততার জন্য flaky test-কে retry দিয়ে ঢাকা বা continue-on-error দেওয়া: pipeline দ্রুত সবুজ হয়, কিন্তু সবুজের অর্থ হারায়।

মূল উপলব্ধি: দ্রুত আর পূর্ণ বিরোধী নয়, যদি check-গুলো খরচ অনুযায়ী স্তরে সাজানো হয়: সস্তা check সবচেয়ে আগে আর সবচেয়ে কাছে, দামি check কম ঘন ঘন, আর সিদ্ধান্তের কর্তৃত্ব সবসময় CI-র।

অনুশীলন

হিসাব, সমালোচনা, ফল অনুমান আর কমান্ড, চারটিই আছে।

  1. Pipeline-এর সময় (numerical): ধরুন CI-তে প্রতিটি job-এর শুরুতে checkout, setup আর npm ci মিলে ২০ সেকেন্ড (cache hit). বাকি কাজ: quality ৮ s; integration-এ service container চালু ১০ s আর test ৫ s; build ৭ s (needs quality); প্রতিটি e2e shard-এ browser install ৩০ s আর test ১২ s (needs build ও integration, ২টি shard একসাথে); report ৩ s. (ক) পুরো pipeline-এর ঘড়ির সময় কত? (খ) Runner-এর মোট সময় কত? (গ) সবকিছু একটি job-এ পরপর চালালে (install একবার, service একবার, browser একবার, দুই shard-এর test পরপর) কত? (ঘ) Cache hit ৮০% হলে আর miss-এ install ১০০.৪ s, hit-এ ১৭.৩ s ধরলে, গড় install সময় কত?

    (ক) quality শেষ 20 + 8 = 28 s; integration শেষ 20 + 10 + 5 = 35 s; build শুরু 28-এ, শেষ 28 + 20 + 7 = 55 s; shard শুরু max(55, 35) = 55, শেষ 55 + 20 + 30 + 12 = 117 s; report শেষ 120 s. (খ) 28 + 35 + 27 + 62 + 62 + 3 = 217 s runner-সময়। (গ) 20 + 8 + 10 + 5 + 7 + 30 + 12 + 12 + 3 = 107 s: এই ছোট project-এ একটি job-ই parallel DAG-এর চেয়ে দ্রুত, কারণ প্রতিটি job-এর ২০ সেকেন্ডের স্থির খরচ parallel-এর লাভ খেয়ে ফেলে। (ঘ) 0.8 × 17.3 + 0.2 × 100.4 = 13.84 + 20.08 = 33.92 s; hit ৯৫% হলে 21.455 s. শিক্ষা: lockfile কম বদলালে cache-এর লাভ বেশি, আর job ভাঙার সিদ্ধান্ত মেপে নিতে হয়।

  2. Workflow সমালোচনা (নকশা): একটি দলের workflow-এ এগুলো আছে। অন্তত ছয়টি সমস্যা আর প্রতিটির সমাধান বলুন: on: pull_request_target; permissions: write-all; uses: actions/checkout@main আর with: ref: ${{ github.event.pull_request.head.sha }}; run: npm install; run: echo "Testing ${{ github.event.pull_request.title }}"; run: npx prisma migrate dev production-এর DATABASE_URL দিয়ে, যা repository secret; E2E step-এ continue-on-error: true; deploy job-এ নতুন করে next build, কোনো অনুমোদন নেই।
    • pull_request_target আর PR-এর head checkout: অচেনা কোড secret-সহ চলে। pull_request ব্যবহার করুন।
    • write-all: contents: read দিয়ে শুরু, দরকারি job-এ শুধু দরকারি অধিকার।
    • @main: branch-এ পিন সবচেয়ে বিপজ্জনক; পূর্ণ SHA আর মন্তব্যে version.
    • npm install: lockfile বদলাতে পারে; npm ci.
    • শিরোনাম সরাসরি shell-এ: script injection; env: TITLE: …, তারপর "$TITLE".
    • migrate dev production-এ: নিজে migration বানিয়ে চালাতে পারে, এমনকি DROP COLUMN; একটি আলাদা migrate job-এ migrate deploy, আর চাবি environment secret-এ।
    • continue-on-error: লাল E2E-কে সবুজ দেখায়; সরিয়ে দিন, flaky test সারান।
    • Deploy-এ নতুন build আর অনুমোদন নেই: test হওয়া artifact-ই deploy, environment: production-এ reviewer.
  3. ফল অনুমান: একটি workflow-এ চারটি job: A (কোনো needs নেই), B (needs: [A]), C (needs: [A], if: ${{ !cancelled() }}), D (needs: [B, C]). A-র তৃতীয় step ফেল করল, আর কেউ run বাতিল করেননি। (ক) A-র চতুর্থ step চলবে? (খ) B, C আর D-এর অবস্থা কী হবে? (গ) C-তে if লাইনটি না থাকলে C আর D-এর অবস্থা কী হতো?

    (ক) না: job-এর ভেতরে step fail-fast, পরের step চলে না (step-এ নিজস্ব if না থাকলে)। A হবে failure. (খ) B skipped, কারণ তার needs ব্যর্থ। C চলবে, কারণ !cancelled() শর্ত needs-এর ব্যর্থতা উপেক্ষা করে; ফল নির্ভর করবে তার নিজের step-এর ওপর। D skipped, কারণ তার needs-এর একটি (B) skip হয়েছে আর D-র কোনো status শর্ত নেই। (গ) C-ও skipped হতো, কারণ default শর্তে সব needs সফল হতে হয়; D আগের মতোই skipped. সাবধানতা: GitHub-এর docs অনুযায়ী if-শর্তে skip হওয়া job “Success” রিপোর্ট করে আর required check হলেও merge আটকায় না, তাই ব্যর্থতার মূল job, এখানে A, required তালিকায় রাখুন।

  4. কমান্ড লিখুন: বাজারঘরের ল্যাবের মতো এই কাজগুলোর কমান্ড লিখুন: (ক) দুটি workflow ফাইল actionlint দিয়ে যাচাই; (খ) production database-এ কোন migration বাকি আছে তা দেখা, কিছু না বদলে; (গ) সেগুলো চালানো; (ঘ) E2E-র দ্বিতীয় shard চালানো, blob report-সহ; (ঙ) দুই shard-এর blob একটি folder-এ রেখে একটি HTML report বানানো।

    (ক) actionlint .github/workflows/ci.yml .github/workflows/deploy.yml (git repository-র ভেতরে শুধু actionlint-ও চলে)। (খ) npx prisma migrate status; বাকি থাকলে exit code ১, gate-এ কাজে লাগে। (গ) npx prisma migrate deploy, migrate job-এ, environment secret-এর DATABASE_URL দিয়ে; কখনো migrate dev নয়। (ঘ) npx playwright test --shard=2/2 --reporter=blob (ল্যাবে --reporter=list,blob, আর PLAYWRIGHT_BLOB_OUTPUT_DIR দিয়ে আলাদা folder). (ঙ) npx playwright merge-reports --reporter html ./all-blob-reports.

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

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

ক্রমNext.js আর Prisma অ্যাপের একটি pipeline-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Lockfile থেকে npm ci দিয়ে dependency বসানো
  2. Lint, type check আর unit ও component test
  3. খালি test database-এ migrate deploy, তারপর integration test
  4. Production build তৈরি আর artifact হিসেবে রাখা
  5. সেই build-এর বিরুদ্ধে Playwright E2E, shard-এ ভাগ করে
  6. অনুমোদনের পরে production-এ migrate deploy, তারপর release

CI-তে dependency বসানোর জন্য npm install-এর বদলে npm ci ব্যবহারের মূল কারণ কোনটি?

  • npm ci সবসময় network ছাড়া চলে, তাই দ্রুত
  • এটি lockfile-এর হুবহু version বসায়, অমিল হলে থামে, আর lockfile বদলায় না
  • এটি আগের node_modules রেখে শুধু নতুন package যোগ করে
  • এটি install-এর সাথে নিজে থেকে সব vulnerability ঠিক করে

একটি Next.js ও Prisma অ্যাপের ১০টি instance rolling deploy-এ চালু হয়। Production database-এর migration চালানোর সবচেয়ে উপযুক্ত পদ্ধতি কোনটি?

  • প্রতিটি instance চালুর সময় prisma migrate dev
  • প্রতিটি instance চালুর সময় prisma migrate deploy
  • Release-এর আগে একটি আলাদা job-এ একবার prisma migrate deploy, সফল হলে তবেই release
  • Deploy-এর পরে হাতে prisma db push

একটি রিলিজে Product.name কলাম মুছে title রাখা হচ্ছে। Rolling deploy-এর সময় পুরোনো আর নতুন version কিছুক্ষণ একসাথে চলে। কোন ক্রম শূন্য downtime রাখে?

  • আগে title যোগ ও backfill, নতুন কোড দুটোতে লেখে; পরের রিলিজে শুধু title; তার পরের রিলিজে name মোছা
  • একটি migration-এ RENAME COLUMN, তারপর নতুন কোড deploy
  • আগে name মোছা, তারপর title যোগ আর নতুন কোড
  • সব migration একসাথে চালানো, কারণ migrate deploy সফল হলে অ্যাপও নিরাপদ

একটি open-source repository-তে fork থেকে আসা pull request-এর pull_request workflow run-এ preview deploy-এর step secret না পেয়ে ফেল করল। সবচেয়ে সঠিক ব্যাখ্যা আর পদক্ষেপ কোনটি?

  • Workflow-কে pull_request_target-এ বদলে PR-এর head checkout করা
  • Secret-টি workflow ফাইলে সরাসরি লিখে দেওয়া
  • permissions: write-all দেওয়া
  • এটি প্রত্যাশিত নিরাপত্তা আচরণ; fork-এর PR-এ preview বন্ধ রাখা বা maintainer কোড দেখে নিরাপদভাবে চালানো

একটি workflow-এ uses: actions/setup-node@v7 লেখা। একজন নিরাপত্তা পর্যালোচক একে পূর্ণ commit SHA-তে পিন করতে বললেন। এর মূল কারণ কোনটি?

  • SHA দিলে action দ্রুত নামে
  • Tag অন্য commit-এ সরানো যায়, কিন্তু commit SHA একটি নির্দিষ্ট, অপরিবর্তনীয় কোড নির্দেশ করে
  • SHA দিলে নতুন নিরাপত্তা-সংশোধন নিজে থেকে চলে আসে
  • GitHub tag দিয়ে লেখা action আর চালায় না

Production-এ নতুন release-এর পরে checkout-এ error বাড়ল। Release-টিতে শুধু একটি expand migration ছিল (নতুন nullable কলাম যোগ)। দ্রুততম নিরাপদ rollback কোনটি?

  • আগে down migration দিয়ে কলাম মুছে, তারপর পুরোনো কোড
  • পুরোনো commit থেকে নতুন করে build করে deploy
  • আগের commit-এর সংরক্ষিত build আবার deploy, database যেমন আছে তেমন রেখে
  • Release-এর আগের database backup ফেরানো

একটি PR-এ lint, tsc --noEmit আর সব unit test সবুজ, কিন্তু next build ফেল করল: route segment config dynamic cacheComponents-এর সাথে চলে না। এ থেকে pipeline নকশার সবচেয়ে সঠিক শিক্ষা কোনটি?

  • Build আলাদা, আবশ্যিক check হওয়া উচিত, কারণ framework-এর কিছু নিয়ম শুধু build-ই ধরে
  • tsc-এর strict mode চালু করলেই এটি ধরা পড়ত, তাই build বাদ দেওয়া যায়
  • Build step-এ continue-on-error দেওয়া উচিত
  • শুধু lint-এর নিয়ম বাড়ালেই যথেষ্ট

শব্দকোষ

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

মনে রাখুন

  1. একই ধাপ, প্রতিটি পরিবর্তনে: npm ci, lint, type check, unit ও component, PostgreSQL-সহ integration, build, E2E; needs দিয়ে DAG, fail-fast, আর লাল হলে merge বন্ধ।
  2. দ্রুত রাখুন মেপে: ল্যাবের run-এ install প্রায় অর্ধেক সময়; cache, পুরোনো run বাতিল, সস্তা check আগে, আর shard শুধু যেখানে test-এর সময় স্থির খরচের চেয়ে বড়।
  3. Migration একবার, আগে: আলাদা job-এ migrate deploy, release-এর আগে; migrate dev কখনো নয়; expand আগে, contract সেই release-এ যার আগের কোড পুরোনো কলাম ছোঁয় না।
  4. পাহারা: fork-এর PR-এ secret নেই, pull_request_target-এ অচেনা কোড নয়, PR-এর লেখা env দিয়ে, action পূর্ণ SHA-তে, production-এর চাবি environment-এ, সম্ভব হলে OIDC.
  5. Deploy একটি পরীক্ষিত build: অনুমোদনের পরে migrate, তারপর release; সমস্যায় আগের SHA-র build আবার, database নয়; তারপর test-সহ roll forward.

পরের পর্বে: পর্ব ৮২ · কোথায় চালাবেন — VPS, shared hosting, PaaS আর serverless. এই pipeline-এর শেষের deploy.sh আসলে কোথায় যায়: নিজের server, shared hosting-এ static export, PaaS আর serverless-এর তুলনা।

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

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