یک Pipeline سبز فقط وقتی ارزش دارد که بدانیم دقیقاً چه چیزی را، روی کدام Commit و Toolchain، با چه Testهایی سنجیده و کدام Artifact را آماده تحویل کرده است. سه مرحله‌ی checkout → npm install → npm test ممکن است Demo را اجرا کند، اما هنوز lockfile، Test Report، timeout، runner trust، Artifact identity، cancellation و Deployment gate ندارد. در این آموزش، یک Contract واحد را هم با Jenkinsfile و هم با GitLab CI پیاده می‌کنیم تا تفاوت ابزار، معنی Quality Gate را عوض نکند.

نمونه برای یک پروژه Node.js نوشته شده، ولی الگو به Java، Python و .NET قابل انتقال است: نصب deterministic، فرمان‌های CLI غیرتعاملی، JUnit، Artifact immutable، Evidence و exit code. برای معماری Fast/Slow/Event-driven در مقیاس بزرگ‌تر، مقاله تست مداوم در CI/CD مکمل این آموزش قدم‌به‌قدم است.

پاسخ کوتاه: Pipeline پایه باید روی Merge Request و شاخه اصلی اجرا شود، SHA و runtime را ثبت کند، dependency را از lockfile نصب کند، Lint/Unit/Build/Integration را با exit code واقعی اجرا کند، JUnit و Artifact را حتی در Failure قابل مشاهده سازد، Build را فقط یک‌بار بسازد و همان checksum را به Stage بعد بدهد. Jenkins Controller یا GitLab Runner نباید Secret پرارزش را به کد Pull Request غیرقابل اعتماد بدهد.

CI، Continuous Delivery و Continuous Deployment چه تفاوتی دارند؟

Continuous Integration یا CI

CI یک عمل تیمی است: تغییرهای کوچک و مکرر در خط اصلی ادغام می‌شوند و هر تغییر با Build/Test خودکار و بازخورد قابل اقدام سنجیده می‌شود. ابزار CI فقط اجرای Job نیست؛ قرارداد ادغام، branch policy، ownership شکست و زمان تعمیر نیز بخشی از سیستم‌اند.

Continuous Delivery

در Continuous Delivery، هر تغییرِ عبورکرده از Gateها در وضعیت قابل انتشار نگه داشته می‌شود. Artifact، configuration، migration و روش rollback آماده‌اند؛ تصمیم Production می‌تواند دستی و وابسته به Risk/Business باشد.

Continuous Deployment

در Continuous Deployment، تغییر واجد شرایط بدون approval انسانی تا Production پیش می‌رود. این انتخاب به test evidence، observability، progressive delivery، rollback و تحمل ریسک نیاز دارد. حذف دکمه approval به‌تنهایی Continuous Deployment بالغ نمی‌سازد.

این آموزش تا کجا می‌رود؟

Pipeline این مقاله CI کامل و یک Gate دستی Staging برای نشان‌دادن مرز Delivery دارد. Production deployment عمداً نسخه عمومی ندارد؛ credential، topology، change policy و rollback هر سازمان متفاوت است. هدف این است که Artifact نامزد انتشار قابل شناسایی و تست‌شده باشد.

Contract مشترک Jenkins و GitLab CI

پیش از نوشتن YAML یا Groovy، خروجی هر Stage را تعریف کنید:

Stage ورودی Gate موفقیت Evidence/خروجی
Checkout SCM event SHA دقیق و workspace تمیز Commit/branch/source
Runtime Runner + Image Node/npm و platform مورد انتظار Version و Image digest
Dependencies package-lock.json npm ci موفق Install log؛ cache فقط شتاب‌دهنده
Lint Source صفر violation در Rule profile مصوب Exit code/report
Unit Source + dependency Test command غیرصفر نباشد JUnit و در صورت نیاز coverage
Build همان SHA Build reproducible و موفق dist/ و Artifact checksum
Integration Build output + محیط کنترل‌شده Contract/Journey منتخب Pass JUnit، log و environment identity
Staging همان Artifact شاخه/approval/credential مجاز Deployment record و environment URL

Result state فقط Pass/Fail نیست

  • Passed: فرمان و Oracleهای تعریف‌شده موفق‌اند؛
  • Failed: Evidence به نقض رفتار محصول یا Quality Gate اشاره دارد؛
  • Blocked: پیش‌شرط مانند dependency یا environment مهیا نیست؛
  • Infrastructure failure: Runner، network، registry یا CI service شکست خورده؛
  • Cancelled/Superseded: Run قدیمی با Commit جدید بی‌ارزش شده؛
  • Unstable/Quarantined: سیگنال تست نامطمئن است و owner/expiry دارد.

مدیریت attempt و status را با چرخه اجرای تست هماهنگ کنید؛ Pipeline UI نباید Failure تست و Failure زیرساخت را در یک سطل مبهم بریزد.

Jenkins یا GitLab CI؛ انتخاب براساس Operating Model

