Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
da003ba
test(deploy): real-build integration + gated live e2e for the Cloudfl…
Brooooooklyn Jul 22, 2026
8fcfa2c
test(deploy): match the KV title exactly, fail e2e teardown on leaks,…
Brooooooklyn Jul 22, 2026
40b91d1
test(deploy): poll the live worker until 200 so a propagation-window …
Brooooooklyn Jul 22, 2026
8449728
test(deploy): gate the live e2e on exclusive resource ownership and f…
Brooooooklyn Jul 22, 2026
d90fce6
fix(deploy): fail closed on ambiguous ownership lookups in the live e…
Brooooooklyn Jul 22, 2026
71d740b
test(deploy): make the live Cloudflare e2e run the real deploy pipeline
Brooooooklyn Jul 22, 2026
1cb7b09
test(deploy): snapshot the FP-lock's expected config before the build
Brooooooklyn Jul 22, 2026
f8ca9c5
test(deploy): spawn wrangler via cross-spawn for Windows portability
Brooooooklyn Jul 22, 2026
f159f96
test(deploy): run the live Cloudflare e2e on every CI
Brooooooklyn Jul 22, 2026
21aa086
test(deploy): gate the live e2e behind a Cloudflare environment and h…
Brooooooklyn Jul 22, 2026
7464e8d
test(deploy): fail the live-e2e job closed when Cloudflare creds are …
Brooooooklyn Jul 22, 2026
01b3b58
test(deploy): cover D1, KV, cron, and re-deploy idempotency in the li…
Brooooooklyn Jul 23, 2026
dee7f6e
test(deploy): harden the live e2e — bounded deploys, wider KV poll, s…
Brooooooklyn Jul 23, 2026
4319634
test(deploy): bound the wrangler helper and CF-API existence checks i…
Brooooooklyn Jul 23, 2026
1c90652
test(deploy): bound the whole fetch-retry loop by the enclosing test'…
Brooooooklyn Jul 23, 2026
f17334d
test(deploy): bound the cron poll and aggregate wrangler work to each…
Brooooooklyn Jul 23, 2026
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
44 changes: 44 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ jobs:
runs-on: ${{ matrix.os }}
permissions:
contents: read
# No Cloudflare creds in this job. The credentialed live deploy e2e runs in the separate
# `live-e2e` job (below), whose secrets are bound to the `cloudflare-e2e` environment. Here the
# live suite (part of `vp run test:all`) sees empty creds and self-skips on every event.
strategy:
fail-fast: false
matrix:
Expand All @@ -43,11 +46,52 @@ jobs:

- run: vp run test:all

