ããã«ã¡ã¯ïŒå²è¶ã§ãã
ã€ãã«Windsurfå®å šãã¹ã¿ãŒè¬åº§ã®æçµåãPart5ããå±ãããŸãïŒä»åã¯ãå人éçºããäžæ©é²ãã§ãããŒã éçºãšãããã¯ãéçšã®äžçãžãæ¡å ããŸãããð
ãäžäººã§éçºããã®ã¯æ £ãããã©ãããŒã ã§ã¯ã©ãããã®ïŒããäœã£ãã¢ããªãã¡ãããšãŠãŒã¶ãŒã«å±ãããããããã®éçºçŸå Žã£ãŠã©ããªæãïŒã
ãããªçãããšäžç·ã«ãGit FlowãCI/CDãã¢ãã¿ãªã³ã°ããããŠã¹ã±ãŒãªã³ã°ãŸã§ããããã§ãã·ã§ãã«ãªéçºã®å šãŠãåŠãã§ãããŸãããïŒ
- ð¯ Part5ã§åŠã¶ãããã§ãã·ã§ãã«ã¹ãã«
- ð€ ããŒã éçºïŒGit Flowãšã³ã©ãã¬ãŒã·ã§ã³
- ð CI/CDïŒèªååããããããã€ã¡ã³ããã€ãã©ã€ã³
- ð ç£èŠã»åæïŒãããã¯ãã®å¥åº·ç¶æ ãææ¡
- ð ã¹ã±ãŒãªã³ã°ãšã€ã³ãã©ã¹ãã©ã¯ãã£
- ð ãããã¯ãæé·ïŒA/Bãã¹ããšæ©èœãã©ã°
- ð Windsurfå®å šãã¹ã¿ãŒè¬åº§å®çµ
- ð¡ å²è¶ããã®æçµã¡ãã»ãŒãž
- ð å®å šãã¹ã¿ãŒéæããã§ãšãããããŸãïŒ
ð¯ Part5ã§åŠã¶ãããã§ãã·ã§ãã«ã¹ãã«
ð åŠç¿ããŒãããã
| åé | æè¡ã»ææ³ | ç¿åŸæé | ããžãã¹äŸ¡å€ |
|---|---|---|---|
| ð€ ããŒã éçº | Git Flowãã³ãŒãã¬ãã¥ãŒããã¢ããã°ã©ãã³ã° | 2-3é±é | âââââ |
| ð CI/CD | GitHub Actionsãèªåãã¹ããèªåããã〠| 1-2é±é | âââââ |
| ð ç£èŠã»åæ | ãšã©ãŒãã©ããã³ã°ãããã©ãŒãã³ã¹ç£èŠ | 1é±é | ââââ |
| ð ã€ã³ãã© | ã¯ã©ãŠããCDNãã¹ã±ãŒãªã³ã° | 2-3é±é | ââââ |
| ð ãããã¯ãæé· | A/Bãã¹ããåæãæ©èœãã©ã° | 1-2é±é | âââââ |
ð€ ããŒã éçºïŒGit Flowãšã³ã©ãã¬ãŒã·ã§ã³
ð¿ Git Flowã®å®è·µ
Git Flowãšã¯ïŒ
Git Flowã¯ãããŒã éçºã§Gitãå¹ççã«äœ¿ãããã®ãã©ã³ãæŠç¥ã§ãã以äžã®5çš®é¡ã®ãã©ã³ãã䜿ããŸãïŒ
- main/masterïŒæ¬çªç°å¢ã®ã³ãŒã
- developïŒéçºçšã®ã¡ã€ã³ãã©ã³ã
- feature/*ïŒæ°æ©èœéçºçš
- release/*ïŒãªãªãŒã¹æºåçš
- hotfix/*ïŒç·æ¥ä¿®æ£çš
ð¬ Cascadeãšã®ããŒã éçºèšå®äŒè©±
ããªãããŒã éçºãå§ãããããGit Flowã®èšå®ãšãã«ãªã¯ãšã¹ãã®ãã³ãã¬ãŒããã³ãŒãã¬ãã¥ãŒã®ã¬ã€ãã©ã€ã³ãäœæããŠãã ãããåå¿è ã«ãåãããããããã¥ã¡ã³ããå«ããŠãé¡ãããŸãã”
CascadeããŒã éçºã®ãã¹ããã©ã¯ãã£ã¹ãå®è£ ããŸãããïŒä»¥äžã®æ§æã§é²ããŸãïŒ
**ã»ããã¢ããå 容ïŒ**
1. Git Flowèšå®ãšãã©ã³ãä¿è·ã«ãŒã«
2. ãã«ãªã¯ãšã¹ããã³ãã¬ãŒã
3. ã³ãŒãã¬ãã¥ãŒãã§ãã¯ãªã¹ã
4. ããŒã éçºã¬ã€ãã©ã€ã³
5. WindsurfãšGitã®é£æºèšå®åå¿è ã®æ¹ã«ãåããããããããå®äŸä»ãã§èª¬æããŸããã”
Git Flowå®è£ äŸ
# .github/PULL_REQUEST_TEMPLATE.md
## ð 倿Žå
容
## ð¯ 倿Žã®çç±
## 𧪠ãã¹ã
- [ ] ãŠããããã¹ãã®å®è¡
- [ ] æåãã¹ãã®å®è¡
- [ ] æ¢åæ©èœãžã®åœ±é¿ç¢ºèª
## ð· ã¹ã¯ãªãŒã³ã·ã§ãã
## â
ãã§ãã¯ãªã¹ã
- [ ] ã³ãŒãã¬ãã¥ãŒã¬ã€ãã©ã€ã³ã«åŸã£ãŠãã
- [ ] ãã¹ãã远å /æŽæ°ãããŠãã
- [ ] ããã¥ã¡ã³ããæŽæ°ãããŠããïŒå¿
èŠã«å¿ããŠïŒ
- [ ] ãã¬ã€ãã³ã°ãã§ã³ãžã®å Žåã¯æèšãããŠãã
## ð é¢é£Issue
Closes #
---
### ã¬ãã¥ã¯ãŒãžã®ãé¡ã
# ããŒã éçºã¬ã€ãã©ã€ã³ïŒTEAM_GUIDELINES.mdïŒ
## ð¿ ãã©ã³ãéçšã«ãŒã«
### ãã©ã³ãåœåèŠå
```bash
# æ©èœéçº
feature/todo-edit-functionality
feature/user-authentication
# ãã°ä¿®æ£
bugfix/fix-todo-delete-error
bugfix/resolve-memory-leak
# ããããã£ãã¯ã¹
hotfix/fix-critical-security-issue
# ãªãªãŒã¹æºå
release/v1.2.0
```
### éçºãããŒ
#### 1. æ°æ©èœéçºã®æµã
```bash
# 1. developãã©ã³ãããææ°ãååŸ
git checkout develop
git pull origin develop
# 2. featureãã©ã³ããäœæ
git checkout -b feature/todo-edit-functionality
# 3. éçºãé²ãã
# ... ã³ãŒãç·šé ...
git add .
git commit -m "feat: ToDoã¢ã€ãã ã®ç·šéæ©èœã远å "
# 4. 宿çã«developãã©ã³ãã®å€æŽãåã蟌ã
git checkout develop
git pull origin develop
git checkout feature/todo-edit-functionality
git merge develop
# 5. ããã·ã¥ããŠãã«ãªã¯ãšã¹ãäœæ
git push origin feature/todo-edit-functionality
# GitHubã§ãã«ãªã¯ãšã¹ãäœæ
```
#### 2. ã³ãããã¡ãã»ãŒãžèŠåïŒConventional CommitsïŒ
```bash
# æ°æ©èœ
feat: ãŠãŒã¶ãŒèªèšŒæ©èœã远å
# ãã°ä¿®æ£
fix: ToDoã¢ã€ãã å逿ã®ãšã©ãŒãä¿®æ£
# ããã¥ã¡ã³ã
docs: READMEã«ã»ããã¢ããæé ã远å
# ãªãã¡ã¯ã¿ãªã³ã°
refactor: TodoServiceã¯ã©ã¹ãåå²
# ãã¹ã
test: ToDoã¢ã€ãã äœæã®ãã¹ãã远å
# ã¹ã¿ã€ã«
style: ESLintãšã©ãŒãä¿®æ£
# ããã©ãŒãã³ã¹
perf: ç»åèªã¿èŸŒã¿ãæé©å
# ãã«ã
build: webpackã®èšå®ãæŽæ°
```
GitHub Actionsèšå®
# .github/workflows/ci.yml
name: CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
# ã³ãŒãå質ãã§ãã¯
quality-check:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint check
run: npm run lint
- name: Format check
run: npm run format:check
- name: Type check
run: npm run type-check
# ãã¹ãå®è¡
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm run test:coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
# E2Eãã¹ã
e2e-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build application
run: npm run build
- name: Run Cypress tests
uses: cypress-io/github-action@v5
with:
start: npm start
wait-on: 'http://localhost:3000'
# ã»ãã¥ãªãã£ãã§ãã¯
security:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Run security audit
run: npm audit --audit-level=moderate
- name: Run Snyk security check
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
# ããã©ãŒãã³ã¹ãã¹ã
lighthouse:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build application
run: npm run build
- name: Serve application
run: npm start &
- name: Wait for server
run: npx wait-on http://localhost:3000
- name: Run Lighthouse CI
run: npx @lhci/cli@0.9.x autorun
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
# èªåãããã€ïŒmainãã©ã³ãã®ã¿ïŒ
deploy:
needs: [quality-check, test, e2e-test]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build application
run: npm run build
- name: Deploy to Vercel
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.ORG_ID }}
vercel-project-id: ${{ secrets.PROJECT_ID }}
vercel-args: '--prod'
- name: Notify Slack
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
channel: '#deployments'
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
if: always()
ð¥ ã³ãŒãã¬ãã¥ãŒã®ãã¹ããã©ã¯ãã£ã¹
广çãªã³ãŒãã¬ãã¥ãŒã¬ã€ã
# ã³ãŒãã¬ãã¥ãŒãã§ãã¯ãªã¹ãïŒCODE_REVIEW_CHECKLIST.mdïŒ
## ð æ©èœé¢ã®ãã§ãã¯
### â
èŠä»¶å
è¶³
- [ ] 仿§æžã»Issueã®èŠä»¶ãæºãããŠãã
- [ ] ãšããžã±ãŒã¹ãèæ
®ãããŠãã
- [ ] ãšã©ãŒãã³ããªã³ã°ãé©å
- [ ] ããã©ãŒãã³ã¹ãžã®åœ±é¿ãæå°é
### â
ãã¹ã
- [ ] æ°æ©èœã«ãã¹ãã远å ãããŠãã
- [ ] æ¢åãã¹ããéã
- [ ] ãã¹ãã«ãã¬ããžãç¶æ/åäžããŠãã
- [ ] ãã¹ãã±ãŒã¹ãé©å
## ðš ã³ãŒãå質ã®ãã§ãã¯
### â
å¯èªæ§
- [ ] 倿°ã»é¢æ°åãåããããã
- [ ] ã³ã¡ã³ããé©åïŒå€ãããå°ãªãããïŒ
- [ ] ã³ãŒãã®æ§é ãçè§£ãããã
- [ ] ããžãã¯ãã³ããŒã宿°åãããŠãã
### â
ä¿å®æ§
- [ ] DRYååã«åŸã£ãŠããïŒéè€ãé©åã«æé€ïŒ
- [ ] SOLIDååã«åŸã£ãŠãã
- [ ] 颿°ã»ã¯ã©ã¹ã®è²¬ä»»ãåäž
- [ ] äŸåé¢ä¿ãæç¢º
### â
ã»ãã¥ãªãã£
- [ ] ãŠãŒã¶ãŒå
¥åãé©åã«ãµãã¿ã€ãºãããŠãã
- [ ] æ©å¯æ
å ±ãããŒãã³ãŒããããŠããªã
- [ ] æš©éãã§ãã¯ãé©å
- [ ] SQLã€ã³ãžã§ã¯ã·ã§ã³çã®è匱æ§ããªã
## ð¬ ã³ãŒãã¬ãã¥ãŒã³ã¡ã³ãã®æžãæ¹
### â
建èšçãªãã£ãŒãããã¯
```markdown
# è¯ãäŸ
ð¡ **ææ¡**: ãã®éšåã颿°ã«åé¢ãããšãåå©çšæ§ãåäžãããã§ãã
```javascript
function calculateTotalPrice(items) {
return items.reduce((total, item) => total + item.price, 0);
}
```
# æªãäŸ
â ãã®ã³ãŒãã¯ãã¡ã§ãã
```
### â
å
·äœçãªæ¹åæ¡
```markdown
# è¯ãäŸ
ð§ **æ¹åæ¡**: ããã©ãŒãã³ã¹åäžã®ãããuseMemoã䜿ã£ãæé©åãæ€èšããŠã¿ãŠãã ããã
```javascript
const expensiveValue = useMemo(() => {
return heavyCalculation(data);
}, [data]);
```
# æªãäŸ
é
ãããŸãã
```
### â
åŠç¿æ©äŒã®æäŸ
```markdown
# è¯ãäŸ
ð **åè**: ãã®å®è£
ãã¿ãŒã³ã«ã€ããŠããã¡ãã®èšäºãåèã«ãªããŸãïŒ

Example Domain
# æªãäŸ
ããã¯ééãã§ãã
```
## ð¯ ã¬ãã¥ãŒæã®å¿æ§ã
### ã¬ãã¥ã¢ãŒãšããŠ
- ð€ **建èšçã§ããããš**: æ¹å€ã§ã¯ãªãæ¹åææ¡ã
- ð **åŠç¿æ©äŒ**: ãªããã®æ¹æ³ãè¯ããã説æ
- â° **è¿
éãªå¯Ÿå¿**: 24æé以å
ã«ã¬ãã¥ãŒ
- ð¯ **éèŠåºŠã®åºå¥**: å¿
é vs ææ¡ vs nitïŒè»œåŸ®ãªææïŒ
### ã¬ãã¥ã€ãŒãšããŠ
- ð **æè¬ã®æ°æã¡**: ãã£ãŒãããã¯ã«æè¬ã瀺ã
- ð€ **çè§£ãæ·±ãã**: åãããªãææã¯è³ªåãã
- ð **çŽ æ©ãä¿®æ£**: ææäºé
ãžã®è¿
éãªå¯Ÿå¿
- ð **åŠç¿å§¿å¢**: åãææãç¹°ãè¿ããªã
## ð ãã«ãªã¯ãšã¹ãã®æžãæ¹
### â
è¯ãã¿ã€ãã«äŸ
- `feat: ToDoã¢ã€ãã ã®ç·šéæ©èœã远å `
- `fix: ãã°ã€ã³æã®ããªããŒã·ã§ã³ãšã©ãŒãä¿®æ£`
- `refactor: UserServiceã¯ã©ã¹ãåå²ããŠãã¹ã¿ããªãã£ãåäž`
### â
è¯ã説ææäŸ
```markdown
## 倿Žå
容
ToDoã¢ã€ãã ãã€ã³ã©ã€ã³ç·šéã§ããæ©èœã远å ããŸããã
## 倿Žã®çç±
ãŠãŒã¶ãŒããã®èŠæã§ãToDoã¢ã€ãã ãç·šéããããã«
å¥ç»é¢ã«é·ç§»ããã®ãäžäŸ¿ãšãã声ãå€ããã£ãããã
## å®è£
詳现
- ããã«ã¯ãªãã¯ã§ç·šéã¢ãŒãã«åãæ¿ã
- EnterããŒã§ä¿åãEscapeããŒã§ãã£ã³ã»ã«
- ããªããŒã·ã§ã³ãšã©ãŒã®è¡šç€º
## ãã¹ã
- [ ] æ£åžžãªç·šéãããŒ
- [ ] 空æåã§ã®ä¿å詊è¡
- [ ] ãã£ã³ã»ã«æäœ
- [ ] æ¢åæ©èœãžã®åœ±é¿ç¢ºèª
```
ð CI/CDïŒèªååããããããã€ã¡ã³ããã€ãã©ã€ã³
ð ç¶ç¶çã€ã³ãã°ã¬ãŒã·ã§ã³
CI/CDèšå®ã®äŸé ŒïŒ“ãããã¯ã·ã§ã³ã¬ãã«ã®CI/CDãã€ãã©ã€ã³ãæ§ç¯ãããã§ããGitHub Actionsã䜿ã£ãŠããã¹ãããã«ãããããã€ãŸã§ãèªååããŠãã ããã”
é«åºŠãªã¯ãŒã¯ãããŒèšå®
# .github/workflows/production.yml
name: Production Deployment
on:
push:
branches: [ main ]
tags: [ 'v*' ]
env:
NODE_VERSION: '18'
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
# 倿޿€ç¥ïŒmonorepo察å¿ïŒ
changes:
runs-on: ubuntu-latest
outputs:
frontend: ${{ steps.changes.outputs.frontend }}
backend: ${{ steps.changes.outputs.backend }}
infrastructure: ${{ steps.changes.outputs.infrastructure }}
steps:
- uses: actions/checkout@v3
- uses: dorny/paths-filter@v2
id: changes
with:
filters: |
frontend:
- 'frontend/**'
- 'shared/**'
backend:
- 'backend/**'
- 'shared/**'
infrastructure:
- 'infrastructure/**'
- '.github/workflows/**'
# ããã³ããšã³ã CI
frontend-ci:
needs: changes
if: ${{ needs.changes.outputs.frontend == 'true' }}
runs-on: ubuntu-latest
strategy:
matrix:
browser: [chrome, firefox, safari]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
- name: Install dependencies
working-directory: frontend
run: npm ci
- name: Run linting
working-directory: frontend
run: npm run lint
- name: Run type checking
working-directory: frontend
run: npm run type-check
- name: Run unit tests
working-directory: frontend
run: npm run test:coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./frontend/coverage/lcov.info
flags: frontend
- name: Build application
working-directory: frontend
run: npm run build
env:
NODE_ENV: production
VITE_API_URL: ${{ secrets.PROD_API_URL }}
- name: Run E2E tests
working-directory: frontend
run: npm run test:e2e:${{ matrix.browser }}
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
- name: Upload E2E artifacts
uses: actions/upload-artifact@v3
if: failure()
with:
name: cypress-screenshots-${{ matrix.browser }}
path: frontend/cypress/screenshots
- name: Performance audit
run: |
npm install -g @lhci/cli@0.9.x
lhci autorun
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: frontend-build
path: frontend/dist
retention-days: 30
# ããã¯ãšã³ã CI
backend-ci:
needs: changes
if: ${{ needs.changes.outputs.backend == 'true' }}
runs-on: ubuntu-latest
services:
postgres:
image: postgres:14
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis:7
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 6379:6379
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: backend/package-lock.json
- name: Install dependencies
working-directory: backend
run: npm ci
- name: Run linting
working-directory: backend
run: npm run lint
- name: Run security audit
working-directory: backend
run: npm audit --audit-level=moderate
- name: Run database migrations
working-directory: backend
run: npm run migrate
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
- name: Run tests
working-directory: backend
run: npm run test:coverage
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
REDIS_URL: redis://localhost:6379
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./backend/coverage/lcov.info
flags: backend
- name: Build Docker image
run: |
docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
--build-arg NODE_VERSION=${{ env.NODE_VERSION }} \
backend/
- name: Log in to Container Registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Push Docker image
run: |
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
docker tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
# ã»ãã¥ãªãã£ã¹ãã£ã³
security-scan:
runs-on: ubuntu-latest
permissions:
security-events: write
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy scan results
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'
- name: Run CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
languages: javascript
# ãããã€ã¡ã³ã
deploy:
needs: [frontend-ci, backend-ci, security-scan]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment:
name: production
url: https://app.example.com
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Download build artifacts
uses: actions/download-artifact@v3
with:
name: frontend-build
path: ./dist
- name: Deploy to CDN
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Sync to S3
run: |
aws s3 sync ./dist s3://${{ secrets.S3_BUCKET }} --delete
aws cloudfront create-invalidation \
--distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} \
--paths "/*"
- name: Deploy backend
uses: azure/k8s-deploy@v1
with:
manifests: |
k8s/deployment.yaml
k8s/service.yaml
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
kubeconfig: ${{ secrets.KUBE_CONFIG }}
- name: Run smoke tests
run: |
npx newman run postman/smoke-tests.json \
--environment postman/production.json \
--reporters cli,json \
--reporter-json-export smoke-test-results.json
- name: Notify deployment status
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
channel: '#deployments'
text: |
ð Production deployment ${{ job.status }}!
ðŠ Version: ${{ github.sha }}
ð URL: https://app.example.com
ð Smoke tests: ${{ steps.smoke-tests.outcome }}
<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Details>
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
if: always()
- name: Create GitHub release
if: startsWith(github.ref, 'refs/tags/')
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
body: |
## 倿Žå
容
詳现ãªå€æŽå
容ã«ã€ããŠã¯ [CHANGELOG.md](./CHANGELOG.md) ãã確èªãã ããã
## ãããã€æ
å ±
- Frontend: https://app.example.com
- API: https://api.example.com
- Documentation: https://docs.example.com
draft: false
prerelease: false
ð ç°å¢ç®¡çãšã·ãŒã¯ã¬ãã管ç
ç°å¢å¥èšå®ã®ç®¡ç
# config/environments.js
const environments = {
development: {
API_URL: 'http://localhost:3001',
DATABASE_URL: 'postgresql://localhost:5432/myapp_dev',
REDIS_URL: 'redis://localhost:6379',
LOG_LEVEL: 'debug',
ENABLE_DEBUGGING: true,
RATE_LIMIT: {
windowMs: 15 * 60 * 1000, // 15å
max: 1000 // ãªã¯ãšã¹ãæ°
}
},
staging: {
API_URL: 'https://api-staging.example.com',
DATABASE_URL: process.env.DATABASE_URL,
REDIS_URL: process.env.REDIS_URL,
LOG_LEVEL: 'info',
ENABLE_DEBUGGING: false,
RATE_LIMIT: {
windowMs: 15 * 60 * 1000,
max: 100
}
},
production: {
API_URL: 'https://api.example.com',
DATABASE_URL: process.env.DATABASE_URL,
REDIS_URL: process.env.REDIS_URL,
LOG_LEVEL: 'warn',
ENABLE_DEBUGGING: false,
RATE_LIMIT: {
windowMs: 15 * 60 * 1000,
max: 100
}
}
};
// ç°å¢èšå®ã®ååŸ
function getConfig() {
const env = process.env.NODE_ENV || 'development';
const config = environments[env];
if (!config) {
throw new Error(`Unknown environment: ${env}`);
}
// å¿
é ç°å¢å€æ°ã®ãã§ãã¯
if (env !== 'development') {
const requiredVars = ['DATABASE_URL', 'REDIS_URL'];
const missing = requiredVars.filter(key => !process.env[key]);
if (missing.length > 0) {
throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
}
}
return config;
}
module.exports = { getConfig };
# .env.exampleïŒããŒã å
±æçšã®ãã³ãã¬ãŒãïŒ
# ããŒã¿ããŒã¹
DATABASE_URL=postgresql://username:password@localhost:5432/myapp
# Redis
REDIS_URL=redis://localhost:6379
# å€éšAPI
OPENWEATHER_API_KEY=your_api_key_here
STRIPE_SECRET_KEY=sk_test_your_stripe_key
# èªèšŒ
JWT_SECRET=your_jwt_secret
JWT_EXPIRES_IN=7d
# ã¡ãŒã«
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASS=your-app-password
# ãã¡ã€ã«ã¢ããããŒã
AWS_ACCESS_KEY_ID=your_aws_key
AWS_SECRET_ACCESS_KEY=your_aws_secret
AWS_S3_BUCKET=your-bucket-name
AWS_REGION=us-east-1
# ç£èŠã»åæ
SENTRY_DSN=https://your-sentry-dsn
GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX
# éçºã»ãããã°
DEBUG=myapp:*
LOG_LEVEL=debug
ð ç£èŠã»åæïŒãããã¯ãã®å¥åº·ç¶æ ãææ¡
ð ãšã©ãŒãã©ããã³ã°ãšç£èŠ
ç£èŠã·ã¹ãã ã®äŸé ŒïŒ“ãããã¯ã·ã§ã³ã§çºçãããšã©ãŒãããã©ãŒãã³ã¹åé¡ãå³åº§ã«æ€ç¥ã§ããç£èŠã·ã¹ãã ãæ§ç¯ããŠãã ãããSentryãGoogle Analyticsãã«ã¹ã¿ã ã¡ããªã¯ã¹ãçµã¿åããããã§ãã”
å æ¬çç£èŠã·ã¹ãã ã®å®è£
// monitoring/errorTracking.js
import * as Sentry from '@sentry/browser';
import { Integrations } from '@sentry/tracing';
class ErrorTracker {
constructor(dsn, environment = 'production') {
this.init(dsn, environment);
this.setupGlobalErrorHandlers();
}
init(dsn, environment) {
Sentry.init({
dsn,
environment,
integrations: [
new Integrations.BrowserTracing(),
new Sentry.Replay({
// ã»ãã·ã§ã³ã®10%ãèšé²
sessionSampleRate: 0.1,
// ãšã©ãŒããã£ãã»ãã·ã§ã³ã¯100%èšé²
errorSampleRate: 1.0,
}),
],
// ããã©ãŒãã³ã¹ç£èŠ
tracesSampleRate: 0.1,
// ãªãªãŒã¹æ
å ±
release: process.env.REACT_APP_VERSION,
// ãŠãŒã¶ãŒã³ã³ããã¹ã
beforeSend(event, hint) {
// æ©å¯æ
å ±ã®ãã£ã«ã¿ãªã³ã°
if (event.exception) {
const error = hint.originalException;
// ãã¹ã¯ãŒããããŒã¯ã³ãå«ããšã©ãŒã¯éä¿¡ããªã
if (error.message &&
/password|token|secret|key/i.test(error.message)) {
return null;
}
}
return event;
},
// ã«ã¹ã¿ã ãã£ã³ã¬ãŒããªã³ãã£ã³ã°
fingerprint: ['{{ default }}', '{{ transaction }}']
});
}
setupGlobalErrorHandlers() {
// æªåŠçã® Promise ãšã©ãŒ
window.addEventListener('unhandledrejection', event => {
console.error('Unhandled promise rejection:', event.reason);
Sentry.captureException(event.reason, {
tags: {
errorType: 'unhandledRejection'
},
extra: {
promise: event.promise
}
});
});
// React Error Boundary ã§ã¯ææã§ããªããšã©ãŒ
window.addEventListener('error', event => {
if (event.error) {
Sentry.captureException(event.error, {
tags: {
errorType: 'globalError'
},
extra: {
filename: event.filename,
lineno: event.lineno,
colno: event.colno
}
});
}
});
}
// ãŠãŒã¶ãŒæ
å ±ã®èšå®
setUser(user) {
Sentry.setUser({
id: user.id,
email: user.email,
username: user.username,
subscription: user.subscription
});
}
// ã«ã¹ã¿ã ãšã©ãŒã®å ±å
captureError(error, context = {}) {
Sentry.captureException(error, {
tags: context.tags || {},
extra: context.extra || {},
level: context.level || 'error'
});
}
// ãŠãŒã¶ãŒã¢ã¯ã·ã§ã³ã®è¿œè·¡
trackUserAction(action, data = {}) {
Sentry.addBreadcrumb({
message: action,
data,
level: 'info',
type: 'user'
});
}
// ããã©ãŒãã³ã¹ã®æž¬å®
measurePerformance(name, fn) {
const transaction = Sentry.startTransaction({ name });
return Promise.resolve(fn())
.then(result => {
transaction.setStatus('ok');
return result;
})
.catch(error => {
transaction.setStatus('internal_error');
throw error;
})
.finally(() => {
transaction.finish();
});
}
}
// 䜿çšäŸ
const errorTracker = new ErrorTracker(
process.env.REACT_APP_SENTRY_DSN,
process.env.NODE_ENV
);
// ãŠãŒã¶ãŒãã°ã€ã³æ
errorTracker.setUser(currentUser);
// APIåŒã³åºãæã®ãšã©ãŒãã©ããã³ã°
async function fetchTodos() {
try {
errorTracker.trackUserAction('fetch_todos_started');
const response = await errorTracker.measurePerformance(
'api_fetch_todos',
() => fetch('/api/todos')
);
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
const todos = await response.json();
errorTracker.trackUserAction('fetch_todos_success', {
count: todos.length
});
return todos;
} catch (error) {
errorTracker.captureError(error, {
tags: {
operation: 'fetch_todos',
api_endpoint: '/api/todos'
},
extra: {
userAgent: navigator.userAgent,
timestamp: new Date().toISOString()
}
});
throw error;
}
}
export default errorTracker;
ã«ã¹ã¿ã ã¡ããªã¯ã¹åé
// monitoring/metrics.js
class MetricsCollector {
constructor() {
this.metrics = new Map();
this.timers = new Map();
this.setupPerformanceObserver();
this.setupResourceMonitoring();
}
// Core Web Vitals ã®æž¬å®
setupPerformanceObserver() {
// Largest Contentful Paint (LCP)
new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
const lastEntry = entries[entries.length - 1];
this.recordMetric('web_vitals_lcp', lastEntry.startTime, {
element: lastEntry.element?.tagName || 'unknown',
url: lastEntry.url || 'unknown'
});
}).observe({entryTypes: ['largest-contentful-paint']});
// First Input Delay (FID)
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
const fid = entry.processingStart - entry.startTime;
this.recordMetric('web_vitals_fid', fid, {
eventType: entry.name,
target: entry.target?.tagName || 'unknown'
});
}
}).observe({entryTypes: ['first-input']});
// Cumulative Layout Shift (CLS)
let clsValue = 0;
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
if (!entry.hadRecentInput) {
clsValue += entry.value;
}
}
this.recordMetric('web_vitals_cls', clsValue);
}).observe({entryTypes: ['layout-shift']});
}
// ãªãœãŒã¹ç£èŠ
setupResourceMonitoring() {
// ã¡ã¢ãªäœ¿çšéã®ç£èŠ
if ('memory' in performance) {
setInterval(() => {
const memory = performance.memory;
this.recordMetric('memory_used', memory.usedJSHeapSize / 1048576); // MB
this.recordMetric('memory_total', memory.totalJSHeapSize / 1048576);
this.recordMetric('memory_limit', memory.jsHeapSizeLimit / 1048576);
}, 30000); // 30ç§ããš
}
// ãããã¯ãŒã¯ç¶æ
ã®ç£èŠ
if ('connection' in navigator) {
const connection = navigator.connection;
this.recordMetric('network_type', connection.effectiveType, {
downlink: connection.downlink,
rtt: connection.rtt,
saveData: connection.saveData
});
connection.addEventListener('change', () => {
this.recordMetric('network_change', connection.effectiveType, {
downlink: connection.downlink,
rtt: connection.rtt
});
});
}
}
// ã«ã¹ã¿ã ã¡ããªã¯ã¹ã®èšé²
recordMetric(name, value, tags = {}) {
const metric = {
name,
value,
tags,
timestamp: Date.now(),
sessionId: this.getSessionId()
};
// ããŒã«ã«ã§ã®èç©
if (!this.metrics.has(name)) {
this.metrics.set(name, []);
}
this.metrics.get(name).push(metric);
// ãªã¢ã«ã¿ã€ã éä¿¡ïŒéèŠãªã¡ããªã¯ã¹ã®ã¿ïŒ
if (this.isImportantMetric(name)) {
this.sendMetric(metric);
}
// ã³ã³ãœãŒã«ã§ã®ç¢ºèªïŒéçºç°å¢ã®ã¿ïŒ
if (process.env.NODE_ENV === 'development') {
console.log(`ð Metric: ${name} = ${value}`, tags);
}
}
// ããã©ãŒãã³ã¹æž¬å®ã®éå§
startTimer(name) {
this.timers.set(name, {
startTime: performance.now(),
name
});
}
// ããã©ãŒãã³ã¹æž¬å®ã®çµäº
endTimer(name, tags = {}) {
const timer = this.timers.get(name);
if (timer) {
const duration = performance.now() - timer.startTime;
this.recordMetric(`duration_${name}`, duration, tags);
this.timers.delete(name);
return duration;
}
return null;
}
// ãŠãŒã¶ãŒã¢ã¯ã·ã§ã³ã®æž¬å®
measureUserAction(actionName, actionFn) {
this.startTimer(actionName);
this.recordMetric(`action_${actionName}_started`, 1);
const startTime = Date.now();
return Promise.resolve(actionFn())
.then(result => {
const duration = this.endTimer(actionName);
this.recordMetric(`action_${actionName}_success`, 1, {
duration: Math.round(duration)
});
return result;
})
.catch(error => {
this.endTimer(actionName);
this.recordMetric(`action_${actionName}_error`, 1, {
errorType: error.name || 'UnknownError'
});
throw error;
});
}
// éèŠã¡ããªã¯ã¹ã®å€å®
isImportantMetric(name) {
const importantMetrics = [
'web_vitals_lcp',
'web_vitals_fid',
'web_vitals_cls',
'api_error',
'user_action_error'
];
return importantMetrics.some(metric => name.includes(metric));
}
// ã»ãã·ã§ã³IDã®ååŸ/çæ
getSessionId() {
let sessionId = sessionStorage.getItem('sessionId');
if (!sessionId) {
sessionId = 'session_' + Date.now() + '_' +
Math.random().toString(36).substr(2, 9);
sessionStorage.setItem('sessionId', sessionId);
}
return sessionId;
}
// ã¡ããªã¯ã¹ã®éä¿¡
async sendMetric(metric) {
try {
await fetch('/api/metrics', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(metric)
});
} catch (error) {
console.error('Failed to send metric:', error);
}
}
// ãããã§ã¡ããªã¯ã¹ãéä¿¡
async flushMetrics() {
const allMetrics = [];
for (const [name, metrics] of this.metrics.entries()) {
allMetrics.push(...metrics);
}
if (allMetrics.length > 0) {
try {
await fetch('/api/metrics/batch', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(allMetrics)
});
// éä¿¡åŸã«ã¯ãªã¢
this.metrics.clear();
} catch (error) {
console.error('Failed to flush metrics:', error);
}
}
}
}
// ã°ããŒãã«ã€ã³ã¹ã¿ã³ã¹
const metrics = new MetricsCollector();
// ããŒãžé¢è±æã«ã¡ããªã¯ã¹ãéä¿¡
window.addEventListener('beforeunload', () => {
metrics.flushMetrics();
});
// 宿çãªãã©ãã·ã¥ïŒ5åããšïŒ
setInterval(() => {
metrics.flushMetrics();
}, 5 * 60 * 1000);
// 䜿çšäŸ
metrics.measureUserAction('todo_creation', async () => {
const response = await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(todoData)
});
return response.json();
});
export default metrics;
ð ã¹ã±ãŒãªã³ã°ãšã€ã³ãã©ã¹ãã©ã¯ãã£
âïž ã¯ã©ãŠãã¢ãŒããã¯ãã£
ã¹ã±ãŒãªã³ã°èšèšã®äŸé ŒïŒ“ãŠãŒã¶ãŒæ°ã®å¢å ã«å¯Ÿå¿ã§ããã¹ã±ãŒã©ãã«ãªã¢ãŒããã¯ãã£ãèšèšããŠãã ãããCDNãããŒããã©ã³ãµãŒãããŒã¿ããŒã¹ã®ã¹ã±ãŒãªã³ã°æŠç¥ãå«ãããã§ãã”
Docker & Kubernetesèšå®
# DockerfileïŒãã«ãã¹ããŒãžãã«ãïŒ
# Stage 1: Build
FROM node:18-alpine AS builder
WORKDIR /app
# äŸåé¢ä¿ã®ãã£ãã·ã¥æé©å
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
# ã¢ããªã±ãŒã·ã§ã³ã®ãã«ã
COPY . .
RUN npm run build
# Stage 2: Production
FROM node:18-alpine AS production
# ã»ãã¥ãªãã£åŒ·å
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
WORKDIR /app
# å¿
èŠãªãã¡ã€ã«ã®ã¿ã³ããŒ
COPY --from=builder --chown=nextjs:nodejs /app/dist ./dist
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nextjs:nodejs /app/package.json ./package.json
# ãã«ã¹ãã§ãã¯çšã®ã¹ã¯ãªãã
COPY --chown=nextjs:nodejs healthcheck.js ./
USER nextjs
EXPOSE 3000
# ãã«ã¹ãã§ãã¯
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node healthcheck.js
CMD ["npm", "start"]
---
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: vibe-coding-app
labels:
app: vibe-coding-app
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
selector:
matchLabels:
app: vibe-coding-app
template:
metadata:
labels:
app: vibe-coding-app
spec:
containers:
- name: app
image: ghcr.io/yourorg/vibe-coding-app:latest
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: "production"
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: database-url
- name: REDIS_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: redis-url
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
imagePullSecrets:
- name: ghcr-secret
---
# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
name: vibe-coding-app-service
spec:
selector:
app: vibe-coding-app
ports:
- protocol: TCP
port: 80
targetPort: 3000
type: ClusterIP
---
# k8s/hpa.yamlïŒæ°Žå¹³ã¹ã±ãŒãªã³ã°ïŒ
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: vibe-coding-app-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: vibe-coding-app
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 10
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Percent
value: 50
periodSeconds: 30
---
# k8s/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: vibe-coding-app-ingress
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/rate-limit: "100"
nginx.ingress.kubernetes.io/rate-limit-burst: "50"
spec:
tls:
- hosts:
- app.vibecoding.salon
- api.vibecoding.salon
secretName: vibecoding-tls
rules:
- host: app.vibecoding.salon
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: vibe-coding-app-service
port:
number: 80
- host: api.vibecoding.salon
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: vibe-coding-api-service
port:
number: 80
ð ããŒã¿ããŒã¹ã¹ã±ãŒãªã³ã°æŠç¥
èªã¿åãå°çšã¬ããªã«ãšãã£ãã·ã¥æŠç¥
// database/connectionManager.js
class DatabaseConnectionManager {
constructor() {
this.connections = {
write: this.createConnection(process.env.DATABASE_WRITE_URL),
read: this.createReadReplicas(),
cache: this.createRedisConnection()
};
this.readReplicaIndex = 0;
}
createConnection(url) {
return new Pool({
connectionString: url,
max: 20, // æå€§æ¥ç¶æ°
min: 5, // æå°æ¥ç¶æ°
acquireTimeoutMillis: 60000,
createTimeoutMillis: 30000,
destroyTimeoutMillis: 5000,
idleTimeoutMillis: 30000,
reapIntervalMillis: 1000,
createRetryIntervalMillis: 200
});
}
createReadReplicas() {
const readUrls = [
process.env.DATABASE_READ_REPLICA_1,
process.env.DATABASE_READ_REPLICA_2,
process.env.DATABASE_READ_REPLICA_3
].filter(Boolean);
return readUrls.map(url => this.createConnection(url));
}
createRedisConnection() {
return new Redis({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
password: process.env.REDIS_PASSWORD,
db: 0,
retryDelayOnFailover: 100,
enableReadyCheck: false,
maxRetriesPerRequest: 3,
lazyConnect: true
});
}
// æžã蟌ã¿çšæ¥ç¶ååŸ
getWriteConnection() {
return this.connections.write;
}
// èªã¿åãçšæ¥ç¶ååŸïŒã©ãŠã³ãããã³ïŒ
getReadConnection() {
if (this.connections.read.length === 0) {
return this.connections.write; // ãã©ãŒã«ããã¯
}
const connection = this.connections.read[this.readReplicaIndex];
this.readReplicaIndex = (this.readReplicaIndex + 1) % this.connections.read.length;
return connection;
}
// ãã£ãã·ã¥æ¥ç¶ååŸ
getCacheConnection() {
return this.connections.cache;
}
}
// repository/todoRepository.js
class TodoRepository {
constructor(dbManager) {
this.dbManager = dbManager;
this.cache = dbManager.getCacheConnection();
}
// æžãèŸŒã¿æäœ
async createTodo(todoData) {
const writeDb = this.dbManager.getWriteConnection();
const query = `
INSERT INTO todos (title, description, user_id, created_at)
VALUES ($1, $2, $3, NOW())
RETURNING *
`;
const result = await writeDb.query(query, [
todoData.title,
todoData.description,
todoData.userId
]);
const todo = result.rows[0];
// ãã£ãã·ã¥ãç¡å¹å
await this.invalidateUserTodosCache(todoData.userId);
return todo;
}
// èªã¿åãæäœïŒãã£ãã·ã¥æŠç¥ïŒ
async getUserTodos(userId, useCache = true) {
const cacheKey = `user_todos:${userId}`;
if (useCache) {
// ãã£ãã·ã¥ããååŸã詊è¡
const cached = await this.cache.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
}
// ããŒã¿ããŒã¹ããååŸ
const readDb = this.dbManager.getReadConnection();
const query = `
SELECT * FROM todos
WHERE user_id = $1
ORDER BY created_at DESC
`;
const result = await readDb.query(query, [userId]);
const todos = result.rows;
// ãã£ãã·ã¥ã«ä¿åïŒ15åéïŒ
if (useCache) {
await this.cache.setex(cacheKey, 900, JSON.stringify(todos));
}
return todos;
}
// æŽæ°æäœ
async updateTodo(todoId, updates) {
const writeDb = this.dbManager.getWriteConnection();
const setClause = Object.keys(updates)
.map((key, index) => `${key} = ${index + 2}`)
.join(', ');
const query = `
UPDATE todos
SET ${setClause}, updated_at = NOW()
WHERE id = $1
RETURNING *
`;
const values = [todoId, ...Object.values(updates)];
const result = await writeDb.query(query, values);
if (result.rows.length > 0) {
const todo = result.rows[0];
// é¢é£ãã£ãã·ã¥ãç¡å¹å
await this.invalidateUserTodosCache(todo.user_id);
await this.cache.del(`todo:${todoId}`);
return todo;
}
return null;
}
// ãã£ãã·ã¥ç¡å¹å
async invalidateUserTodosCache(userId) {
const keys = [
`user_todos:${userId}`,
`user_todos_count:${userId}`,
`user_completed_todos:${userId}`
];
await Promise.all(keys.map(key => this.cache.del(key)));
}
// çµ±èšæ
å ±ååŸïŒéãã¯ãšãªããã£ãã·ã¥ïŒ
async getUserTodoStats(userId) {
const cacheKey = `user_todo_stats:${userId}`;
// é·æãã£ãã·ã¥ïŒ1æéïŒ
const cached = await this.cache.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
const readDb = this.dbManager.getReadConnection();
const query = `
SELECT
COUNT(*) as total,
COUNT(*) FILTER (WHERE completed = true) as completed,
COUNT(*) FILTER (WHERE completed = false) as pending,
AVG(CASE WHEN completed = true
THEN EXTRACT(EPOCH FROM (completed_at - created_at))/3600
ELSE NULL END) as avg_completion_hours
FROM todos
WHERE user_id = $1
`;
const result = await readDb.query(query, [userId]);
const stats = result.rows[0];
// 1æéãã£ãã·ã¥
await this.cache.setex(cacheKey, 3600, JSON.stringify(stats));
return stats;
}
}
export { DatabaseConnectionManager, TodoRepository };
ð ãããã¯ãæé·ïŒA/Bãã¹ããšæ©èœãã©ã°
𧪠A/Bãã¹ãã·ã¹ãã
A/Bãã¹ãå®è£ ã®äŸé ŒïŒ“ãŠãŒã¶ãŒäœéšã®æ¹åãæž¬å®ãããããA/Bãã¹ãã·ã¹ãã ãå®è£ ããŠãã ãããæ©èœãã©ã°ãšçµã¿åãããŠã段éçãªæ©èœããŒã«ã¢ãŠããã§ããããã«ãããã§ãã”
A/Bãã¹ããã¬ãŒã ã¯ãŒã¯
// experiments/abTestManager.js
class ABTestManager {
constructor(userId, experiments = []) {
this.userId = userId;
this.experiments = new Map();
this.userAttributes = {};
this.loadExperiments(experiments);
this.loadUserAttributes();
}
loadExperiments(experiments) {
experiments.forEach(experiment => {
this.experiments.set(experiment.id, experiment);
});
}
async loadUserAttributes() {
try {
const response = await fetch(`/api/users/${this.userId}/attributes`);
this.userAttributes = await response.json();
} catch (error) {
console.error('Failed to load user attributes:', error);
}
}
// ããªã¢ã³ã決å®ããžãã¯
getVariant(experimentId) {
const experiment = this.experiments.get(experimentId);
if (!experiment) {
console.warn(`Experiment ${experimentId} not found`);
return 'control';
}
// å®éšãæå¹ããã§ãã¯
if (!this.isExperimentActive(experiment)) {
return 'control';
}
// ãŠãŒã¶ãŒã察象ããã§ãã¯
if (!this.isUserEligible(experiment)) {
return 'control';
}
// æ¢ã«å²ãåœãŠæžã¿ããã§ãã¯
const cachedVariant = this.getCachedVariant(experimentId);
if (cachedVariant) {
return cachedVariant;
}
// ããªã¢ã³ã決å®
const variant = this.assignVariant(experiment);
// ãã£ãã·ã¥ã«ä¿å
this.cacheVariant(experimentId, variant);
// ãã©ããã³ã°
this.trackExperimentAssignment(experimentId, variant);
return variant;
}
isExperimentActive(experiment) {
const now = new Date();
const startDate = new Date(experiment.startDate);
const endDate = new Date(experiment.endDate);
return now >= startDate && now <= endDate && experiment.enabled;
}
isUserEligible(experiment) {
const { targeting } = experiment;
if (!targeting) return true;
// å°åãã£ã«ã¿ãªã³ã°
if (targeting.countries &&
!targeting.countries.includes(this.userAttributes.country)) {
return false;
}
// ãã©ãããã©ãŒã ãã£ã«ã¿ãªã³ã°
if (targeting.platforms &&
!targeting.platforms.includes(this.userAttributes.platform)) {
return false;
}
// ãŠãŒã¶ãŒå±æ§ãã£ã«ã¿ãªã³ã°
if (targeting.userSegments) {
const userSegment = this.getUserSegment();
if (!targeting.userSegments.includes(userSegment)) {
return false;
}
}
// ã«ã¹ã¿ã æ¡ä»¶
if (targeting.customCondition) {
return this.evaluateCustomCondition(targeting.customCondition);
}
return true;
}
assignVariant(experiment) {
// ãŠãŒã¶ãŒIDããŒã¹ã®äžè²«ããããã·ã¥
const hash = this.hashUserId(this.userId + experiment.id);
const percentage = hash % 100;
let cumulative = 0;
for (const variant of experiment.variants) {
cumulative += variant.allocation;
if (percentage < cumulative) {
return variant.name;
}
}
return 'control';
}
hashUserId(input) {
let hash = 0;
for (let i = 0; i < input.length; i++) {
const char = input.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // 32bitæŽæ°ã«å€æ
}
return Math.abs(hash);
}
getCachedVariant(experimentId) {
const key = `experiment_${experimentId}`;
return localStorage.getItem(key);
}
cacheVariant(experimentId, variant) {
const key = `experiment_${experimentId}`;
localStorage.setItem(key, variant);
}
getUserSegment() {
const { accountAge, activityLevel, subscription } = this.userAttributes;
if (subscription === 'premium') return 'premium';
if (accountAge > 365) return 'veteran';
if (activityLevel === 'high') return 'power_user';
if (accountAge < 7) return 'new_user';
return 'regular';
}
evaluateCustomCondition(condition) {
// ã·ã³ãã«ãªæ¡ä»¶è©äŸ¡ãšã³ãžã³
try {
// ã»ãã¥ãªãã£äžãeval ã¯äœ¿ããã«ããŒãµãŒãå®è£
return this.parseCondition(condition);
} catch (error) {
console.error('Failed to evaluate custom condition:', error);
return false;
}
}
parseCondition(condition) {
// äŸ: "age > 18 AND country = 'US'"
// å®éã®å®è£
ã§ã¯é©åãªããŒãµãŒã䜿çš
const { age, country } = this.userAttributes;
if (condition.includes('age > 18') && age <= 18) return false;
if (condition.includes("country = 'US'") && country !== 'US') return false;
return true;
}
trackExperimentAssignment(experimentId, variant) {
// åæã·ã¹ãã ã«ã€ãã³ãéä¿¡
this.track('experiment_assignment', {
experimentId,
variant,
userId: this.userId,
timestamp: new Date().toISOString()
});
}
// ã€ãã³ããã©ããã³ã°
track(eventName, properties = {}) {
// Google Analytics
if (window.gtag) {
window.gtag('event', eventName, properties);
}
// ã«ã¹ã¿ã åæ
fetch('/api/analytics/track', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
event: eventName,
properties: {
...properties,
userId: this.userId,
sessionId: this.getSessionId(),
timestamp: new Date().toISOString()
}
})
}).catch(error => {
console.error('Failed to track event:', error);
});
}
getSessionId() {
let sessionId = sessionStorage.getItem('sessionId');
if (!sessionId) {
sessionId = 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
sessionStorage.setItem('sessionId', sessionId);
}
return sessionId;
}
}
// æ©èœãã©ã°ãããŒãžã£ãŒ
class FeatureFlagManager {
constructor(userId) {
this.userId = userId;
this.flags = new Map();
this.loadFlags();
}
async loadFlags() {
try {
const response = await fetch(`/api/feature-flags?userId=${this.userId}`);
const flags = await response.json();
flags.forEach(flag => {
this.flags.set(flag.name, flag);
});
} catch (error) {
console.error('Failed to load feature flags:', error);
}
}
isEnabled(flagName, defaultValue = false) {
const flag = this.flags.get(flagName);
if (!flag) {
return defaultValue;
}
// ã°ããŒãã«ç¡å¹å
if (!flag.enabled) {
return false;
}
// ããŒã«ã¢ãŠãçãã§ãã¯
if (flag.rolloutPercentage < 100) {
const hash = this.hashUserId(this.userId + flagName);
const percentage = hash % 100;
if (percentage >= flag.rolloutPercentage) {
return false;
}
}
// ãŠãŒã¶ãŒã»ã°ã¡ã³ããã§ãã¯
if (flag.targetSegments && flag.targetSegments.length > 0) {
const userSegment = this.getUserSegment();
if (!flag.targetSegments.includes(userSegment)) {
return false;
}
}
return true;
}
hashUserId(input) {
let hash = 0;
for (let i = 0; i < input.length; i++) {
const char = input.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash);
}
getUserSegment() {
// ãŠãŒã¶ãŒã»ã°ã¡ã³ãã®å€å®ããžãã¯
// ABTestManager ãšåæ§
return 'regular';
}
}
// å®éšå®çŸ©äŸ
const experiments = [
{
id: 'new_todo_button_color',
name: 'Todo Button Color Test',
enabled: true,
startDate: '2025-06-01',
endDate: '2025-06-30',
variants: [
{ name: 'control', allocation: 50 },
{ name: 'blue', allocation: 25 },
{ name: 'green', allocation: 25 }
],
targeting: {
countries: ['US', 'CA', 'UK'],
platforms: ['web', 'mobile'],
userSegments: ['new_user', 'regular']
}
}
];
// 䜿çšäŸ
const abTest = new ABTestManager(currentUser.id, experiments);
const featureFlags = new FeatureFlagManager(currentUser.id);
// A/Bãã¹ãã®ããªã¢ã³ãååŸ
const buttonColorVariant = abTest.getVariant('new_todo_button_color');
// æ©èœãã©ã°ã®ç¢ºèª
const showNewFeature = featureFlags.isEnabled('new_dashboard_layout');
// UIã§ã®äœ¿çš
function renderTodoButton() {
const variant = abTest.getVariant('new_todo_button_color');
let buttonClass = 'btn-primary'; // ããã©ã«ã
switch (variant) {
case 'blue':
buttonClass = 'btn-blue';
break;
case 'green':
buttonClass = 'btn-green';
break;
}
return ``;
}
export { ABTestManager, FeatureFlagManager };
ð Windsurfå®å šãã¹ã¿ãŒè¬åº§å®çµ
ð Part5ã§ç¿åŸãããããã§ãã·ã§ãã«ã¹ãã«
ð€ ããŒã éçºã¹ãã«ïŒ
- Git Flowãšãã©ã³ãæŠç¥
- 广çãªã³ãŒãã¬ãã¥ãŒ
- ãã«ãªã¯ãšã¹ãã®ãã¹ããã©ã¯ãã£ã¹
- ããŒã éçºã¬ã€ãã©ã€ã³ã®çå®
ð CI/CDã¹ãã«ïŒ
- GitHub Actions ã«ããèªåå
- 倿®µéãã¹ããã€ãã©ã€ã³
- èªåãããã€ã¡ã³ã
- ç°å¢ç®¡çãšã·ãŒã¯ã¬ãã管ç
ð ç£èŠã»åæã¹ãã«ïŒ
- Sentryã«ãããšã©ãŒãã©ããã³ã°
- ã«ã¹ã¿ã ã¡ããªã¯ã¹åé
- ããã©ãŒãã³ã¹ç£èŠ
- ãŠãŒã¶ãŒè¡ååæ
ð ã€ã³ãã©ã¹ãã©ã¯ãã£ã¹ãã«ïŒ
- Docker & Kubernetes
- ããŒã¿ããŒã¹ã¹ã±ãŒãªã³ã°
- ãã£ãã·ã¥æŠç¥
- CDNãšããŒããã©ã³ã·ã³ã°
ð ãããã¯ãæé·ã¹ãã«ïŒ
- A/Bãã¹ããã¬ãŒã ã¯ãŒã¯
- æ©èœãã©ã°ç®¡ç
- 段éçããŒã«ã¢ãŠã
- ããŒã¿ããªãã³ãªæææ±ºå®
â æçµãã§ãã¯ãªã¹ã
ãããã§ãã·ã§ãã«éçºè ãšããŠ
- â¡ Git Flowã䜿ã£ãããŒã éçºãã§ãã
- â¡ CI/CDãã€ãã©ã€ã³ãèšèšã»éçšã§ãã
- â¡ ãããã¯ã·ã§ã³ã¬ãã«ã®ç£èŠã·ã¹ãã ãæ§ç¯ã§ãã
- â¡ ã¹ã±ãŒã©ãã«ãªã¢ãŒããã¯ãã£ãèšèšã§ãã
- â¡ A/Bãã¹ãã§ãããã¯ããæ¹åã§ãã
- â¡ ããŒã ããªãŒãããæè¡çæææ±ºå®ãã§ãã
- â¡ ããžãã¹èŠä»¶ãæè¡å®è£ ã«èœãšã蟌ãã
- â¡ åŸèŒ©ã«ã¡ã³ã¿ãªã³ã°ãã§ãã
ð¡ å²è¶ããã®æçµã¡ãã»ãŒãž
ðž ãããã§ãã·ã§ãã«ããããããŒãšããŠã®æ ç«ã¡
Part1ãã Part5ãŸã§ãæ¬åœã«ãç²ãããŸã§ãããð
çããã¯ä»ãåãªãVibe CodingãŠãŒã¶ãŒã§ã¯ãããŸããããããã§ãã·ã§ãã«ãªéçºè ãšããŠãã©ããªãããžã§ã¯ãã«ã察å¿ã§ããå®åã身ã«ã€ããŸããã
ããããã®çãããž
- ð èªä¿¡ãæã£ãŠææŠïŒã©ããªå€§ããªãããžã§ã¯ãããä»ã®çãããªãä¹ãè¶ããããŸã
- ð ç¶ç¶çãªåŠç¿ïŒæè¡ã¯æ¥ã é²åããŸããåŠã³ç¶ããå§¿å¢ã倧åã«
- ð€ ã³ãã¥ããã£è²¢ç®ïŒä»åºŠã¯çãããåŸèŒ©ãå°ãçªã§ã
- ð¡ 䟡å€åµé ïŒæè¡ã§ãŠãŒã¶ãŒã«äŸ¡å€ãå±ãã瀟äŒãããè¯ãããŠãããŸããã
Windsurfãšå ±ã«æ©ãæªæ¥
Windsurfã¯çããã®æåŒ·ã®ããŒãããŒã§ãããããããæ°ããæ©èœã远å ããããã䟿å©ã«ãªã£ãŠãããŸãã
çãããWindsurfãšå ±ã«ãçŽ æŽããããããã¯ããçã¿åºãç¶ããããšããå¿ããæ¥œãã¿ã«ããŠããŸãã
ð¯ æ¬¡ã®ã¹ããã
çæç®æšïŒ1-3ã¶æïŒ
- åŠãã ã¹ãã«ãå®éã®ãããžã§ã¯ãã§å®è·µ
- GitHubã§ãªãŒãã³ãœãŒã¹ãããžã§ã¯ãã«è²¢ç®
- æè¡ããã°ã§åŠç¿å 容ãã¢ãŠãããã
äžæç®æšïŒ6ã¶æ-1幎ïŒ
- ããŒã ã§ã®ããã¯ãªãŒãçµéš
- å匷äŒãã«ã³ãã¡ã¬ã³ã¹ã§ã®ç»å£
- æ°ããæè¡ã¹ã¿ãã¯ãžã®ææŠ
é·æç®æšïŒ1幎以äžïŒ
- ãããã¯ãã®æè¡è²¬ä»»è
- ã¹ã¿ãŒãã¢ããã®åµæ¥ã»CTO
- æè¡ã³ãã¥ããã£ã®ãªãŒããŒ
ð å®å šãã¹ã¿ãŒéæããã§ãšãããããŸãïŒ
Windsurfå®å šãã¹ã¿ãŒè¬åº§Part1ãPart5ãéããŠãçããã¯ïŒ
- åºç€ããå¿çšãŸã§ïŒWindsurfã®å šæ©èœãç¿åŸ
- å人éçºããããŒã éçºãŸã§ïŒããããéçºã¹ã¿ã€ã«ã«å¯Ÿå¿
- åŠç¿ããéçšãŸã§ïŒãããã¯ãã©ã€ããµã€ã¯ã«å šäœãã«ããŒ
ãã®ç¥èãšã¹ãã«ãæã£ãçãããªããã©ããªæè¡ç課é¡ãä¹ãè¶ããããŸãã
ããã§ã¯ãæ°ããVibe Codingãšãã¹ããŒããšããŠãçŽ æŽãããæªæ¥ãç¯ããŠãã£ãŠãã ããããâš
ð ç¡æã¡ã«ãã¬ã§ææ°æè¡ããã£ããã¢ããïŒ
ææ°ã®æè¡ãã¬ã³ããæ¥çååãé«åºŠãªéçºãã¯ããã¯ããã€ã±ãã€ããããã¡æ©ããå±ãããŸãããã£ãªã¢æŠç¥ã«ã€ããŠã宿é ä¿¡äžã§ãããïŒ
ðž Vibe Codingãµãã³ã§ãšãã¹ããŒãå士ã®äº€æµã
Vibe Codingã«ã€ããŠãã¿ããªã§ã¯ã€ã¯ã€è°è«ããŠããŸãïŒ
Windsurfå®å šãã¹ã¿ãŒè¬åº§ã¯ä»åã§å®çµã§ãããçããã®åŠç¿ãšæé·ã®æ ã¯ç¶ããŠãããŸãããããããæ°ããæè¡ãšå ±ã«ãçŽ æŽããããããã¯ããäœãç¶ããŠãã ãããã
æåŸãŸã§èªãã§ãã ãã£ãŠãæ¬åœã«ããããšãããããŸããð
Happy Professional Vibe Coding! ð€ðâš


ã³ã¡ã³ã