معیار Jenkins GitLab CI/CD
Control plane Controller مستقل با Plugin/Agent که تیم مدیریت می‌کند یکپارچه با GitLab؛ SaaS یا Self-Managed
Pipeline as Code Jenkinsfile، Declarative یا Scripted .gitlab-ci.yml
Runner model Static/Cloud Agent با Label Instance/Group/Project Runner با Tag
Extension Plugin ecosystem بزرگ؛ upgrade/test سازگاری لازم Keywordها و integration پلتفرم؛ upgrade نسخه GitLab/Runner لازم
SCM/MR UX به Branch Source/Plugin و پیکربندی بستگی دارد Pipeline/MR/Report یکپارچه
Isolation ownership تیم Jenkins مسئول Controller/Agent/Plugin است در Self-Managed تیم؛ در SaaS مسئولیت مشترک با provider
مناسب‌تر وقتی… زیرساخت متنوع، legacy و integration سفارشی دارید کد و workflow از قبل در GitLab متمرکز است

«Jenkins انعطاف‌پذیرتر» یا «GitLab ساده‌تر» بدون context تصمیم کافی نیست. با یک Pipeline یکسان، زمان Setup، patch ownership، runner isolation، secret model، artifact retention، queue time، export و هزینه را PoC کنید. چارچوب Pilot و Scale در استراتژی اتوماسیون تست قابل استفاده است.

پیش‌نیازهای Repository

فرمان‌ها باید محلی و در CI یکسان باشند

CI نباید منطق Test را داخل ده‌ها خط YAML/Groovy پنهان کند. Repository یک interface CLI کوچک داشته باشد:

{
  "scripts": {
    "lint": "eslint .",
    "test:unit": "jest --selectProjects unit",
    "build": "tsc -p tsconfig.build.json",
    "test:integration": "jest --selectProjects integration"
  }
}

نام Projectهای Jest نمونه است و باید با config پروژه شما تطبیق داده شود. Reporter jest-junit نیز باید در devDependencies و lockfile باشد؛ نصب ad-hoc آن داخل Pipeline، نسخه اجرا را نامعلوم می‌کند.

Lockfile باید authoritative باشد

  • package-lock.json همراه تغییر dependency Commit شود؛
  • npm ci در صورت اختلاف package.json و lockfile Fail می‌کند و lockfile را بازنویسی نمی‌کند؛
  • node_modules میان Runnerها Artifact نشود؛ OS/architecture/native module و symlink می‌تواند فرق کند؛
  • cache فقط package download را شتاب دهد و correctness به آن وابسته نباشد؛
  • dependency update یک Pull Request با Test و review باشد.

Runtime پشتیبانی‌شده و pin‌شده

در زمان بازبینی این مقاله، صفحه رسمی انتشارهای Node.js نسخه ۲۴ را LTS و Node ۱۶ نمونه قدیمی مقاله را EOL نشان می‌دهد. قبل از استفاده، وضعیت فعلی را دوباره بررسی کنید. در Pipeline، tag خوانا را همراه digest مورد تأیید registry سازمان pin کنید؛ placeholder زیر باید با manifest digest واقعی platform Runner جایگزین شود.

ساختار فایل پیشنهادی

.
├── Jenkinsfile
├── .gitlab-ci.yml
├── package.json
├── package-lock.json
├── src/
├── tests/
│   ├── unit/
│   └── integration/
├── ci/
│   └── deploy-staging.sh
└── reports/        # generated; ignored by Git

reports/، artifacts/، dist/ و node_modules/ معمولاً generated هستند و در Git Commit نمی‌شوند.

راه‌اندازی Jenkins برای Pipeline تست خودکار

Controller و Agent

Jenkins Controller باید Job scheduling/configuration را مدیریت کند، نه اجرای Build غیرقابل اعتماد را. مستندات Controller Isolation توصیه می‌کند executor مربوط به built-in node صفر باشد و Build روی Agent جدا اجرا شود. برای نمونه ما Agent با Label linux-container، Docker runtime و workspace کوتاه‌عمر لازم است.

Pluginهای حداقلی

  • Pipeline و Declarative Pipeline؛
  • SCM/Branch Source متناسب با Git provider؛
  • Docker Pipeline برای Agent کانتینری؛
  • JUnit برای Test Report؛
  • Credentials Binding فقط در Stage نیازمند Secret.

Plugin «پیشنهادی» را کورکورانه روی Production Controller نصب نکنید. inventory، version pin/upgrade window، backup و تست سازگاری داشته باشید.

Multibranch Pipeline و Webhook

  1. یک Multibranch Pipeline یا Organization Folder بسازید؛
  2. SCM credential فقط برای discovery/checkout با کمترین scope تعریف کنید؛
  3. Script path را Jenkinsfile بگذارید؛
  4. Webhook provider را فعال و polling دوره‌ای را fallback محدود نگه دارید؛
  5. Branch/PR discovery و trust policy forkها را صریح تنظیم کنید.

URL repository را در Jenkinsfile hard-code نکنید؛ checkout scm همان revision انتخاب‌شده توسط Multibranch job و credential تنظیم‌شده را می‌گیرد.

