This documentation is available as Markdown. For the complete index, see llms.txt. Skip to content

Pushing suites

For the complete documentation index, see llms.txt

Tests reach a box in one direction only: you push them. A box holds no deploy key and no token to anybody’s repository and never pulls, so nothing is on it that was not pushed to it.

Terminal window
npx @unotest/web bundle push --box https://tests.example.com --token unob_…

UNOTEST_BOX_URL and UNOTEST_BOX_TOKEN supply the two values instead of flags — a CI job usually takes them from its secret store.

What a bundle is

bundle push packs the unotest/ directory — the suite package: its scenarios, helpers, collections, unotest.config, package.json and lockfile — into one tar.gz and uploads it. The product around it never travels, and neither do its dependencies: the box runs npm ci over the suite’s own manifest.

  • In a git project, git decides what belongs. The file list is git ls-files --cached --others --exclude-standard, so whatever your .gitignore keeps out stays out. Content is read from the working tree, so uncommitted changes travel — the manifest records dirty: true and the viewer labels the bundle wip.
  • Secrets never travel. unotest/.env, .env.<name>, .secrets, .secrets.<name> — and any <name>.env / <name>.secrets — are excluded even when committed. A file that looks like one and still reaches the packer stops the push. Environment values live on the box; see Environments & values.
  • Also excluded: node_modules, .git, run and queue directories, .unotest/, .tmp, .debug, the viewer lock file.
  • Install hooks are stripped from the packed package.json (postinstall, prepare, …): a box installs the suite, it does not run your project’s hooks. The push reports what it removed.
  • Your dependencies’ install scripts run on the box, so a native module builds or fetches its binary as usual. They run in a container of their own — the bundle tree is its only writable mount, and none of the box’s state is in reach — which is what makes running somebody’s postinstall on a shared machine affordable.
  • The id is the content. bundleId is a hash of the packed files, so re-pushing an unchanged suite is an instant no-op (already had this exact bundle), and two people packing the same tree get the same id.
  • Names of externals travel, values do not. The bundle carries the names your .env / .secrets files (or their .example twins) declare, so the box can answer the push with what an environment cannot supply.

Refused before upload

What would fail on the box is refused on your machine, with the file, the line and the fix:

  • a scenario that reads a file outside unotest/ (MODULE_NOT_FOUND on the box, hours later);
  • no lockfile, or no @unotest/web in the suite’s dependencies;
  • a symlink;
  • a secrets file inside unotest/;
  • a suite over 256 MB;
  • a schedules registry that cannot be read — the box reads it to know what runs on its own.

--dry-run packs and checks without uploading; --out <file> keeps the archive (tar -tzf shows what is inside); --json prints { bundleId, status }.

Refused by the box

The box verifies the archive before writing a single file and answers with a typed refusal; the CLI prints it with what to do next:

RefusalMeaning
unauthorizedThe token is unknown, revoked, or for another box.
min-versionThe bundle pins an @unotest/web older than the box runs. Bump it in unotest/package.json, commit the lockfile, push again.
scope-mismatchThis repository belongs to another project on the box than the token’s.
too-largeOver the box’s upload limit (128 MB by default).
malformed / id-mismatchThe upload did not arrive intact — retry; if it repeats, a proxy is rewriting the body.

The token alone decides which project a bundle lands in. Nothing in the request can name one.

After a successful push the box may add notes: environments that hold no value for a name the suite declares, with the command that sets it.

Running after a push

Terminal window
npx @unotest/web bundle push --run --env test --collection smoke

