Repository navigation
Fix issues found in a live self-hosted platform end-to-end test - #374
Merged
Merged
Conversation
Cloudflare addresses dispatch namespaces by name, but uninstall sent the namespace UUID. Cloudflare answered 404, which uninstall treats as already gone, so the namespace survived while the manifest recorded it as deleted. deleteResource now takes the manifest resource and uses its name for dispatch namespaces and its id for every other kind. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The installer's empty-database check only looked at tables on the search path, so a database that still held drizzle.__drizzle_migrations passed as empty. The leftover history was then read as already-applied migrations after the claim and after Cloudflare resources were created, and the install failed late. The check now also looks at the drizzle schema and refuses the database up front. A resumed install that already holds the matching claim is unaffected. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`void platform uninstall` now names every resource it keeps (kind, name and id) in the order of the manual cleanup steps, plus recorded Access resources. `--plan` shows the same list before any change. When the installation sends email, both say that Email Routing for the sender domain is kept. The final message points to the local installation record. A created AI Gateway that is already gone from Cloudflare is now recorded as deleted instead of retained. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The shared box renderer threw "Invalid count value: -1" when the terminal reported fewer than six columns, as a 0x0 pty does, so any note() call (for example the uninstall plan) crashed the command. Route every void note() through a small wrapper in output.ts that treats a non-positive width as unknown (80 columns, like the rest of the CLI) and draws narrower terminals at the smallest width the box supports. The vendored renderer stays byte-for-byte upstream. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The uninstall guide now deletes the tail Worker last, after the platform Workers and the dispatch namespace apps that send logs to it, and adds an Email Routing step before the email gateway Worker is deleted. It also says where the local uninstall record and recovery file stay, and that both can be deleted after cleanup. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The platform Cloudflare client can now delete Worker routes, custom domains, Queue consumers, dispatch namespace scripts, and Access applications and service tokens, and list Queue consumers and namespace scripts. Workers are deleted by script name, and an AI Gateway is deleted with the runtime credential when one is set, like its other calls. Deletes never send force, treat 404 as already gone, accept a success response without a result, and include Cloudflare error codes in their errors (for example 100109 for a tail Worker still in use). Emptying an R2 bucket can run an ownership check before each page of deletes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…reated `--purge-all` includes `--purge-data` and also deletes the platform Workers, Queues, the dispatch namespace scripts, the R2 bucket, the AI Gateway, the platform DNS record, Worker routes and custom domains, and the Access applications and service tokens the installer created. It keeps zones, adopted resources, external PostgreSQL, Email Routing settings, Analytics Engine datasets, and API tokens. It deletes in an order Cloudflare accepts: traffic and DNS, Access, Queue consumers, Workers except the tail Worker, Queues, the dispatch namespace, the tail Worker, Hyperdrive and KV, R2 (emptied first), the AI Gateway, and routing KV last. Each resource is proven to belong to the installation right before its DELETE. Routes and custom domains must still serve the installer Worker they were recorded for. Progress is checkpointed, so rerunning the command continues after a failure. Before any change it refuses while projects remain, and checks that the credential can make every planned deletion: browser login cannot delete DNS records or Access resources, and missing token grants are named. `--plan` lists every resource it will delete, in order, and every resource it keeps, with the token permissions it needs. Ownership checks now list dispatch namespaces only when one is being checked, so installations without Workers for Platforms make no such call. The kept-resource list after uninstall now puts the tail Worker after the dispatch namespace, matching the manual cleanup steps. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Look up and delete Access resources in the account that holds them, so a service token in a separate identity account is deleted instead of being recorded as gone. The credential check names that account too. - Delete the AI Gateway only when Cloudflare reports it was created next to the installer's provision intent. A gateway created later under the same id, or one without a reported creation time, is kept and listed. - Read the project list again before deleting the application Workers in the dispatch namespace, and stop if a project was created during the run. - Save the R2 bucket's generation when its ownership marker is removed, so a rerun can finish buckets recorded before creation fingerprints existed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…stalls with --purge-all - A route, custom domain, DNS record, or Access resource that no longer matches what the installation recorded is kept instead of stopping the run. It is listed with the reason in the final kept note, and a rerun does not look at it again. - When the platform sends email, --purge-all deletes the Email Routing rules in the sender zone that send mail to this installation's email gateway Worker, and resets such a catch-all rule to its disabled drop default, after proving the Worker still carries the installation marker and before deleting it. Rules for other Workers, Email Routing itself, and the sender domain's MX, SPF, and DKIM records are kept and listed. The credential check names Zone → Email Routing Rules → Edit, and only when email is enabled. - If a plain or --purge-data uninstall stopped before it finished, --purge-all now deletes the resources that run recorded as retained, including routing KV, with the same proof as ready ones. A finished uninstall still cannot be reopened. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Cloudflare's account token links cannot preselect a zone, so the dashboard opens zone permissions under All Domains. The management and runtime token prompts now name the exact change (All Domains to Specified Domains and the zone to select), keep All Domains when Void creates the zone, and the installation docs say the same. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Page title, description and layout in .md files are now found when the page opens with a <script> block, matching how @void/md reads the same frontmatter. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The playground defined its live stream in lib/, which binding inference does not scan, so builds emitted no VOID_LIVE Durable Object and /api/live/publish failed. Move it to src/live.ts as the live guide shows, document that the module importing void/live must be in a scanned directory, and list void/live in the resource inference table. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…urces with other kept items - When --purge-all finishes an uninstall that stopped before it finished, the resources that run recorded as retained are now proven before the first change, like ready ones. A Worker that no longer carries this installation's marker stops the run before anything is deleted, and the retained resources stay recorded as retained. - An Access resource that an earlier --purge-all kept because it changed is listed under the kept resources with its reason, not under "Keep Access resources you connected". Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
analog, react-pages, tanstack-start, vinext-app, vinext-pages, vue-pages and pg-test shipped a hand-written initial SQL migration without a Drizzle journal or snapshot, so every deploy stopped at the schema drift check. Their schemas also disagreed with the SQL (missing unique email, missing created_at, serial author_id). Align each db/schema.ts with its SQL and add a journal plus snapshot under the existing migration tag, keeping the SQL files unchanged, so the drift check passes and later `void db generate` runs diff from this baseline. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…grations When db/migrations has SQL files but no Drizzle journal or snapshot, the drift error told users to run `void db generate`, which cannot work: it emits a migration that creates the existing tables again and fails the fresh-database check. The error now names the missing metadata and the steps to add it, and keeps the generate advice for Drizzle-managed projects. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The test drives two full project batches and passes in about 1.5s alone, but took 10.4-14.9s when the platform API suite ran alongside other test runs, so its 10s limit timed it out. Under that load it timed out in 8 of 9 runs before and 0 of 9 after (slowest 15.5s). This matches the 30s limit the 103-project domain activation test already uses. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The runtime token prompt told installers to switch to Specified Domains even when the install plan creates the zone, which cannot be selected before it exists. It now keeps All Domains in that case so Cache Purge reaches the new zone, matching the management token prompt and the credentials guide. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Setting inference.scanDirs replaces the default directories, so the live guide now tells users to list the defaults they still use alongside lib/. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Cloudflare names a Worker queue consumer in script_name. Teardown only read script and service, so it skipped the installation's own consumers and then failed to delete their Workers and Queues. Read script_name first, keep the older names Wrangler still reads, and refuse a Worker consumer that has no name instead of skipping it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Project creation now takes the shared configuration lock that `void platform uninstall` holds across its last project check. A create request admitted before traffic was disabled either commits before that check, so --purge-all refuses before changing anything, or waits and is refused with 409 once the uninstall is recorded. Before, such a project could commit right after the check and was only noticed after traffic, Access, Workers and Queues were already deleted. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… are changing --purge-all proves each resource right before deleting it, but Cloudflare has no conditional delete, so a change made in between is not detected. Say so in the uninstall guide and next to the proof helper. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Creating a project while `void platform uninstall` is in progress now answers 409 with "The platform is being uninstalled, so new projects cannot be created." instead of the Zero Trust and deployments message shared by other refused requests. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A tail batch from the dispatch Worker's consumer holds the dispatcher item and the user Worker items it invoked. Each user Worker's own tail consumer already delivers those items, so the tail Worker now writes only the dispatcher items from such batches. Batches without a dispatcher item (including crons, migrations, alarms and queues) are written as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On an ISR miss the dispatch Worker wrote its HTML and JSON edge copies with `x-isr-cache: MISS`, so every later request served from the edge still reported MISS. The edge copies now say HIT, matching the direct runtime; only the response that triggered the render reports MISS. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
D1 exec() treats each line as its own statement, so a multi-statement migration without Drizzle breakpoints failed on any multi-line statement such as CREATE TABLE. Legacy migrations now split on top-level semicolons (ignoring strings, quoted identifiers and comments) and run the statements plus the tracking insert in one D1 batch. SQL that may hold a BEGIN ... END body, such as CREATE TRIGGER, still falls back to exec(). The legacy-plan warning now tells users to add `--> statement-breakpoint` between statements, and the playground migrations use breakpoints. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… accurately The CLI dropped migration_warning events, so warnings from the migration runner never reached the user. They now print with log.warn, which stays visible above the spinner. The migration_start count covers every bundled migration, not only the unapplied ones, so the spinner now reads "Checking N bundled migration(s)..." instead of "Running N migration(s)...". Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With `[voidPlugin(), vinext()]`, vinext's RSC entry replaced the Cloudflare worker entry for the rsc environment, so the build shipped vinext's bare handler instead of Void's wrapper and the worker could not serve requests. Void now restores the Cloudflare worker entry after all plugin configs are applied. The deploy wrapper's readiness probe now answers 503 with ready:false when the wrapped entry has no fetch handler, so such a build is not promoted. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
When Cloudflare answers 401 or 403 while the install plan reads DNS records for the application wildcard or the custom API hostname, report which zone was denied and that the management token needs Zone → DNS → Edit, instead of surfacing the raw "Authentication error". Other errors pass through. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The tail worker only receives trace events from the dispatch worker, so it no longer publishes a workers.dev subdomain or preview URLs when platform endpoints are public. Upgrades turn off an existing tail subdomain because the live subdomain no longer matches the desired trigger state. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Worker deploys add a per-deploy cache-buster file to the asset manifest sent to Cloudflare. The asset_upload event and the final `assets` count included it, so a deploy of N files reported N + 1. Report the count of the user's assets instead, and keep the internal count for gating Cloudflare asset work. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`void project status` showed `-` in the isr column for deployments that only configure per-path revalidation. The column now shows the global revalidate interval when set, otherwise the number of per-path rules (for example `1 rule`), and `-` only when ISR is not configured. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Deploy log names only carried a millisecond timestamp and were opened in append mode, so two deploys started at the same moment on one machine wrote interleaved records into one file. Add the process id and a short random suffix to the name and open it exclusively. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…mmands `void platform status` now prints that the platform is disabled and the `void platform enable` command when discovery returns the traffic gate's 503 response, instead of exiting with a raw discovery error. Operator requests that fail without a message now include the API error, so a disabled platform reads "Platform disabled. Run `void platform enable`." Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The disable plan now says it blocks all platform traffic and the platform API, and that routes, custom domains, Workers, and data stay installed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… CI project selection Disable also blocks the platform API and CLI, leaving only the administrator status page and sign-in. Enable redeploys and verifies the platform Workers before restoring traffic. On a Void platform, SPAs return 404 for missing files under the hashed assets folder. CI deploys must choose a project with VOID_PROJECT, --project, or a committed .void/project.json. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Token create, list, and renew responses passed Postgres timestamp strings through unchanged (for example "2026-10-06 04:01:25.899+08"). They now return UTC ISO-8601 strings; a missing revocation time stays null. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Operator human output now uses sentence-case field labels that keep known acronyms (API version, GitHub OAuth, Callback URL, Installation ID). `void platform config auth` commands print the same readable text by default and keep JSON for --json. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ing --purge-all The API Worker checks the proxy through a service binding and the proxy calls the API through another, so Cloudflare refused whichever went first and --purge-all stopped at the proxy Worker. When Cloudflare refuses a platform Worker, purge-all now reads what depends on it the way `wrangler delete` does and forces the DELETE only when every dependent is a Worker this installation created. A Worker outside the installation still stops the run, and the message now names it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…aller's gateway Cloudflare reports an AI Gateway's created_at without a zone, for example "2026-10-05 18:29:14". JavaScript read that as local time, so outside UTC the gateway the installer created seemed hours away from its creation record, and --purge-all kept it as not created during installation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`void project delete` printed "Project deleted." from its spinner and then "Deleted <slug>". The spinner now clears, so only "Deleted <slug>" remains. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
--purge-all forced a platform Worker's DELETE when each Worker that bound to it had the name of a Worker the installation created. A Worker that took one of those names during the run, or after the installation's own Worker was recorded as deleted, passed that check, so the forced DELETE left it with a broken binding. Each dependent must now be a Worker this run still deletes and must carry this installation's ownership marker; otherwise the run stops before forcing anything. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An upgrade's staged window and a Sandbox drain close the API traffic gate with the same 503 as `void platform disable`, so `void platform status` reported the platform as disabled, suggested `void platform enable` on machines without the saved operation, and exited 0. Operator commands suggested enabling it too. The API now adds `reason: "maintenance"` to that 503 while an upgrade or repair has traffic paused. `void platform status` then exits with an error that says an upgrade or repair is in progress, as the docs describe, and operator commands say to retry when it finishes instead of suggesting `void platform enable`. A platform without the field is still reported as disabled. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Deploys now print migration warnings, and the runner built them from every bundled migration. An app with an older migration that lacks statement breakpoints saw the advice to add them on every deploy, though that migration never runs again. The runner now drops the warning once the migration is recorded as applied. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The CI guide suggests committing .void/project.json, but new apps list .void in .gitignore, so git refuses to add it. The guide now gives the ignore rules that keep the rest of .void out and let the link in. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… note Queue deliveries reach user Workers through the dispatch Worker, so they are not among the invocations whose only log path is the user Worker's own tail consumer. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ping The maintenance flag on PlatformDisabledError was a constructor parameter property, which Node's strip-only TypeScript mode rejects, so scripts that import the CLI source directly failed to load. It is now a plain field. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A request routed through the dispatch Worker reaches the tail Worker twice: through the dispatch Worker's tail consumer and through the user Worker's own consumer. On a live install Cloudflare delivered each copy in its own tail invocation, never in one batch with the dispatcher item, so the tail Worker could not drop the second copy and every request appeared twice in `void project logs` and the deployment log views. The tail Worker again writes every item it receives. The project log query and the operator deployment log query now group by every stored column, so each invocation is returned once, and paging still counts unique rows. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
🚀 Deploying Preview to Cloudflare 🚀Preview URL: https://fix-platform-e2e-findings-void-cloud.voidzero-docs.workers.dev (commit 277617d)This URL reflects your latest Preview deploymentPreview Deployments by commit
|
Member
|
Nice. fwiw I left resource removal intentionally out initially (and kinda forgot to come back to it). I'm wondering if |
The flag does not delete everything: zones, adopted resources, and external PostgreSQL stay. The new name says what it does and matches --delete-zone. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Both destructive uninstall flags now use the same verb: --purge-data and --purge-resources. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Member
Author
|
Renamed it to |
cpojer
approved these changes
Oct 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes found during a live end-to-end test of the self-hosted Void Platform on a disposable Cloudflare account: clean up an old installation, install a new platform on a domain, deploy every app kind, run lifecycle commands, and uninstall.
Uninstall
void platform uninstall --purge-resources: deletes every resource the installer created (Workers, Queues, KV, R2, Hyperdrive, dispatch namespace, AI Gateway, routes, DNS records, custom domains, installer Access apps, Email Routing rules that target the platform's gateway), in an order Cloudflare accepts. Each resource is proven against the installation's ownership record right before its DELETE. Changed or foreign resources are kept and listed. Runs are resumable. Zones, adopted resources and external PostgreSQL are never deleted.--planand the final output list every kept resource in manual cleanup order. The docs give an order Cloudflare accepts, with the tail Worker last and Email Routing included.Install
install --plannames the missing token permission. Before, it printed a raw 403.Runtime and deploy
void project logsshows each runtime log line once.<script>block.project status, a log file per concurrent deploy, ISO-8601 token timestamps, readable operator output, a singleproject deleteresult line, CLI notes in zero-width terminals.platform statusand operator commands report a disabled platform, or an upgrade in progress, clearly.Playgrounds and docs
.void/project.json.Verification
vp run check,vp lint,vp fmt --check: clean.vp test run: 680 files, 12,866 tests passed, 29 skipped.--purge-resourcesremoved every installer resource in one run, and the account matched its state from before the install.mainalso covered the four Pages adapters, SPA, SSR, SSG, static directories, WebSockets, SvelteKit, Nuxt, rollback, disable/enable, project tokens, secrets and logs.GitHub OAuth login was not exercised.
Follow-ups (product decisions, not changed here)
void project logsrejects--projectwhile sibling commands accept it.--dirdeploys mark/assets/*immutable even when file names have no hash.secret deleteof a missing secret reports success.platform statusexits 0 for a disabled platform.platform config authprints text, not JSON, when stdout is not a TTY.🤖 Generated with Claude Code