Jenkinsfile اجرایی برای Node.js

pipeline {
  agent {
    docker {
      image 'node:24.18.0-bookworm@sha256:<approved-node-digest>'
      label 'linux-container'
      args '--user 1000:1000'
    }
  }

  options {
    skipDefaultCheckout(true)
    timestamps()
    timeout(time: 20, unit: 'MINUTES')
    disableConcurrentBuilds(abortPrevious: true)
    buildDiscarder(
      logRotator(
        numToKeepStr: '30',
        artifactNumToKeepStr: '10'
      )
    )
  }

  environment {
    CI = 'true'
    NPM_CONFIG_CACHE = '.npm'
  }

  stages {
    stage('Checkout') {
      steps {
        checkout scm
        sh 'git rev-parse HEAD'
        sh 'test -f package-lock.json'
      }
    }

    stage('Runtime') {
      steps {
        sh 'node --version'
        sh 'npm --version'
        sh 'uname -a'
      }
    }

    stage('Dependencies') {
      steps {
        sh 'npm ci --cache .npm --prefer-offline'
      }
    }

    stage('Lint') {
      steps {
        sh 'npm run lint'
      }
    }

    stage('Unit') {
      environment {
        JEST_JUNIT_OUTPUT_DIR = 'reports/unit'
        JEST_JUNIT_OUTPUT_NAME = 'junit.xml'
      }
      steps {
        sh 'mkdir -p reports/unit'
        sh '''
          npm run test:unit -- \
            --ci \
            --runInBand \
            --reporters=default \
            --reporters=jest-junit
        '''
      }
      post {
        always {
          junit(
            allowEmptyResults: false,
            testResults: 'reports/unit/junit.xml'
          )
        }
      }
    }

    stage('Build') {
      steps {
        sh 'npm run build'
        sh '''
          mkdir -p artifacts
          tar -czf artifacts/app.tgz \
            dist package.json package-lock.json
          sha256sum artifacts/app.tgz \
            > artifacts/app.tgz.sha256
        '''
      }
    }

    stage('Integration') {
      environment {
        JEST_JUNIT_OUTPUT_DIR = 'reports/integration'
        JEST_JUNIT_OUTPUT_NAME = 'junit.xml'
      }
      steps {
        sh 'mkdir -p reports/integration'
        sh '''
          npm run test:integration -- \
            --ci \
            --runInBand \
            --reporters=default \
            --reporters=jest-junit
        '''
      }
      post {
        always {
          junit(
            allowEmptyResults: false,
            testResults: 'reports/integration/junit.xml'
          )
        }
      }
    }

  }

  post {
    always {
      archiveArtifacts(
        allowEmptyArchive: true,
        artifacts: 'artifacts/**/*,reports/**/*',
        fingerprint: true
      )
    }
    cleanup {
      deleteDir()
    }
  }
}

این Jenkinsfile چه چیزهایی را اصلاح می‌کند؟

  • محیط اجرای شناور agent any با Label و Image مشخص جایگزین شده است؛
  • Checkout همان SCM revision است و Repository URL دوباره تعریف نمی‌شود؛
  • npm ci lockfile را enforce می‌کند؛
  • Unit و Integration JUnit مستقل دارند؛
  • Build فقط یک بار تولید و checksum آن ثبت می‌شود؛
  • timeout و superseded run از مصرف بی‌پایان Agent جلوگیری می‌کند؛
  • retention Artifact/Build صریح است؛
  • Artifact نامزد Release archive و fingerprint می‌شود؛
  • workspace در پایان پاک می‌شود.

چرا Deploy در همین Jenkinsfile نیست؟ این نمونه یک Docker Agent سراسری دارد. قرار دادن input در انتهای آن، Agent را تا زمان تصمیم انسانی اشغال و timeout بیست‌دقیقه‌ای CI را با زمان approval مخلوط می‌کند. برای Staging یک Promotion Pipeline جدا بسازید که Artifact همین Build را با build ID و checksum دریافت کند، input را بدون نگه‌داشتن Build Agent منتظر بماند، سپس روی Agent محدود staging-deployer همان Artifact را Deploy کند؛ هرگز آن را دوباره Build نکند.

JUnit در Jenkins

مرحله post { always { ... } } حتی پس از Failure تست تلاش می‌کند Report را publish کند. مرجع رسمی JUnit Plugin می‌گوید Jenkins از XML برای trend و نمایش Failure استفاده می‌کند. allowEmptyResults: false عمدی است: نبود فایل Report وقتی Test باید اجرا شده باشد، نقض Contract است؛ آن را Pass نکنید.

Pipeline syntax را با نسخه Jenkins خود Validate کنید

Declarative Directive و optionها با Plugin/Core نسخه‌دارند. مرجع Pipeline Syntax و Snippet Generator همان Controller معیار نهایی‌اند. Jenkinsfile را در Pull Request review و در Controller غیرProduction یا validation job آزمایش کنید.