--run orders a run of the bundle just pushed, on the same token:

  • --env <name> is required — which environments a box has is the operator’s decision.
  • --collection <name> names what to run; repeat for several. Without it the box runs what that environment’s schedules name.
  • --pr <number> ties the push to a pull request. The queue shows it (PR #412@1a2b3c4), and a newer push of the same pull request withdraws its older runs that are still waiting; a run that already started is never killed.

The box answers 202 as soon as the runs are queued, with their run ids — a suite takes minutes, and a request held open that long is a timeout, not a result. The ids are real from that moment; --json carries them (runs: [{ runId, collection }], plus superseded when older runs were withdrawn), so a CI log can print links into the viewer at once.

Run requests are rate-limited (60 per minute per box, and per token).

Refusals: unknown-environment, scope-mismatch (the environment belongs to another project), unknown-bundle (the box no longer holds it — push again), no-collections (nothing named and no schedule for that environment), malformed (the box is older than this CLI — push without --run).

A minimal CI job

jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npx @unotest/web bundle push --run --env test --collection smoke --json
env:
UNOTEST_BOX_URL: ${{ secrets.UNOTEST_BOX_URL }}
UNOTEST_BOX_TOKEN: ${{ secrets.UNOTEST_BOX_TOKEN }}

Exit codes: 0 pushed (and queued), 1 the box refused or was unreachable, 2 the suite was refused on your machine — nothing left it. With --json, a failed run order after a successful upload still reports the bundle, so the job does not push it again for nothing.

From a GitHub webhook

With the box’s GitHub App installed (see Administration), the box also reacts to your repository’s events. Every mapping lives on the box, never in the repository:

  • a deployment event’s environment name is mapped to one of the box’s environments; an unmapped name is logged and dropped, never guessed;
  • pull requests run in a named environment, or each in its own preview environment (below), running the collections the project’s CI configuration names or the environment’s schedules;
  • the bundle is matched by commit sha against what has been pushed. A commit no bundle was pushed for waits up to 15 minutes for your CI job to upload, then the check fails saying so;
  • fork pull requests are not run. Their head is somebody else’s branch, and running it would hand it the environment’s secrets. A maintainer re-requesting the check in GitHub is the opt-in.

The box reports one check per collection (unotest / <collection>): queued when the event arrives, in progress when the run takes its slot, completed with a summary and a link into the viewer.

Pull-request environments

A project with preview deployments can give each pull request an environment of its own. unotest configures a template with you:

  • the target URL shape, with {pr} where the number goes (https://pr-{pr}.preview.acme.dev) — the box substitutes, it never deploys;
  • variables every preview run gets;
  • how many may exist at once (5 by default) and how long one survives after its last run (72 hours by default).

The environment is named pr-<n>, created when the pull request is first seen and removed when it closes or goes unused past the TTL. Preview environments learn the suite’s schedules but never arm them: what CI asks for runs, the clock does not. Secrets are set per environment by an administrator; a preview environment that needs one is not one that should be created automatically.

Bundles in the viewer

On a box the viewer shows what it is running:

  • Bundle badge in the status bar — main@1a2b3c4, or wip for a bundle from an uncommitted tree, with the author, the pinned @unotest/web and who switched the environment last.
  • Switching. Clicking the badge lists the bundles pushed to this project and switches the environment to one. The switch is a maintenance ticket: a running suite finishes, then the code changes, then the next run starts on the new bundle. An environment with allowSwitch off refuses (follows its own branch — it cannot be switched by hand).
  • Following the branch. An environment follows the newest bundle of its defaultBranch until somebody selects one by hand; that selection survives restarts and later pushes until the next person switches. Schedules always run the newest bundle of defaultBranch, regardless of what anyone selected — a tester’s switch never redirects tonight’s suite.
  • Runs remember their bundle. The runs list marks runs that ran on another bundle, and a this bundle only filter narrows the history to the bundle in front of you.
  • Project / environment switcher in the tree header, where a local viewer shows the checkout directory. Each environment is its own viewer process, so switching is a full page load.

Retention

Each push leaves an archive and, once installed, a node_modules. The box keeps the newest 5 bundles per branch regardless of age and removes older ones after 30 days (both configurable) — never a bundle an environment currently points at or a waiting run has asked for.