# Credentialed live Cloudflare deploy e2e, ISOLATED from `test` so the account token exists ONLY in
# a job bound to the `cloudflare-e2e` GitHub Environment. That environment's deployment branch policy
# allows `main` ONLY and the ref is enforced server-side, so a `pull_request` run (ref
# refs/pull/<n>/merge) can never reach the secret — even a PR that edits this file — and a modified
# workflow cannot exfiltrate it. The `if` also keeps the job off everything except a direct push to
# the `main` branch: tag pushes and `workflow_call` (release) carry a non-main ref, so they are
# skipped — which both avoids a branch-policy failure and matches where the secret is allowed, and
# it is scoped to the canonical repo so forks don't run it. It deploys a UNIQUELY-named worker
# (VOID_E2E_RUN_ID) to a throwaway account and tears it down. A preflight step fails the job when a
# credential is empty (unset / deleted / not-yet-rotated token), so a missing secret can never turn
# the live-deploy gate into a silent green no-op (the `test` job's uncredentialed run still skips).
live-e2e:
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && github.repository == 'voidzero-dev/void' }}
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: read
environment: cloudflare-e2e
env:
VOID_E2E_CF_TOKEN: ${{ secrets.VOID_E2E_CF_TOKEN }}
VOID_E2E_CF_ACCOUNT_ID: ${{ secrets.VOID_E2E_CF_ACCOUNT_ID }}
VOID_E2E_RUN_ID: ${{ github.run_id }}-${{ github.run_attempt }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- uses: voidzero-dev/setup-vp@2dec1e33f4ab2c6d5bce1b0c4607961bb1a3f7a1 # v1.12.0
with:
node-version-file: .node-version
cache: true
# Fail closed BEFORE the build: a missing/empty credential must fail this job, not self-skip. The
# `${VAR:?}` expansion exits nonzero when the secret is unset OR empty (GitHub resolves an unset
# secret to '').
- name: Require live Cloudflare credentials
run: |
: "${VOID_E2E_CF_TOKEN:?empty — set it as a cloudflare-e2e environment secret}"
: "${VOID_E2E_CF_ACCOUNT_ID:?empty — set it as a cloudflare-e2e environment secret}"
# build:core produces dist/cli/cli.mjs + the built `void` plugin the deploy uses to build the app.
- run: vp run build:core
- run: vp test run packages/void/test/e2e/cloudflare-deploy-live.test.ts

done:
runs-on: ubuntu-latest
if: always()
needs:
- test
- live-e2e
steps:
- run: exit 1
if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
2 changes: 1 addition & 1 deletion packages/void/src/cli/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2094,7 +2094,7 @@ function isCleanWranglerExit(result: DeployWranglerRunResult): boolean {
* unreadable — callers then treat every field as unset (safe: a genuinely broken
* config fails later in wrangler with a clear error).
*/
function parseRootWranglerConfig(configPath: string): Record<string, unknown> {
export function parseRootWranglerConfig(configPath: string): Record<string, unknown> {
if (!existsSync(configPath)) {
return {};
}
Expand Down
85 changes: 85 additions & 0 deletions packages/void/test/e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# `packages/void/test/e2e`

Gated live-account end-to-end tests. These run REAL Cloudflare operations against a REAL account, so
they **skip unless credentials are present**. Locally that means exporting the env vars below; in CI
they run on **pushes to `main`** (see [In CI](#in-ci)).

## `cloudflare-deploy-live.test.ts`

Deploys `playground/spa` (D1 + KV + cron) to the target account via `void deploy --backend cloudflare`,
verifies the live worker answers HTTP 200, then deletes the worker and every resource it created.

It does **not** drive `--provision`: that path fails closed in non-interactive shells by design (a
spawned CLI has no TTY, and CI cannot commit back the ids a create would mint). So the test plays the
provisioner's role itself — it creates the id-bearing D1 + KV directly with `wrangler … create`,
writes their real ids into the app's root config exactly as `--provision` would, then runs the
CI-safe `void deploy --backend cloudflare` (no `--provision`). That exercises the real
build → tamper-guard → remote-migrations → `wrangler deploy` pipeline end to end.

### Prerequisites

The suite is skipped unless BOTH env vars are set:

| Env var | Purpose |
| ------------------------ | -------------------------------------------------------------------------- |
| `VOID_E2E_CF_TOKEN` | Cloudflare API token that can create + delete Workers Scripts, D1, and KV. |
| `VOID_E2E_CF_ACCOUNT_ID` | Cloudflare account id to deploy into. |

The token needs, on that account, **Workers Scripts: Edit**, **D1: Edit**, and **Workers KV Storage:
Edit** (Edit implies read, covering the ownership/teardown list calls). Cron triggers ride along with
the worker deploy — no extra permission.

The account must also have a **workers.dev subdomain** already registered (with its wildcard TLS cert
provisioned). Without one, `wrangler deploy` uploads the worker but cannot publish a public URL, and
the HTTP-200 assertion has nothing to hit. Opening the Workers & Pages dashboard once registers a
subdomain automatically; a freshly-registered subdomain takes a few minutes for its cert to go live,
during which the first run may see a TLS handshake failure — rerun once the cert is ready.

With no env vars the suite SKIPS cleanly (0 run, N skipped).

### Running it

```sh
# Ensure the CLI is built first (dist/cli/cli.mjs):
vp run build:core

VOID_E2E_CF_TOKEN=... VOID_E2E_CF_ACCOUNT_ID=... vp test run packages/void/test/e2e
```

### In CI

`VOID_E2E_CF_TOKEN` and `VOID_E2E_CF_ACCOUNT_ID` are **environment secrets** on the `cloudflare-e2e`
GitHub Environment (not repo secrets), and a dedicated `live-e2e` job in `.github/workflows/ci.yml`
consumes them — the main `test` job carries no creds at all. Two layers keep the account token off
untrusted code:

- The `live-e2e` job runs only on a direct push to `main` (`github.event_name == 'push' && github.ref
== 'refs/heads/main'`), so `pull_request` runs, tag pushes, and `workflow_call` (release) skip it.
- The `cloudflare-e2e` environment's deployment branch policy allows `main` **only**, enforced by
GitHub server-side on the run's ref. A `pull_request` run's ref is `refs/pull/<n>/merge`, so it can
never reach the secret **even if a PR edits this workflow** — the expression-level gate alone would
be bypassable that way; the environment ref-check is the real boundary.

So the credentialed live deploy runs on every merge to `main`; everywhere else the creds are empty and
the suite SKIPS (the `test` job still imports the file and skips it). The `live-e2e` job itself is
fail-closed — a preflight step errors if either credential is empty (a missing, deleted, or
not-yet-rotated secret), so a misconfigured gate is loud instead of a silent green skip. Each
`live-e2e` run also gets `VOID_E2E_RUN_ID = <run-id>-<attempt>` for the unique per-run resource names
below.

### Resource naming & cleanup

Each run derives a UNIQUE resource name — `void-e2e-cf-deploy-<suffix>` — so overlapping runs never
collide on remote resources. The suffix is `VOID_E2E_RUN_ID` (CI: `<run-id>-<attempt>`) or, locally, a
timestamp + random. Derived names mirror the provisioner: `<name>-db` (D1), `<name>-kv` (KV). The
shared `void-e2e-cf-deploy-` prefix keeps every resource greppable, so an orphan from a hard-killed run
is easy to find and `wrangler delete`. A pre-run ownership gate refuses to start if the run's names
already exist (failing closed on any lookup it cannot confirm), so the test never adopts or deletes a
resource it does not own. Teardown runs even when the deploy or an assertion fails.

> **Local concurrency:** unique names isolate _remote_ resources, but a run also snapshots and
> overwrites the shared `playground/spa/wrangler.jsonc` and builds in place. Do **not** run two live
> e2e invocations against the same checkout at once — they would race that file. CI is unaffected (each
> runner is an isolated checkout, and only the single `main`-push `live-e2e` job deploys). The deploy
> test also asserts the returned host's worker label equals this run's `WORKER_NAME`, so a clobbered
> overlay fails loudly instead of validating another run's worker.
Loading