راه‌اندازی GitLab Runner

Runner باید با Job match و با Trust هماهنگ باشد

  1. Project/Group Runner کوتاه‌عمر یا isolate انتخاب کنید؛
  2. Tag مانند linux-container را روی Runner و Job یکسان بگذارید؛
  3. برای Stageهای دارای Secret/Deploy از Protected Runner استفاده کنید؛
  4. Runner را از production network و credential غیرلازم جدا کنید؛
  5. concurrency، CPU/memory/disk، cache و cleanup را محدود کنید.

مستندات پیکربندی GitLab Runner نقش Tag و Protected Runner را توضیح می‌دهد. Protected بودن Runner به‌تنهایی کافی نیست؛ Branch/Tag protection، variable scope و ruleهای Job نیز باید همسو باشند.

Repository variable و Secret

نمونه CI تست به Secret نیاز ندارد. برای Deploy، token/SSH key در GitLab CI/CD Variable یا Secret provider تعریف، masked/protected و environment-scoped شود. هیچ value واقعی داخل .gitlab-ci.yml یا command echo نشود.

.gitlab-ci.yml اجرایی و معادل Jenkinsfile

workflow:
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
    - if: '$CI_COMMIT_TAG'
    - when: never

default:
  image: "node:24.18.0-bookworm@sha256:<approved-node-digest>"
  tags:
    - linux-container
  interruptible: true
  retry:
    max: 1
    when:
      - runner_system_failure
      - stuck_or_timeout_failure
  before_script:
    - node --version
    - npm --version
    - test -f package-lock.json
    - npm ci --cache .npm --prefer-offline

variables:
  CI: "true"
  NPM_CONFIG_CACHE: ".npm"

cache:
  key:
    files:
      - package-lock.json
  paths:
    - .npm/
  policy: pull-push

stages:
  - verify
  - test
  - build
  - integration
  - deploy

lint:
  stage: verify
  timeout: 10m
  script:
    - npm run lint

unit:
  stage: test
  timeout: 15m
  variables:
    JEST_JUNIT_OUTPUT_DIR: "reports/unit"
    JEST_JUNIT_OUTPUT_NAME: "junit.xml"
  script:
    - mkdir -p reports/unit
    - >-
      npm run test:unit --
      --ci
      --runInBand
      --reporters=default
      --reporters=jest-junit
  artifacts:
    when: always
    expire_in: 7 days
    paths:
      - reports/unit/
    reports:
      junit: reports/unit/junit.xml

build:
  stage: build
  timeout: 15m
  script:
    - npm run build
    - mkdir -p artifacts
    - tar -czf artifacts/app.tgz dist package.json package-lock.json
    - sha256sum artifacts/app.tgz > artifacts/app.tgz.sha256
  artifacts:
    name: "app-$CI_COMMIT_SHA"
    expire_in: 7 days
    paths:
      - dist/
      - artifacts/app.tgz
      - artifacts/app.tgz.sha256

integration:
  stage: integration
  timeout: 20m
  needs:
    - job: build
      artifacts: true
  variables:
    JEST_JUNIT_OUTPUT_DIR: "reports/integration"
    JEST_JUNIT_OUTPUT_NAME: "junit.xml"
  script:
    - mkdir -p reports/integration
    - >-
      npm run test:integration --
      --ci
      --runInBand
      --reporters=default
      --reporters=jest-junit
  artifacts:
    when: always
    expire_in: 7 days
    paths:
      - reports/integration/
    reports:
      junit: reports/integration/junit.xml

deploy_staging:
  stage: deploy
  timeout: 20m
  interruptible: false
  resource_group: staging
  needs:
    - job: build
      artifacts: true
    - job: integration
      artifacts: false
  before_script: []
  script:
    - ./ci/deploy-staging.sh artifacts/app.tgz
  environment:
    name: staging
  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
      when: manual
    - when: never
  allow_failure: false

نکات مهم YAML

  • workflow: rules Pipelineهای MR، شاخه اصلی و Tag را می‌سازد و بقیه را حذف می‌کند؛ با workflow واقعی تیم تطبیق دهید.
  • interruptible: true Validation قدیمی را قابل لغو می‌کند؛ Deploy عمداً false است.
  • Retry فقط failureهای Runner/timeout انتخابی را پوشش می‌دهد، نه script_failure یا Test Failure.
  • Cache با hash lockfile package tarballها را شتاب می‌دهد؛ correctness به cache وابسته نیست.
  • Artifact Build از dist/ و tar/checksum تشکیل می‌شود؛ node_modules Artifact نیست.
  • needs Integration را به Build Artifact دقیق متصل و Deploy را تا عبور Integration متوقف می‌کند.
  • resource_group: staging دو Deployment همزمان به یک Environment را serialize می‌کند.

قواعد فعلی GitLab را از مرجع بخوانید

Keywordهای workflow، rules، needs، interruptible، retry و resource_group در مرجع رسمی GitLab CI/CD YAML تعریف شده‌اند. فایل را با CI Lint همان GitLab instance اعتبارسنجی کنید؛ feature availability و syntax ممکن است با نسخه Self-Managed فرق داشته باشد.

