# Repository workflow

Your repository is the source of truth. `waitcher.json` declares one project, its environments and its monitors; test files hold the scripts; `waitcher deploy` makes one environment match the file.

```sh
waitcher config validate                          # offline, every environment
waitcher deploy plan --env production --verify    # local test runs, then server checks
waitcher deploy apply --env production --based-on BASED_ON --smoke
```

## Write tests

API and browser monitors are ordinary `e2e` tests in `*.e2e.ts` files. The project next to `waitcher.json` needs the exact framework versions the server runs:

```sh
waitcher runtime install          # lists the steps, changes nothing
waitcher runtime install --yes    # pins package.json, runs npm install, updates .gitignore
```

It installs Chromium when the file has browser entries (`--browser` forces it). `npm install` runs package install scripts, so read the steps before `--yes`. It is safe to repeat; rerun it after a CLI update to upgrade the project.

A browser test opens paths relative to the environment's `baseUrl`, so one test serves every environment:

```ts
import { test } from "@e2e-dev/web";
import { expect } from "e2e";

test("visitor can start a game", async ({ app, browser, screen }) => {
  await app.open("/games/klondike/");
  await expect(browser.locator("main h1")).toContainText("Solitaire");
  await expect(screen.getByRole("button", "New game")).toBeVisible();
  await app.screenshot("game");
});
```

`screen` finds elements the way a visitor does (`getByRole`, `getByLabel`, `getByText`, `getByPlaceholder`, `getByTestId`); `browser` navigates and offers CSS locators. API tests import `test` and `expect` from `e2e` and read the base URL from `process.env.TARGET_URL`.

Tests may import project files, JSON, Node builtins, `e2e` and `@e2e-dev/web`. Other packages fail the build (`BUILD_FAILED`); use `fetch` or project code instead. A built script must fit 50,000 characters.

## Describe monitors in waitcher.json

```json
{
  "schemaVersion": 1,
  "project": "Shop",
  "environments": {
    "production": { "baseUrl": "https://shop.example" },
    "staging": { "baseUrl": "https://staging.shop.example" }
  },
  "defaults": { "interval": 1800, "timeout": 15 },
  "monitors": [
    { "key": "home", "type": "http", "path": "/", "settings": { "contains": "Shop" } },
    { "key": "tls", "type": "certificate" },
    { "key": "checkout", "type": "browser", "entry": "monitoring/checkout.e2e.ts", "timeout": 45 },
    { "key": "orders-api", "type": "api", "entry": "monitoring/orders.e2e.ts",
      "settings": { "envNames": ["CHECK_ORDERS_TOKEN"] } },
    { "key": "nightly-export", "type": "heartbeat", "environments": ["production"],
      "heartbeat": { "graceSeconds": 300, "completionSeconds": 900 } }
  ]
}
```

- `key` identifies the monitor: 1 to 63 lowercase letters, digits or hyphens. Changing a key replaces the monitor and loses its history; `name` defaults to the key and can change freely.
- `path` resolves against the environment's `baseUrl`. Script and certificate targets default to `baseUrl`; heartbeats use the key as their label.
- `environments` limits an entry to some environments (default: all).
- HTTP `settings`: `method`, `expectedStatus`, `contains`, `jsonPath` with `jsonValue`, `maxLatency` (ms). Certificates: `certificateMinDays`.
- Never put `enabled`, `script`, `scriptFile`, `schemaVersion`, `context` or `demo` in an entry. Visual and AI monitors cannot be deployed from the file.
- `waitcher config schema` prints the JSON Schema offline.
- Limits: 256 KiB, 100 monitors, 10 environments.

## Validate

```sh
waitcher config validate [--env NAME]
```

Offline: resolves every environment and builds every entry. Each issue has a `code`, a `path` such as `monitors[2].entry` and a hint. Exit 2 means something to fix.

## Plan

```sh
waitcher deploy plan --env production --verify
```

