# Staging y Production Deployments

## Flujo de Promoción

```
Tareas Aprobadas
      ↓
   Merge a Staging
      ↓
  Deploy Staging
      ↓
  Tests Automáticos
      ↓
  Aprobación Manual
      ↓
Merge a Production
      ↓
Deploy Production
```

## Merge a Staging

### 1. Agrupar Tareas

```typescript
// api/routes/task-groups.ts
router.post('/task-groups', async (req, res) => {
  const { projectId, taskIds, notes } = req.body

  // Validate all tasks are approved
  const tasks = await db.query.tasks.findMany({
    where: inArray(tasks.id, taskIds),
  })

  const notApproved = tasks.filter((t) => t.state !== 'approved')

  if (notApproved.length > 0) {
    return res.status(400).json({
      error: 'All tasks must be approved',
      notApproved: notApproved.map((t) => t.id),
    })
  }

  // Create task group
  const groupId = crypto.randomUUID()

  await db.insert(taskGroups).values({
    id: groupId,
    projectId,
    taskIds: JSON.stringify(taskIds),
    status: 'pending',
    notes,
    createdBy: req.user?.id,
  })

  // Enqueue merge job
  await enqueueMerge({
    taskGroupId: groupId,
    projectId,
    taskIds,
    targetBranch: 'staging',
  })

  res.status(201).json({
    taskGroupId: groupId,
    status: 'pending',
  })
})
```

### 2. Merge Worker

```typescript
// services/queue/merge-worker.ts
export const mergeWorker = new Worker('merges', async (job) => {
  const { taskGroupId, projectId, taskIds, targetBranch } = job.data

  logger.info(`Merging tasks to ${targetBranch}:`, taskIds)

  const project = await db.query.projects.findFirst({
    where: eq(projects.id, projectId),
  })

  const tasks = await db.query.tasks.findMany({
    where: inArray(tasks.id, taskIds),
  })

  job.updateProgress(10)

  // 1. Clone repo
  const repoDir = `/tmp/merge-${taskGroupId}`
  await exec(`git clone ${project.giteaRepoUrl} ${repoDir}`)
  process.chdir(repoDir)

  // 2. Checkout target branch
  await exec(`git checkout ${targetBranch}`)

  job.updateProgress(20)

  // 3. Merge each task's branch
  for (const task of tasks) {
    if (!task.branchName) {
      logger.warn(`Task ${task.id} has no branch, skipping`)
      continue
    }

    try {
      await exec(`git fetch origin ${task.branchName}`)
      await exec(`git merge origin/${task.branchName} --no-ff -m "Merge task: ${task.title}"`)

      logger.info(`Merged ${task.branchName}`)

      job.updateProgress(20 + (40 / tasks.length))
    } catch (error) {
      logger.error(`Failed to merge ${task.branchName}:`, error)

      // Create conflict resolution task
      await db.update(tasks)
        .set({ state: 'needs_input' })
        .where(eq(tasks.id, task.id))

      throw new Error(`Merge conflict in ${task.branchName}`)
    }
  }

  job.updateProgress(60)

  // 4. Push to staging
  await exec(`git push origin ${targetBranch}`)

  job.updateProgress(70)

  // 5. Create staging PR (if using main as production)
  if (targetBranch === 'staging') {
    const pr = await giteaClient.createPullRequest(
      project.giteaOwner,
      project.giteaRepoName,
      {
        title: `Deploy to Production - ${new Date().toISOString().split('T')[0]}`,
        body: generateStagingPRDescription(tasks),
        head: 'staging',
        base: 'main',
      }
    )

    await db.update(taskGroups)
      .set({
        stagingBranch: 'staging',
        stagingPrNumber: pr.number,
        stagingPrUrl: pr.html_url,
      })
      .where(eq(taskGroups.id, taskGroupId))
  }

  job.updateProgress(80)

  // 6. Update tasks
  for (const task of tasks) {
    await db.update(tasks)
      .set({
        state: 'staging',
        deployedStagingAt: new Date(),
      })
      .where(eq(tasks.id, task.id))
  }

  // 7. Update task group
  await db.update(taskGroups)
    .set({ status: 'staging' })
    .where(eq(taskGroups.id, taskGroupId))

  job.updateProgress(90)

  // 8. Trigger staging deployment
  await enqueueDeploy({
    deploymentId: crypto.randomUUID(),
    projectId,
    environment: 'staging',
    branch: 'staging',
    commitHash: await getLatestCommit(repoDir, 'staging'),
  })

  job.updateProgress(100)

  logger.info(`Merge completed: ${taskGroupId}`)

  return { success: true }
})

function generateStagingPRDescription(tasks: Task[]) {
  return `
## Tasks Included

${tasks.map((t) => `- [x] ${t.title} (#${t.id.slice(0, 8)})`).join('\n')}

## Changes

${tasks.map((t) => `### ${t.title}\n${t.description}\n`).join('\n')}

## Testing Checklist

${tasks.map((t) => `- [ ] Test: ${t.title}`).join('\n')}

---

🤖 Generated by AiWorker
  `.trim()
}
```