نگاشت دقیق Jenkins و GitLab CI

Contract Jenkins GitLab
Revision checkout scm checkout خودکار Runner برای SHA Job
Runtime Docker Agent image + label image + runner tag
Timeout options timeout timeout
Superseded run disableConcurrentBuilds(abortPrevious) interruptible + auto-cancel policy
Test report junit artifacts:reports:junit
Build artifact archiveArtifacts + fingerprint artifacts:paths + checksum
Dependency edge Stage ordering needs
Manual approval input when: manual
Deploy serialization Lock/Job policy در setup سازمان resource_group
Retention buildDiscarder expire_in و instance policy

نام Directiveها متفاوت است، اما Risk contract نباید متفاوت شود. مهاجرت ابزار وقتی ساده است که Stageها به commandهای Repository و Artifact استاندارد متکی باشند.

JUnit، exit code و Artifact؛ سه مفهوم متفاوت

JUnit Report نتیجه Job را تعیین نمی‌کند

GitLab در مستندات Unit Test Reports توضیح می‌دهد که JUnit برای نمایش Result است و به‌تنهایی Job را Fail نمی‌کند؛ command تست باید exit code غیرصفر بدهد. در Jenkins نیز Report publication نباید با || true Test Failure را سبز کند مگر عمداً Result را بعداً مدیریت کنید.

Report همیشه، Artifact به‌اندازه نیاز

  • JUnit حتی در Failure upload شود؛
  • Screenshot/log/trace شکست با redaction و retention محدود نگه‌داری شود؛
  • Build Artifact فقط پس از Build موفق ایجاد شود؛
  • Secret، .env، core dump یا workspace کامل archive نشود؛
  • نام Artifact شامل SHA یا build identity باشد؛
  • checksum و در مسیر بالغ‌تر provenance/SBOM ثبت شود.

Cache Artifact نیست

مرجع GitLab CI Cache Cache را برای dependencyهای قابل دانلود و Artifact را برای خروجی Job و انتقال میان Stageها جدا می‌کند. Cache ممکن است missing، stale یا evicted شود؛ Pipeline درست باید بدون Cache هم نتیجه یکسان، فقط کندتر، داشته باشد.

ساخت یک‌باره و Promotion

Build once

اگر Staging یا Production دوباره npm run build بزند، Artifact منتشرشده دقیقاً همان Artifact تست‌شده نیست. Pipeline نمونه dist را یک‌بار می‌سازد، Integration را پس از Build اجرا می‌کند و tar/checksum را به Deploy می‌دهد.

چه چیزی هنوز کم است؟

  • برای Container deployment، Image digest جای tar را می‌گیرد؛
  • برای package registry، version immutable و checksum/provenance لازم است؛
  • Migration باید forward/backward compatibility و owner داشته باشد؛
  • Staging health/smoke و Deployment record پس از deploy لازم است؛
  • rollback باید با Artifact قبلی تمرین شود.

محیط Integration می‌تواند با Docker Compose کوتاه‌عمر ساخته شود؛ الگوی health/migration/evidence/teardown در داکر برای محیط تست آمده است.

طراحی Test Lane در Pipeline

Pre-merge سریع

  • format/lint/type/static؛
  • Unit و Component؛
  • Integrationهای باریک و deterministic؛
  • Contractهای Consumer/Provider ضروری؛
  • Build و package candidate.

Post-merge

  • Integration matrix گسترده‌تر؛
  • migration و backward compatibility؛
  • Security/SCA/Image/IaC scan؛
  • System Journeyهای منتخب؛
  • environment smoke.

Scheduled/Release

  • Performance/Soak/Resilience با محیط و stop rule؛
  • device/browser/locale matrix؛
  • dependency واقعی یا Sandbox پرهزینه؛
  • Recovery و rollback rehearsal.

همه Testها را در Merge Request نریزید. Quality Gate باید Risk class، duration budget و ownership داشته باشد. Static/SAST/SCA/Secret/IaC boundaries را در تحلیل استاتیک برای QA و Contract gate را در تست قراردادی Pact ببینید.

Security و Trust؛ کد Pull Request فرمان اجرا می‌کند

Threat model ساده

هر نویسنده‌ای که بتواند Test، build script، dependency یا Pipeline file را تغییر دهد، می‌تواند روی Runner command اجرا کند. Mask کردن Secret در Log مانع exfiltration عمدی نیست. پس Untrusted MR باید بدون production credential و روی Runner isolate اجرا شود.

Jenkins credential boundary

  • Credential در پایین‌ترین Folder/Item scope تعریف شود؛
  • Controller و Agent filesystem جدا باشند؛
  • PR از fork به credential Deploy دسترسی نداشته باشد؛
  • withCredentials فقط اطراف command لازم و دور از code غیرقابل اعتماد استفاده شود؛
  • Groovy interpolation و shell tracing Secret را افشا نکند؛
  • Controller/Plugin patch و backup/restore آزمایش شود.

