WaitcherDocs

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.

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:

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:

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

{
  "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 } }
  ]
}

Validate

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

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

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

Roll back

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:

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:

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:

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