## Staging Deployment

```yaml
# projects/my-app/staging/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

namespace: my-app-staging

bases:
- ../base

images:
- name: aiworker/my-app
  newTag: staging-abc123

replicas:
- name: my-app
  count: 2

configMapGenerator:
- name: app-config
  literals:
  - NODE_ENV=staging
  - LOG_LEVEL=debug
  - SENTRY_ENVIRONMENT=staging

patchesStrategicMerge:
- patches.yaml
---
# projects/my-app/staging/patches.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  template:
    spec:
      containers:
      - name: app
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: staging-db-credentials
              key: url
```

## Automated Tests en Staging

```typescript
// services/testing/staging-tests.ts
export async function runStagingTests(params: {
  projectId: string
  stagingUrl: string
}) {
  const { projectId, stagingUrl } = params

  logger.info(`Running staging tests for: ${stagingUrl}`)

  const tests = [
    testHealthEndpoint,
    testAuthentication,
    testCriticalFeatures,
    testPerformance,
  ]

  const results = []

  for (const test of tests) {
    try {
      const result = await test(stagingUrl)
      results.push({ test: test.name, passed: result.passed, details: result })

      if (!result.passed) {
        logger.error(`Test failed: ${test.name}`, result)
      }
    } catch (error) {
      results.push({ test: test.name, passed: false, error: error.message })
    }
  }

  const allPassed = results.every((r) => r.passed)

  // Store results
  await db.insert(testRuns).values({
    id: crypto.randomUUID(),
    projectId,
    environment: 'staging',
    results: JSON.stringify(results),
    passed: allPassed,
    runAt: new Date(),
  })

  return { allPassed, results }
}

async function testHealthEndpoint(baseUrl: string) {
  const response = await fetch(`${baseUrl}/health`)
  return {
    passed: response.ok,
    status: response.status,
  }
}

async function testAuthentication(baseUrl: string) {
  // Test login
  const loginResponse = await fetch(`${baseUrl}/api/auth/login`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      email: 'test@example.com',
      password: 'test123',
    }),
  })

  return {
    passed: loginResponse.ok,
    hasToken: !!(await loginResponse.json()).token,
  }
}
```

## Production Deployment

### 1. Aprobación Manual

```typescript
// api/routes/task-groups.ts
router.post('/task-groups/:id/approve-production', async (req, res) => {
  const { id } = req.params

  const taskGroup = await db.query.taskGroups.findFirst({
    where: eq(taskGroups.id, id),
  })

  if (!taskGroup || taskGroup.status !== 'staging') {
    return res.status(400).json({ error: 'Task group not ready for production' })
  }

  // Run final checks
  const stagingTests = await getLatestTestResults(taskGroup.projectId, 'staging')

  if (!stagingTests?.passed) {
    return res.status(400).json({ error: 'Staging tests not passing' })
  }

  // Merge staging to main
  await enqueueMerge({
    taskGroupId: id,
    projectId: taskGroup.projectId,
    taskIds: JSON.parse(taskGroup.taskIds),
    targetBranch: 'main',
  })

  // Update status
  await db.update(taskGroups)
    .set({ status: 'production' })
    .where(eq(taskGroups.id, id))

  res.json({ success: true, status: 'deploying' })
})
```

### 2. Production Deployment con Blue-Green

```typescript
// services/deployment/blue-green.ts
export async function blueGreenDeploy(params: {
  projectId: string
  namespace: string
  newVersion: string
}) {
  const { projectId, namespace, newVersion } = params

  const project = await db.query.projects.findFirst({
    where: eq(projects.id, projectId),
  })

  logger.info(`Blue-green deployment: ${project.name} → ${newVersion}`)

  // 1. Deploy "green" (new version) alongside "blue" (current)
  await k8sClient.createDeployment({
    namespace,
    name: `${project.name}-green`,
    image: `${project.dockerImage}:${newVersion}`,
    replicas: project.replicas,
    envVars: project.envVars,
    labels: {
      app: project.name,
      version: 'green',
    },
  })

  // 2. Wait for green to be ready
  await k8sClient.waitForDeployment(namespace, `${project.name}-green`, 300)

  // 3. Run smoke tests on green
  const greenUrl = await k8sClient.getServiceUrl(namespace, `${project.name}-green`)
  const smokeTests = await runSmokeTests(greenUrl)

  if (!smokeTests.passed) {
    logger.error('Smoke tests failed on green deployment')
    throw new Error('Smoke tests failed')
  }

  // 4. Switch service to point to green
  await k8sClient.updateServiceSelector(namespace, project.name, {
    app: project.name,
    version: 'green',
  })

  logger.info('Traffic switched to green')

  // 5. Wait 5 minutes for monitoring
  await sleep(300000)

  // 6. Check error rates
  const errorRate = await getErrorRate(project.name, 5)

  if (errorRate > 0.01) {
    // >1% errors
    logger.error('High error rate detected, rolling back')

    // Rollback: switch service back to blue
    await k8sClient.updateServiceSelector(namespace, project.name, {
      app: project.name,
      version: 'blue',
    })

    throw new Error('Rollback due to high error rate')
  }

  // 7. Delete blue (old version)
  await k8sClient.deleteDeployment(namespace, `${project.name}-blue`)

  // 8. Rename green to blue for next deployment
  await k8sClient.patchDeployment(namespace, `${project.name}-green`, {
    metadata: {
      name: `${project.name}-blue`,
      labels: { version: 'blue' },
    },
  })

  logger.info('Blue-green deployment completed successfully')

  return { success: true }
}
```