راهنمای رسمی Using a Jenkinsfile هم Pipeline-as-Code، post handling و credential binding را پوشش می‌دهد و هشدار می‌دهد code دارای دسترسی می‌تواند Secret را افشا کند.

GitLab boundary

  • Protected Variable فقط به protected ref/job مجاز برسد؛
  • Deploy روی Protected Runner و environment-scoped credential اجرا شود؛
  • MR fork بدون Secret validate شود؛
  • Runner shared برای network داخلی/Production route استفاده نشود؛
  • Image، include و template خارجی با ref immutable pin شوند؛
  • job token permission و retention Artifact حداقلی باشد.

Webhook، Rule و Duplicate Pipeline

Webhook به‌جای Poll مداوم

Webhook برای Push/MR تغییر را فوری اعلام می‌کند و polling بی‌دلیل را کم می‌کند. با این حال delivery failure، signature validation و retry provider را monitor کنید. Polling محدود می‌تواند fallback باشد، نه تنها trigger.

MR Pipeline و Branch Pipeline را دوبار نسازید

Ruleهای GitLab نمونه فقط MR، default branch و Tag را می‌پذیرد. در Jenkins، Multibranch discovery باید PR و Branch strategy را طوری تنظیم کند که یک Commit دو Job هم‌معنا نسازد. Duplicate Pipeline هزینه، queue و statusهای متناقض می‌سازد.

Tag قابل اعتماد نیست مگر protected

وجود Git tag به‌تنهایی مجوز Deploy نیست. Protected tag/branch، signer/review policy و Artifact checksum لازم است. Pipeline source و actor را در Deployment record ثبت کنید.

Flaky Test، Retry و Quarantine

Retry فقط Infra failure محدود

GitLab نمونه فقط runner_system_failure و timeout زیرساختی منتخب را یک بار Retry می‌کند. Test Failure با script_failure نباید خودکار سبز شود. در Jenkins نیز Retry باید دور operation واقعاً idempotent و با حفظ attempt نخست باشد.

Passed-on-retry Pass عادی نیست

  • Failure اول و Artifact آن نگه‌داری شود؛
  • Test به Flaky candidate با owner تبدیل شود؛
  • علت Product/Test/Data/Environment/Dependency تفکیک شود؛
  • Quarantine scope، expiry و Risk impact داشته باشد؛
  • Quarantined Test از denominator پوشش ادعایی پنهان نشود.

Fail-fast با Evidence

Fail-fast feedback را کوتاه می‌کند، اما Report Stageهای اجراشده باید publish شود. برای Parallel Test sharding، همه shardها JUnit یکتا و merge بدون duplicate name داشته باشند؛ یک shard گم‌شده نباید به‌عنوان صفر Failure تفسیر شود.

Environment و Test Data در CI

Manifest حداقلی Run

run_id: pipeline-1842
pipeline_source: merge_request
commit_sha: a1b2c3d
runner_id: linux-container-07
runner_platform: linux/amd64
runtime_image: node@sha256:<digest>
node_version: "24.x"
lockfile_sha256: "<sha256>"
artifact_sha256: "<sha256>"
environment: integration
data_seed: checkout-v4
dependencies:
  payment: fake-v3
  sms: stub-v2

Secret value در Manifest قرار نمی‌گیرد. Environment health، booking/TTL و dependency fidelity را با مدیریت محیط تست هماهنگ کنید.

Data namespace

هر Pipeline/Worker باید user/order/topic/schema prefix یکتا داشته باشد. Cleanup idempotent و TTL fallback لازم است. حساب مشترک و داده Production واقعی هم Parallel را خراب می‌کند و هم privacy risk دارد.

نکات ویژه تیم‌های ایرانی

Registry و Package دسترس‌پذیر

اختلال npm، Docker registry، Jenkins update center یا GitLab dependency نباید Failure محصول گزارش شود. package cache/registry سازمانی مجاز، lockfile، Image digest و Artifact retention داشته باشید؛ hash و provenance را verify کنید. fallback باید قانونی، امنیتی و مستند باشد.

Runner location و Data residency

  • Runner SaaS را به Production network یا داده مشتری وصل نکنید بدون risk review؛
  • Self-hosted Runner patch، isolation، disk cleanup و access logging می‌خواهد؛
  • Artifact شامل log فارسی، شماره موبایل، آدرس یا token را redact و زمان‌دار کنید؛
  • timezone را Asia/Tehran و clock source را ثبت کنید؛
  • payment/SMS/Push را با Fake/Sandbox نسخه‌دار اجرا کنید.

هزینه را فقط دقیقه Runner نبینید

هزینه شامل queue، نگهداری Controller/Runner، Plugin/upgrade، cache/storage/egress، debugging، secret management و downtime است. PoC Jenkins/GitLab را با workload واقعی و TCO مقایسه کنید.

عیب‌یابی Pipeline

