11.6 CI CD לפרונטאנד פתרון
פתרון - CI/CD לפרונטאנד¶
פתרון תרגיל 1 - Workflow בסיסי¶
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
ci:
name: CI Pipeline
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install Dependencies
run: npm ci
- name: ESLint
run: npm run lint
- name: TypeScript Check
run: npx tsc --noEmit
- name: Prettier Check
run: npx prettier --check "src/**/*.{ts,tsx,css,json}"
- name: Unit Tests
run: npm test -- --ci --coverage
- name: Build
run: npm run build
הסבר: ה-concurrency מקבצת ריצות לפי workflow ו-branch. cancel-in-progress: true מבטל ריצה קודמת אם push חדש מגיע באותו branch, חוסך זמן ומשאבים.
פתרון תרגיל 2 - Pipeline מרובה שלבים¶
דיאגרמת סדר ריצה:
quality -----> test --------> deploy (main only)
| ^
+-------> build ------> e2e (PR only)
|
+---> deploy
# .github/workflows/pipeline.yml
name: Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
NODE_VERSION: '20'
jobs:
quality:
name: Code Quality
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npx tsc --noEmit
- run: npx prettier --check "src/**/*.{ts,tsx}"
test:
name: Unit Tests
runs-on: ubuntu-latest
needs: quality
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- run: npm test -- --ci --coverage
- uses: actions/upload-artifact@v4
with:
name: coverage
path: coverage/
build:
name: Build
runs-on: ubuntu-latest
needs: quality
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- run: npm run build
env:
NEXT_PUBLIC_API_URL: ${{ secrets.API_URL }}
- uses: actions/upload-artifact@v4
with:
name: build
path: .next/
e2e:
name: E2E Tests
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: always()
with:
name: e2e-report
path: playwright-report/
deploy:
name: Deploy to Production
runs-on: ubuntu-latest
needs: [test, build]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment: production
steps:
- uses: actions/checkout@v4
- name: Deploy to Vercel
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: '--prod'
פתרון תרגיל 3 - Preview Deployment עם Lighthouse¶
# .github/workflows/preview-lighthouse.yml
name: Preview & Lighthouse
on:
pull_request:
branches: [main]
jobs:
deploy-preview:
name: Deploy Preview
runs-on: ubuntu-latest
outputs:
preview-url: ${{ steps.deploy.outputs.preview-url }}
steps:
- uses: actions/checkout@v4
- name: Deploy to Vercel
id: deploy
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
lighthouse:
name: Lighthouse Audit
runs-on: ubuntu-latest
needs: deploy-preview
steps:
- uses: actions/checkout@v4
- name: Run Lighthouse
id: lighthouse
uses: treosh/lighthouse-ci-action@v11
with:
urls: ${{ needs.deploy-preview.outputs.preview-url }}
uploadArtifacts: true
temporaryPublicStorage: true
- name: Format Results
id: format
uses: actions/github-script@v7
with:
script: |
const results = ${{ steps.lighthouse.outputs.manifest }};
const summary = results[0].summary;
const score = (value) => Math.round(value * 100);
const emoji = (value) => value >= 0.9 ? 'V' : value >= 0.5 ? '~' : 'X';
const performance = score(summary.performance);
const accessibility = score(summary.accessibility);
const bestPractices = score(summary['best-practices']);
const seo = score(summary.seo);
const comment = `## Lighthouse Results
| Metric | Score | Status |
|--------|-------|--------|
| Performance | ${performance} | ${emoji(summary.performance)} |
| Accessibility | ${accessibility} | ${emoji(summary.accessibility)} |
| Best Practices | ${bestPractices} | ${emoji(summary['best-practices'])} |
| SEO | ${seo} | ${emoji(summary.seo)} |
Preview: ${{ needs.deploy-preview.outputs.preview-url }}`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment,
});
// כישלון אם לא עומד בתקנים
if (performance < 80) {
core.setFailed(`Performance score ${performance} is below threshold of 80`);
}
if (accessibility < 90) {
core.setFailed(`Accessibility score ${accessibility} is below threshold of 90`);
}
פתרון תרגיל 4 - ניהול משתני סביבה¶
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [develop, main]
jobs:
deploy-staging:
name: Deploy to Staging
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/develop'
environment: staging
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run build
env:
NEXT_PUBLIC_API_URL: ${{ vars.API_URL }}
NEXT_PUBLIC_GA_ID: ${{ vars.GA_ID }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
NEXT_PUBLIC_ENV: staging
- name: Deploy
run: echo "Deploying to staging..."
env:
DEPLOY_URL: ${{ vars.DEPLOY_URL }}
deploy-production:
name: Deploy to Production
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment: production
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run build
env:
NEXT_PUBLIC_API_URL: ${{ vars.API_URL }}
NEXT_PUBLIC_GA_ID: ${{ vars.GA_ID }}
NEXT_PUBLIC_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
NEXT_PUBLIC_ENV: production
- name: Deploy
run: echo "Deploying to production..."
רשימת הגדרות לפי סביבה:
Staging - Secrets:
- DATABASE_URL - כתובת מסד נתונים של staging
Staging - Variables:
- API_URL - https://api-staging.myapp.com
- GA_ID - G-STAGING123
- DEPLOY_URL - https://staging.myapp.com
Production - Secrets:
- DATABASE_URL - כתובת מסד נתונים של production
- SENTRY_AUTH_TOKEN - טוקן Sentry
Production - Variables:
- API_URL - https://api.myapp.com
- GA_ID - G-PROD456
- SENTRY_DSN - https://xxx@sentry.io/yyy
- DEPLOY_URL - https://myapp.com
פתרון תרגיל 5 - בדיקת PR אוטומטית¶
# .github/workflows/pr-checks.yml
name: PR Checks
on:
pull_request:
types: [opened, synchronize, reopened, edited]
jobs:
validate-pr:
name: Validate PR
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# בדיקת תיאור PR
- name: Check PR Description
uses: actions/github-script@v7
with:
script: |
const pr = context.payload.pull_request;
if (!pr.body || pr.body.trim().length < 20) {
core.setFailed('PR must include a description of at least 20 characters');
}
# בדיקת גודל PR
- name: Check PR Size
uses: actions/github-script@v7
with:
script: |
const { data: files } = await github.rest.pulls.listFiles({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
});
const totalChanges = files.reduce((sum, f) => sum + f.additions + f.deletions, 0);
if (totalChanges > 400) {
core.warning(`PR has ${totalChanges} line changes. Consider splitting into smaller PRs.`);
}
// בדיקת קבצי .env
const envFiles = files.filter(f =>
f.filename.includes('.env') && !f.filename.endsWith('.example')
);
if (envFiles.length > 0) {
core.setFailed(`PR contains .env files: ${envFiles.map(f => f.filename).join(', ')}`);
}
# Labels אוטומטיות
- name: Auto Label
uses: actions/github-script@v7
with:
script: |
const { data: files } = await github.rest.pulls.listFiles({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
});
const labels = new Set();
const filenames = files.map(f => f.filename);
if (filenames.some(f => f.includes('test') || f.includes('spec'))) {
labels.add('tests');
}
if (filenames.some(f => f.endsWith('.md') || f.includes('docs'))) {
labels.add('documentation');
}
if (filenames.some(f => f.includes('.yml') || f.includes('.yaml') || f.includes('config'))) {
labels.add('infrastructure');
}
const pr = context.payload.pull_request;
const title = pr.title.toLowerCase();
if (title.startsWith('fix') || title.includes('bug')) labels.add('bug');
if (title.startsWith('feat') || title.includes('feature')) labels.add('feature');
if (labels.size > 0) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: Array.from(labels),
});
}
bundle-size:
name: Bundle Size Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run build
- name: Report Bundle Size
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const path = '.next/build-manifest.json';
if (fs.existsSync(path)) {
const manifest = JSON.parse(fs.readFileSync(path, 'utf-8'));
const pages = Object.keys(manifest.pages);
const comment = `## Bundle Size Report\n\nPages analyzed: ${pages.length}\n\nBuild completed successfully.`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment,
});
}
פתרון תרגיל 6 - CI/CD מלא עם התראות¶
# .github/workflows/full-pipeline.yml
name: Full Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
schedule:
- cron: '0 9 * * 1' # כל יום שני ב-9:00
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
NODE_VERSION: '20'
jobs:
quality:
name: Code Quality
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npx tsc --noEmit
- run: npx prettier --check "src/**/*.{ts,tsx}"
test:
name: Tests
runs-on: ubuntu-latest
needs: quality
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- run: npm test -- --ci --coverage
- uses: actions/upload-artifact@v4
with:
name: coverage
path: coverage/lcov.info
# הערה ב-PR עם coverage
- name: Coverage Report
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
if (fs.existsSync('coverage/coverage-summary.json')) {
const summary = JSON.parse(fs.readFileSync('coverage/coverage-summary.json', 'utf-8'));
const total = summary.total;
const comment = `## Test Coverage Report\n\n| Metric | Coverage |\n|--------|----------|\n| Statements | ${total.statements.pct}% |\n| Branches | ${total.branches.pct}% |\n| Functions | ${total.functions.pct}% |\n| Lines | ${total.lines.pct}% |`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment,
});
}
build:
name: Build
runs-on: ubuntu-latest
needs: quality
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- run: npm run build
security:
name: Security Audit
runs-on: ubuntu-latest
if: github.event_name == 'schedule' || github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- run: npm ci
- run: npm audit --audit-level=high
deploy-staging:
name: Deploy Staging
runs-on: ubuntu-latest
needs: [test, build]
if: github.ref == 'refs/heads/develop' && github.event_name == 'push'
environment: staging
steps:
- uses: actions/checkout@v4
- name: Deploy
run: echo "Deploying to staging..."
deploy-production:
name: Deploy Production
runs-on: ubuntu-latest
needs: [test, build]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment: production
steps:
- uses: actions/checkout@v4
- name: Deploy
run: echo "Deploying to production..."
# התראות
notify-success:
name: Notify Success
runs-on: ubuntu-latest
needs: [deploy-staging, deploy-production]
if: always() && (needs.deploy-staging.result == 'success' || needs.deploy-production.result == 'success')
steps:
- name: Notify Discord
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
run: |
curl -H "Content-Type: application/json" \
-d "{\"content\": \"Deployment successful! Branch: ${{ github.ref_name }}, Commit: ${{ github.sha }}\"}" \
$DISCORD_WEBHOOK
notify-failure:
name: Notify Failure
runs-on: ubuntu-latest
needs: [quality, test, build]
if: failure()
steps:
- name: Notify Discord
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
run: |
curl -H "Content-Type: application/json" \
-d "{\"content\": \"CI FAILED! Branch: ${{ github.ref_name }}, Author: ${{ github.actor }}, Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}" \
$DISCORD_WEBHOOK
תשובות לשאלות¶
1. npm install מול npm ci:
npm install מתקין תלויות לפי package.json ועלול לעדכן את package-lock.json. npm ci (clean install) מתקין בדיוק לפי package-lock.json, מוחק node_modules לפני ההתקנה, ונכשל אם יש חוסר התאמה. ב-CI משתמשים ב-npm ci כי: (א) תוצאה זהה בכל ריצה, (ב) מהיר יותר, (ג) לא משנה קבצים ב-repository.
2. Concurrency:
concurrency מקבצת ריצות workflow לפי group. כש-cancel-in-progress: true, ריצה חדשה באותו group מבטלת ריצות קודמות. זה חשוב כי: אם מפתח עושה 3 pushes מהירים, אין טעם להריץ CI על השניים הראשונים - רק האחרון רלוונטי. חוסך זמן ומשאבים (דקות GitHub Actions).
3. Secrets מול Variables:
Secrets הם ערכים מוצפנים שלא גלויים בלוגים ולא ניתן לראות את ערכם אחרי הגדרה - מתאימים למפתחות API, סיסמאות, טוקנים. Variables הם ערכים גלויים שמופיעים בלוגים - מתאימים לכתובות URL, מזהי פרויקט, הגדרות שאינן רגישות.
4. בדיקות E2E ב-CI:
בדיקות יחידה בודקות פונקציות בודדות בבידוד, אך לא מוודאות שהמערכת כולה עובדת יחד. בדיקות E2E מדמות משתמש אמיתי - לחיצות, ניווט, מילוי טפסים - ותופסות באגים שבדיקות יחידה מפספסות: שגיאות אינטגרציה, בעיות CSS, ניתוב שבור. הרצה ב-CI מבטיחה שכל PR נבדק בצורה מקיפה.
5. Preview Deployment:
Preview Deployment הוא פריסה זמנית שנוצרת אוטומטית לכל PR. היתרון: הסוקר יכול לראות ולבדוק את השינויים באתר חי, לא רק בקוד. אפשר גם להריץ בדיקות אוטומטיות (Lighthouse, E2E) על ה-preview. זה מפחית באגים שמגיעים לפרודקשן כי יש שכבת בדיקה נוספת לפני המיזוג.