### 3. Production Deployment con Canary

```yaml
# Using Argo Rollouts
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: my-app
  namespace: my-app-production
spec:
  replicas: 10
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: app
        image: aiworker/my-app:v1.2.3
        ports:
        - containerPort: 3000

  strategy:
    canary:
      steps:
      # 10% of traffic
      - setWeight: 10
      - pause: {duration: 5m}

      # Check metrics
      - analysis:
          templates:
          - templateName: error-rate
          args:
          - name: service-name
            value: my-app

      # 50% of traffic
      - setWeight: 50
      - pause: {duration: 10m}

      # Full rollout
      - setWeight: 100
---
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: error-rate
spec:
  args:
  - name: service-name
  metrics:
  - name: error-rate
    interval: 1m
    successCondition: result < 0.01  # <1% errors
    provider:
      prometheus:
        address: http://prometheus:9090
        query: |
          rate(http_requests_total{service="{{args.service-name}}",status=~"5.."}[5m])
          /
          rate(http_requests_total{service="{{args.service-name}}"}[5m])
```

## Rollback

```typescript
// api/routes/deployments.ts
router.post('/deployments/:id/rollback', async (req, res) => {
  const { id } = req.params

  const deployment = await db.query.deployments.findFirst({
    where: eq(deployments.id, id),
  })

  if (!deployment || deployment.environment !== 'production') {
    return res.status(400).json({ error: 'Can only rollback production' })
  }

  // Find previous successful deployment
  const previous = await db.query.deployments.findFirst({
    where: and(
      eq(deployments.projectId, deployment.projectId),
      eq(deployments.environment, 'production'),
      eq(deployments.status, 'completed'),
      lt(deployments.createdAt, deployment.createdAt)
    ),
    orderBy: [desc(deployments.createdAt)],
  })

  if (!previous) {
    return res.status(400).json({ error: 'No previous deployment found' })
  }

  logger.warn(`Rolling back to ${previous.commitHash}`)

  // Create rollback deployment
  const rollbackId = crypto.randomUUID()

  await db.insert(deployments).values({
    id: rollbackId,
    projectId: deployment.projectId,
    environment: 'production',
    deploymentType: 'rollback',
    branch: previous.branch,
    commitHash: previous.commitHash,
    status: 'pending',
    triggeredBy: req.user?.id,
  })

  // Enqueue immediate deployment
  await enqueueDeploy({
    deploymentId: rollbackId,
    projectId: deployment.projectId,
    environment: 'production',
    branch: previous.branch,
    commitHash: previous.commitHash,
  }, {
    priority: 1, // Highest priority
  })

  res.json({
    rollbackId,
    rollingBackTo: previous.commitHash,
  })
})
```

## Monitoring Production

```typescript
// services/monitoring/production-monitor.ts
export async function monitorProduction() {
  const projects = await db.query.projects.findMany()

  for (const project of projects) {
    const metrics = await getProductionMetrics(project.name)

    // Check error rate
    if (metrics.errorRate > 0.05) {
      // >5%
      await alertTeam({
        severity: 'critical',
        message: `High error rate in ${project.name}: ${metrics.errorRate * 100}%`,
      })
    }

    // Check response time
    if (metrics.p95ResponseTime > 1000) {
      // >1s
      await alertTeam({
        severity: 'warning',
        message: `Slow response time in ${project.name}: ${metrics.p95ResponseTime}ms`,
      })
    }

    // Check pod health
    const pods = await k8sClient.listPods(`${project.k8sNamespace}-prod`)
    const unhealthy = pods.filter((p) => p.status.phase !== 'Running')

    if (unhealthy.length > 0) {
      await alertTeam({
        severity: 'warning',
        message: `Unhealthy pods in ${project.name}: ${unhealthy.length}`,
      })
    }
  }
}

// Run every minute
setInterval(monitorProduction, 60000)
```

## Best Practices

1. **Always test in staging first**
2. **Automated tests must pass before production**
3. **Use blue-green or canary for production**
4. **Monitor error rates closely after deployment**
5. **Have rollback plan ready**
6. **Deploy during low-traffic hours**
7. **Notify team before production deployment**
8. **Keep previous version running for quick rollback**

## Deployment Checklist

- [ ] All tasks tested in preview
- [ ] All tasks approved
- [ ] Merged to staging
- [ ] Staging tests passing
- [ ] Database migrations run (if any)
- [ ] Team notified
- [ ] Monitoring dashboards ready
- [ ] Rollback plan documented
- [ ] Deploy to production
- [ ] Monitor for 30 minutes
- [ ] Confirm success or rollback