نشانه علت محتمل بررسی بعدی
Local سبز، CI قرمز Node/OS/arch/env/data متفاوت Manifest، Image digest، lockfile و command دقیق را diff کنید.
npm ci Fail lockfile با package.json همگام نیست Dependency change را محلی resolve و هر دو فایل را Commit کنید.
JUnit نمایش داده نمی‌شود Reporter نصب/مسیر/نام XML غلط وجود فایل، syntax XML و Artifact path را قبل از upload بررسی کنید.
Job سبز ولی Test Fail نشان می‌دهد command exit code صفر یا || true Test runner و wrapper script را اصلاح کنید.
Job قرمز و Report نیست Test پیش از ساخت Report crash کرده stdout/stderr و setup را حفظ؛ نبود Report را Contract failure بدانید.
GitLab Job در Stuck است Runner Tag/Protected/online mismatch همه Tagهای Job و Runner و ref protection را تطبیق دهید.
Jenkins روی Controller اجرا می‌کند built-in executor یا Label اشتباه executor صفر، Agent label و scheduling log را بررسی کنید.
دو Pipeline برای یک SHA Push و MR rule/discovery همپوشان workflow/discovery را بازطراحی و source را ثبت کنید.
Artifact Deploy با Test یکی نیست Rebuild در Deploy یا Artifact overwrite checksum/digest را میان Build/Test/Deploy مقایسه کنید.
فقط در Parallel Fail Data/port/account/cache/workspace مشترک namespace Worker و resource ownership را audit کنید.
Secret در Log echo، debug، dump env یا code مخرب rotate فوری، revoke، redact و trust boundary را اصلاح کنید.
Pipeline به‌تدریج کند شده queue، cache miss، test growth یا flaky retry queue و stage p50/p95 و critical path را جدا تحلیل کنید.

اولین خطای معنادار را پیدا کنید

آخرین پیام معمولاً «job failed» است. به نخستین Stage و command غیرصفر، JUnit failure، Runner system message و تغییر Manifest نگاه کنید. Console log را با timestamp و Stage boundary نگه دارید؛ log بی‌انتها evidence بهتر نیست.

Anti-patternهای CI/CD برای تست

  • agent any یا Runner ناشناخته: Toolchain و trust تصادفی می‌شود.
  • Node EOL: runtime بدون patch امنیتی و سازگاری package شکننده است.
  • npm install در CI: lockfile contract می‌تواند جابه‌جا شود.
  • Artifact کردن node_modules: cache/dependency با build output قاطی می‌شود.
  • فقط Console log: Test history و MR summary وجود ندارد.
  • || true روی Test: Failure به Green تبدیل می‌شود.
  • Retry همه Failureها: Flake و defect پنهان می‌شود.
  • Secret در همه Jobها: code غیرقابل اعتماد سطح دسترسی غیرضروری می‌گیرد.
  • Rebuild در Deploy: Artifact تست‌شده منتشر نمی‌شود.
  • همه Testها در MR: feedback کند و queue طولانی می‌شود.
  • Polling تهاجمی: latency و بار بی‌دلیل به SCM/Controller می‌دهد.
  • Latest image: تغییر محیط بدون Commit رخ می‌دهد.
  • Artifact بدون retention/redaction: storage و privacy risk رشد می‌کند.
  • Quality Gate با درصدهای جهانی: context ریسک و uncertainty حذف می‌شود.

متریک‌هایی که Pipeline را قابل مدیریت می‌کنند

Flow

  • Queue time و duration p50/p95 هر Stage؛
  • Commit-to-signal و Commit-to-deploy؛
  • Critical path و زمان هدررفته Superseded run؛
  • Cache hit در کنار correctness مستقل از Cache؛
  • Runner utilization و saturation.

Reliability

  • Pipeline success به تفکیک Product/Test/Data/Environment/Dependency/Infra؛
  • Flaky rate و Passed-on-retry؛
  • Missing/invalid JUnit rate؛
  • Artifact checksum/provenance completeness؛
  • زمان تعمیر pipeline/test شکسته؛
  • Quarantine age و owner.

Outcome

  • Change failure و rollback مرتبط با release cohort؛
  • escaped defect براساس Risk/Journey؛
  • Lead time بدون قربانی‌کردن guardrail؛
  • درصد Deployهایی که همان Artifact تست‌شده را promote کرده‌اند.

تعداد Job، تعداد Test و Green rate خام به‌تنهایی قابل بازی‌اند. طراحی سبد متریک و denominator را با متریک‌های تست نرم‌افزار انجام دهید.

برنامه ۱۴روزه راه‌اندازی

روز ۱ تا ۳: Baseline و CLI

  • زمان دستی Build/Test و failureهای فعلی را ثبت کنید؛
  • scriptهای lint/unit/build/integration را محلی deterministic کنید؛
  • lockfile و JUnit reporter را اضافه کنید؛
  • یک runtime LTS و Image digest تصویب کنید.

