Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,32 @@ on:
- "v*"

jobs:
updates:
runs-on: ubuntu-latest
name: Deploy updates (${{ matrix.environment }})
strategy:
fail-fast: false
matrix:
environment: [dev, beta, prod]
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Apply update schema
run: pnpm run updates:migrate:${{ matrix.environment }}
env:
CI: "true"
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
- name: Deploy update service
run: pnpm run updates:deploy:${{ matrix.environment }}
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
deploy:
runs-on: ubuntu-latest
name: Deploy
Expand Down
16 changes: 15 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,27 @@ details, primarily the **environments**.

## Environments

There are two deployed Workers, defined in `wrangler.toml`:
There are two deployed API Workers, defined in `wrangler.toml`:

| Env | Worker name | URL | Deploy command |
| ---- | -------------- | -------------------------------------------- | --------------------- |
| prod | `ww-proxy` | `https://ww-proxy.leviwilkerson.com` | `pnpm run deploy` |
| dev | `ww-proxy-dev` | `https://ww-proxy-dev.<subdomain>.workers.dev` | `pnpm run deploy:dev` |

Three app-update Workers (Hot Updater) are defined separately in
`wrangler-updates.json`, one per app channel, each with its own D1 database and
private R2 bucket of the same name:

| Env | Worker / D1 / R2 | URL | Deploy command |
| ---- | ----------------- | ---------------------------------------------------- | ------------------------------------------------------- |
| dev | `ww-updates-dev` | `https://ww-updates-dev.levi-wilkerson.workers.dev` | `pnpm updates:migrate:dev && pnpm updates:deploy:dev` |
| beta | `ww-updates-beta` | `https://ww-updates-beta.levi-wilkerson.workers.dev` | `pnpm updates:migrate:beta && pnpm updates:deploy:beta` |
| prod | `ww-updates-prod` | `https://ww-updates-prod.levi-wilkerson.workers.dev` | `pnpm updates:migrate:prod && pnpm updates:deploy:prod` |

They share no bindings with ww-proxy, and plain `wrangler deploy` never touches
them. The tag deploy migrates and deploys all three alongside ww-proxy. Setup,
credentials and verification are in [`docs/ota-updates.md`](./docs/ota-updates.md).

Prefer the `pnpm run deploy*` scripts over raw `wrangler deploy` — they also
inject the `SENTRY_RELEASE` var and upload source maps to Sentry (see
[Sentry source maps](#sentry-source-maps) below). CI (`.github/workflows/deploy.yml`)
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -405,3 +405,9 @@ Response to Client
- **Package Manager**: pnpm
- **Monitoring**: Sentry
- **Wrangler**: v4.42.2

## App OTA updates

Hot Updater runs in separate dev, beta and prod Workers, each with its own D1
catalog and private R2 bucket. See [the OTA runbook](docs/ota-updates.md) for
resource identities, migration/deployment commands and credential setup.
82 changes: 82 additions & 0 deletions docs/ota-updates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Hot Updater

The update services are independent of the ww-proxy API Worker. Each has its
own private R2 bucket, D1 database, client credential and app signing key:

| Environment | Worker / bucket / database | App channel |
| --- | --- | --- |
| Development | `ww-updates-dev` | `dev` |
| Beta | `ww-updates-beta` | `beta` |
| Production | `ww-updates-prod` | `production` |

Their URLs are `https://<worker>.levi-wilkerson.workers.dev`. The database IDs
and resource bindings are in `wrangler-updates.json`. Plain `wrangler deploy`
still deploys only ww-proxy; update commands always select their own config and
an explicit environment.

## Deploy

From this repository, for each of `dev`, `beta` and `prod`:

```sh
pnpm updates:migrate:dev
pnpm updates:deploy:dev
```

Apply migrations before deploying the runtime. `0001_hot-updater_1.0.0.sql` is
the unmodified Cloudflare schema supplied by Hot Updater 1.0.2, including API
keys, Insights and Remote Config. Only `handlers.client` is mounted; the admin
protocol and console are not exposed publicly. CLI writes use Cloudflare D1
and R2 credentials in the app repository.

The tag deployment workflow also migrates and deploys these Workers, retaining
their existing secrets. This changes update infrastructure, not installed app
code; publishing an OTA is a separate operation in the app repository.

## Credentials

Each Worker needs `R2_ACCESS_KEY_ID` and `R2_SECRET_ACCESS_KEY` secrets to sign
download URLs. Use an R2 Object Read & Write token scoped to that Worker's
bucket. Save it in the 1Password Agents vault and upload the two values with
`wrangler secret put --config wrangler-updates.json --env <environment>` via
stdin. Do not commit credentials or put them in command arguments or logs.

The app's CLI also needs a D1 Edit API token for this account. A Wrangler OAuth
session can migrate D1 and register client keys, but cannot mint API tokens or
supply persistent R2 S3 credentials.

Register client keys through the `apiKeys()` plugin's provisioning API or
`hot-updater api-key create` in a matching server/config context. Only the
SHA-256 digest is stored in D1. A client key can read updates and submit
operational reports; it cannot deploy or administer updates. Each environment
has a separate client key. Rotation must ship a new client credential before
revoking the old one.

## Verify

Use the official CLI's scoped scaffold and infrastructure doctor checks against
the version-matched generated scaffold. For both iOS and Android, the exact
catalog route must reject an absent/wrong key with 401 and accept the matching
key with 200, or 404 with `x-hot-updater-catalog: none` for an empty catalog.
Keys from another environment must fail. `/version` alone does not prove OTA.

Separately verify D1 migrations, bound bucket identities, a bounded R2 S3 list
with the CLI's credentials, signed artifact downloads, and a native Release
app's application/rollback and Insights reports. See WitnessWork's
`docs/ota-updates.md`. Keep all generated scaffolds, credentials and evidence
under ignored `.verify/` folders.

## Rollout

Before enabling an environment, create its resources, apply the schema,
register its distinct client key and install its bucket-scoped R2 secrets.
Save credentials in Agents as `WitnessWork OTA R2 (<channel>)`; the app's
publisher also needs a persistent account-scoped D1 Edit token. Confirm that
the CI account token can migrate D1 and deploy each configured Worker.

Use development and Beta Release apps on both platforms to verify signed
artifact downloads, activation, disabled-release rollback, invalid-signature
rejection and Insights reports. Record the current setup and proof in the
paired PRs or release record. Preparing production infrastructure does not
release an app bundle; production app rollout follows its separate typed
release gate in WitnessWork.
Loading
Loading