A plan checks the file against your account on the server and changes nothing. It lists `changes` per key: `create`, `adopt`, `update` (with the changed `fields`), `unchanged`, `delete`, `orphaned` or `invalid`, plus `issues`, `basedOn` (the environment's latest deploy) and `next` (the exact follow-up commands).

`--verify` first runs every API and browser monitor of that environment locally against its target. Any failure stops there and sends nothing (exit 4, `VERIFY_FAILED`, evidence under `.waitcher/runs`). It uses your local environment variables, not stored secrets.

## Apply

```sh
waitcher deploy apply --env production --based-on BASED_ON [--activate-new] [--smoke]
```

- The whole environment is applied or nothing is. An apply without changes records nothing, so repeating it is harmless.
- `--based-on` takes `basedOn` from the plan you reviewed, or `none` for the first deploy. If another deploy landed since, apply refuses with `CONFLICT`: plan again.
- New monitors start paused. `--activate-new` schedules them immediately. Afterwards the server owns pause and resume, so a pause during an incident survives the next deploy.
- `--smoke` runs each created or updated monitor once on the server and waits up to `--timeout` seconds (default 180). A failure leaves the deploy applied, exits 4 and `next` points at `run diagnose`. Smoke runs are ordinary manual runs: on a personal account only one fits each 10 minutes, so the others report `not_started` with `RATE_LIMITED`. Script smoke runs also count toward the daily script runs.
- Monitors that are deployed but no longer declared are reported as `orphaned` and keep running. `--prune` deletes them and their history.
- Monitors deployed from a file cannot be edited in the web app or with `monitor apply` (`MANAGED_BY_CONFIG`). Change the file. Pause, resume and run once still work anywhere.
- Each deploy records its version from the Git commit (`-dirty` with uncommitted changes); `--source-version` overrides it.

## Roll back

```sh
waitcher deploy list --project-id PROJECT_ID
waitcher deploy rollback --id DEPLOY_ID --dry-run
waitcher deploy rollback --id DEPLOY_ID --based-on BASED_ON
```

The latest 10 deploys of each environment are `restorable:true`. A rollback reapplies that deploy's file and built scripts through the normal deploy path and is recorded as a new deploy. Secrets are not rolled back; current values stay. Revert the change in your repository too, or the next apply brings it back.

## Adopt existing monitors

If you created monitors before using `waitcher.json`, a plan warns `ADOPTION_CANDIDATE` when an existing monitor looks like an entry. If it is the same check, link it:

```sh
waitcher deploy plan --env production --adopt home=MONITOR_ID
waitcher deploy apply --env production --adopt home=MONITOR_ID --based-on BASED_ON
```

The monitor keeps its ID, history and pause state, and takes its definition from the file. `--adopt` is repeatable. Without it, apply creates a second monitor.

## Environment secrets

Scripts list the `CHECK_*` names they need in `settings.envNames` and read them from `process.env`. Values are stored per project environment, are write-only and never belong in `waitcher.json`:

```sh
printf %s "$VALUE" | waitcher secret set --env staging --name CHECK_ORDERS_TOKEN --value-stdin
waitcher secret set --env production --name CHECK_ORDERS_TOKEN --value-file ./private/orders-token
waitcher secret list --env staging
waitcher secret delete --env staging --name CHECK_ORDERS_TOKEN --yes
```

Secrets address the project by its name in `waitcher.json`, so you can set them before the first deploy. A plan reports `SECRET_MISSING` with the names that have no value in that environment. Values are up to 8 KiB, 50 per environment. For `--verify`, export the same `CHECK_*` variables in your shell (never in files). The Monitors page shows the same list, missing names first, when one project environment is selected, and can set a value there too.

**How values are protected.** A stored value is encrypted with a key kept outside the database, bound to your account, the environment and the name, and never shown again, not even to you. Setting the same name again replaces it (the revision goes up); runs already queued keep the value they started with. A hosted run receives only the names its monitor lists.

**What evidence shows.** Reports, traces and logs, hosted and local, show a listed value of at least 6 characters as `<secret:CHECK_NAME>`. Shorter values, values your script transforms (base64, part of a token) and pixels are not recognised: screenshots and video show whatever the page shows. To keep a password out of your test code and out of screenshots, fill it with the framework's handle instead of `process.env`; the run takes no screenshot after that fill:

```ts
import { secrets } from "e2e";
await screen.getByLabel("Password").fill(secrets.get("CHECK_LOGIN_PASSWORD"));
```