روز ۴ تا ۶: Pipeline CI

  • Jenkinsfile یا GitLab YAML را با Checkout/Install/Lint/Unit/Build بسازید؛
  • JUnit و Artifact/checksum را publish کنید؛
  • timeout، retention و cancellation را تنظیم کنید؛
  • MR و default branch trigger را آزمایش کنید.

روز ۷ تا ۱۰: Integration و Security

  • Environment/data identity و Integration JUnit؛
  • Runner isolation و Controller/Protected Runner؛
  • Fork/MR بدون Secret؛
  • Failure taxonomy، infra-only retry و Quarantine policy.

روز ۱۱ تا ۱۴: Delivery و تصمیم

  • Deploy دستی Staging با همان Artifact؛
  • checksum و Deployment record؛
  • rollback rehearsal؛
  • p50/p95 queue/duration، Flake و maintenance را با baseline مقایسه کنید؛
  • Stage بعدی را براساس Risk اضافه کنید، نه فهرست ابزار.

چک‌لیست نهایی Pipeline پایه

  • Pipeline as Code در همان Repository و زیر review است.
  • Jenkins Controller Build اجرا نمی‌کند یا GitLab Runner trust مشخص دارد.
  • SCM event به SHA دقیق نگاشت می‌شود و Duplicate Pipeline نداریم.
  • Runtime LTS، Image digest، OS/arch و lockfile ثبت شده‌اند.
  • npm ci استفاده می‌شود؛ node_modules Artifact نیست.
  • Lint، Unit، Build و Integration command محلی یکسان دارند.
  • JUnit حتی در Failure upload و نبود آن Fail محسوب می‌شود.
  • Test command exit code واقعی می‌دهد و || true ندارد.
  • Build فقط یک بار تولید و checksum/digest آن ثبت می‌شود.
  • Staging همان Artifact تست‌شده را مصرف می‌کند.
  • timeout، cancellation، retention و cleanup صریح‌اند.
  • Retry فقط Failure زیرساختی محدود را پوشش می‌دهد.
  • Flaky/Quarantine owner و expiry دارد.
  • MR/Fork غیرقابل اعتماد به Deploy Secret یا network دسترسی ندارد.
  • Secret masked/protected/scoped است و در Artifact/Log نیست.
  • Environment/data/dependency Manifest و Failure taxonomy داریم.
  • Queue/Stage p95، time-to-signal و Artifact identity اندازه‌گیری می‌شوند.

سوالات متداول درباره Jenkins و GitLab CI

برای شروع CI/CD، Jenkins بهتر است یا GitLab CI؟

ابزار برنده عمومی وجود ندارد. اگر GitLab مرکز SCM و workflow شماست، integration داخلی مزیت دارد؛ اگر Agent/Plugin/integration متنوع و زیرساخت legacy دارید، Jenkins ممکن است مناسب‌تر باشد. یک Pipeline هم‌معنا را با TCO، patch ownership، isolation، queue و exit test مقایسه کنید.

چرا در CI از npm ci استفاده کنیم؟

npm ci برای نصب خودکار از lockfile طراحی شده، در اختلاف package/lock Fail می‌کند و dependency tree را در CI بازنویسی نمی‌کند. این رفتار reproducibility را بهتر می‌کند؛ ولی همچنان runtime/Image، registry، platform و install scriptها باید کنترل شوند.

آیا JUnit XML به‌تنهایی Job را Fail می‌کند؟

در GitLab نه؛ command تست باید exit code غیرصفر بدهد و JUnit برای نمایش/مقایسه است. در Jenkins نیز publisher می‌تواند Build را Unstable کند، اما نباید command واقعی را با || true بی‌اثر کنید مگر Result policy را آگاهانه مدیریت کنید.

آیا Docker برای CI/CD اجباری است؟

خیر. VM، shell agent، Kubernetes executor و راه‌های دیگر وجود دارند. Docker runtime را بسته‌بندی می‌کند، اما daemon/security، Image supply chain، UID/workspace و environment fidelity را باید مدیریت کرد. Tool را از نیاز isolation و reproducibility انتخاب کنید.

اولین Quality Gate چه باشد؟

با کمترین مجموعه قابل اعتماد شروع کنید: lockfile install، lint/type، Unitهای پرارزش، Build و یک Integration باریک. Gate باید owner، timeout، Evidence و failure policy داشته باشد. Coverage درصدی یا تعداد Test را بدون Risk context threshold جهانی نکنید.

جمع‌بندی: Pipeline خوب مجموعه‌ای از لوگوها نیست؛ یک زنجیره Evidence از Commit تا Artifact است. Jenkinsfile و GitLab YAML این مقاله عمداً Contract یکسان دارند: runtime pin‌شده، نصب lockfile، Test Report، Build یک‌باره، Integration، checksum و Deploy کنترل‌شده. از همین نسخه پایه شروع کنید، Secret را از MR جدا نگه دارید، Failure زیرساخت را از Failure محصول تفکیک کنید و تنها وقتی Stage تازه اضافه کنید که تصمیم Release به Evidence آن نیاز دارد.

دیدگاهتان را بنویسید