# ContractBot Deployment Pipeline

## Flow

```text
dev branch -> GitHub Actions -> staging Worker + staging Pages -> smoke tests
main branch -> staging health gate -> production approval -> production Worker + Pages -> release gates
```

Deploy is not the same as publish. Production should only be promoted after staging is healthy.

## Required GitHub Secrets

- `CLOUDFLARE_API_TOKEN`
- `CLOUDFLARE_ACCOUNT_ID`

Optional but recommended:

- Stripe API secret for checkout and billing operations
- Stripe webhook signing secret for payment event verification
- Admin deploy-control secret (enables deploy-control evaluation after production deploy)

## Optional GitHub Variables

- `STAGING_APP_URL`
- `STAGING_API_URL`
- `STAGING_PAGES_PROJECT`
- `PRODUCTION_APP_URL`
- `PRODUCTION_API_URL`
- `PRODUCTION_PAGES_PROJECT`

Defaults are already set in `.github/workflows/deploy.yml`.

## GitHub Environments

Create these environments in GitHub:

- `staging`
- `production`

For `production`, enable required reviewers. This gives manual approval before production deploy.

## GitHub CLI Secrets

Install and authenticate GitHub CLI:

```bash
brew install gh
gh auth login
```

If the current folder is not detected as the repository, bind the target repo explicitly:

```bash
gh repo set-default OWNER/REPO
gh repo view
```

Create environment secrets without putting token values in shell history:

```bash
read -s CF_STAGING_TOKEN
printf "%s" "$CF_STAGING_TOKEN" | gh secret set CLOUDFLARE_API_TOKEN --env staging
gh secret list --env staging | grep CLOUDFLARE_API_TOKEN

read -s CF_PRODUCTION_TOKEN
printf "%s" "$CF_PRODUCTION_TOKEN" | gh secret set CLOUDFLARE_API_TOKEN --env production
gh secret list --env production | grep CLOUDFLARE_API_TOKEN

read -s CF_ACCOUNT_ID
printf "%s" "$CF_ACCOUNT_ID" | gh secret set CLOUDFLARE_ACCOUNT_ID --env staging
printf "%s" "$CF_ACCOUNT_ID" | gh secret set CLOUDFLARE_ACCOUNT_ID --env production
gh secret list --env staging | grep CLOUDFLARE_ACCOUNT_ID
gh secret list --env production | grep CLOUDFLARE_ACCOUNT_ID

unset CF_STAGING_TOKEN CF_PRODUCTION_TOKEN CF_ACCOUNT_ID
history -d $((HISTCMD-1)) 2>/dev/null || true
```

## Local Commands

Build Pages output:

```bash
npm run build:pages
```

Deploy production locally:

```bash
npm run deploy:production
```

Deploy staging locally:

```bash
npm run deploy:staging
```

For local deploys, create `.env.local` in the repo root. It is ignored by git and loaded automatically by `deploy.sh` and `deploy-pages.sh`:

```bash
CLOUDFLARE_API_TOKEN=cf_real_token_here
CLOUDFLARE_ACCOUNT_ID=your_account_id
ENV=production
```

Local deploys fail fast when either Cloudflare value is missing. The deploy script also prints the target environment and stamps `APP_VERSION` into `workers/router/wrangler.toml` before Worker deploy. If the checkout has no git metadata, the version falls back to `local`.

Validate local deploy wiring without publishing:

```bash
DRY_RUN=1 npm run deploy:production
DRY_RUN=1 npm run deploy:staging
```

## Release Gate

Production deploy runs:

```bash
npm run check:release
```

This checks:

- public documentation content,
- protected paths,
- secret leakage,
- entitlement protection.

Production also waits for a short metrics window and validates:

- `/api/system-status` still returns the deployed git version,
- error rate is below `3%`,
- p95 latency is below `1200ms` when metrics exist.
- the scheduled AI Deploy Brain continues promotion or rollback after CI sets the initial canary.

## AI Deploy Brain

The Worker cron runs every minute in production. It reads recent request metrics, checks the active deploy config, and adjusts canary traffic automatically:

- rollback by `STEP_DOWN` when `ERROR_BUDGET` or `P95_BUDGET_MS` is exceeded,
- promote by `STEP_UP` when metrics are comfortably inside budget,
- do nothing during cooldown, low sample windows, or manual override,
- when canary reaches `100%`, make it the stable version and reset canary traffic to `0`.

Decision records are stored as `d:*` keys in the metrics KV for debugging and future dashboard views. Request metrics are stored as short-lived `m:*` keys.

## Deploy Control API

Admin-only endpoints:

```text
GET  /api/deploy/control
POST /api/deploy/control
POST /api/deploy/evaluate
POST /api/admin/override
```

They expose:

- current runtime identity (`env`, `version`),
- canary config (`stable`, `canary`, `percent`),
- recent request metrics,
- recent AI Deploy Brain decisions,
- an evaluation decision that can promote or roll back canary based on the same logic as cron,
- actions: `set_canary`, `promote`, `rollback`, `kill`.

If dedicated KV bindings are not configured, deploy control falls back to `EVENTS`.

CLI examples:

```bash
API_BASE_URL=https://contractbot.eu ADMIN_DEPLOY_KEY="<key>" npm run deploy-control -- status
API_BASE_URL=https://contractbot.eu ADMIN_DEPLOY_KEY="<key>" VERSION="$(git rev-parse --short HEAD)" npm run deploy-control -- set-canary 10
API_BASE_URL=https://contractbot.eu ADMIN_DEPLOY_KEY="<key>" npm run deploy-control -- evaluate
API_BASE_URL=https://contractbot.eu ADMIN_DEPLOY_KEY="<key>" npm run deploy-control -- override 0
API_BASE_URL=https://contractbot.eu ADMIN_DEPLOY_KEY="<key>" npm run deploy-control -- promote
API_BASE_URL=https://contractbot.eu ADMIN_DEPLOY_KEY="<key>" npm run deploy-control -- rollback
API_BASE_URL=https://contractbot.eu ADMIN_DEPLOY_KEY="<key>" npm run deploy-control -- kill
```

`override` sets `manual_override=true`, which makes the scheduled Brain silent until `set-canary` or another control action clears it. `kill` is the emergency switch: it sets canary traffic to `0` and marks `force_stable=true`.

## Rollback

List Worker deployments:

```bash
cd workers/router
wrangler deployments list
```

Rollback production Worker:

```bash
npm run rollback:worker -- <deployment-id>
```

Rollback staging Worker:

```bash
WORKER_ENV=staging npm run rollback:worker -- <deployment-id>
```

Rollback Pages:

```text
Cloudflare Dashboard -> Workers & Pages -> contractbot -> Deployments -> Promote previous deployment
```

Optional shell alias:

```bash
alias cb-rollback="npm run rollback:worker --"
```
