Repository navigation
Commit 5802901
fix(platform): stop before the traffic pause when an upgrade cannot finish (#437)
## Problem
An operator upgraded a self-hosted platform from 0.27 to 1.1. Every
upgrade attempt failed **after** Void paused application traffic, and
every retry failed the same way.
```
void platform upgrade
close traffic gate ──▶ application traffic paused
upload platform Worker (cf CLI, no terminal)
└─ Worker was changed in the dashboard / with the API
(for example a hand-set EMAIL_FROM)
cf strict mode: "Aborting the upload operation because of conflicts.
... remove the `--strict` flag"
✗ run fails, gate stays closed ──▶ traffic stays paused
retry ──▶ same abort (--yes does not answer cf's prompt)
```
Causes, checked against cf 1.0.0-beta.17 and live throwaway Workers:
| Cause | Detail |
| --- | --- |
| cf strict mode is hard-coded for `deploy` and `versions upload` | With
`last_deployed_from` = `api`, or `dash` with a config diff that is not
add-only, cf asks. Without a TTY it aborts. No flag or env var skips it.
|
| Void runs cf with `stdio: ['ignore', 'pipe', 'pipe']` | So cf can
never ask. |
| The check happens after the traffic gate | The installer closes the
gate before the first Worker upload. |
| Operators had to edit Workers by hand | The installer never set
`EMAIL_FROM`, and the API README told operators to set it in the
dashboard. |
| Void's own trigger-record check | An API-edited Worker's active
version has no Void record, so the deploy refused it ("predates safe
trigger snapshots"), also after the gate. |
| Email permissions | The runtime token needs Email Routing permissions.
Nothing checked them before the pause, and the late error did not name a
permission. |
## What this PR changes
```
upgrade | repair | enable | rollback | install --resume
│
├─ find platform Workers the run uploads that need approval: (no Worker change yet)
│ last_deployed_from dash | api
│ or active version without a Void deployment record
│ or (interrupted cutover) the version the run restores has no record
│ unreadable (after 2 retries) or no single version at 100% ──▶ STOP
│
├─ email on? check runtime token: Zone Read, Zone Settings, (no Worker change yet)
│ Email Routing Rules, Email Routing Addresses (read-only GETs)
│
├─ none ────────────────▶ normal run (unchanged)
├─ no TTY or --yes ─────▶ STOP "No platform Worker changed. This command did not pause
│ application traffic. ... Run this command once in a terminal"
└─ TTY ─▶ note + "Replace the configuration of these Workers?" (default No)
├─ No ──────▶ STOP (same wording)
└─ Yes
▼
installPlatform
├─ provision resources, prepare database
├─ for dash/api Workers: pause spinner ─▶ cf versions upload, stdio inherited
│ (cf shows its diff and asks; version is NOT deployed) ─▶ re-read marker
│ └─ refused ─▶ STOP "No deployment changed. This command did not pause application traffic."
├─ traffic gate
└─ deploy (agreed Workers may have an active version without a Void record)
```
Other changes:
- **Email preflight.** When platform email is on, the runtime token's
Email Routing permissions are checked before the pause. A denial names
each missing permission and keeps the saved token (edit its permissions;
the value stays the same). A `401` on the token saved on this machine
warns and continues, because the platform may use a newer token. A late
shared-email setup failure now names the likely permission.
- **Default sender.** Platform mail (invitations, welcome, deploy
failures, error alerts) sends from `<display name>
<noreply@<EMAIL_SENDER_DOMAIN>>`. An explicit `EMAIL_FROM` still wins.
Void Cloud's hosted sender does not change.
- **cf strict-abort text.** It no longer says "Wrangler" or "remove the
`--strict` flag". It names an outside change only for the marker cases.
A remote secret with the same name as a Void value says which secret to
delete. A workflow owned by another Worker says to delete or rename it.
- **Docs.** Maintenance (upgrade, changes made outside Void, a
paused-traffic note), credentials (email permissions before an upgrade),
email (default sender), CI, CLI reference, and the API README no longer
tells operators to set `EMAIL_FROM` / `AUTH_REDIRECT_URI` by hand.
## Decisions
| Topic | Decision |
| --- | --- |
| Override without asking | No new flag. `--yes` is not consent to
replace outside changes. cf has no non-interactive override, so CI stops
and names the command to run once in a terminal. |
| Where cf asks | Inside `installPlatform`, after resources and the
database are ready, before the gate. An upgrade that adds a resource
(for example the deployment queue) needs it to exist before cf can
upload a valid version. |
| What cf uploads | An undeployed version built from the run's own
config. The real deploy follows in the same run. |
| `dash` markers | Always flagged, also when cf would accept an add-only
diff. Void owns these Workers, so it shows the change and asks, and
never replaces it without approval. |
| Failed marker or version read | Retry twice on 429, 5xx or a network
error, then stop before any Worker change. Nothing has changed at that
point, so a stop costs only a retry; a guess could pause traffic and
then fail. A 404 means the Worker does not exist, unless its marker
shows an outside change. |
| Email check scope | Only when platform email is on and the
installation is not staying disabled. A platform without email makes no
new request. |
| Saved token on this machine | `403` stops with text that names the
saved token and the env var. `401` warns and continues. |
| Sender override | No new installer option. `EMAIL_FROM` set in the
dashboard is a change outside Void, and the next upgrade asks before it
replaces it. |
## Behavior changes to note in the release
- `void platform upgrade|repair|enable|rollback` and `install --resume`
stop before they change a platform Worker or pause traffic when a
platform Worker was changed in the Cloudflare dashboard or API. In a
terminal they ask first. In CI, run the command once in a terminal.
- These commands, and their `--plan`, also stop when Cloudflare cannot
answer for a platform Worker after 2 retries, or when a platform Worker
has no single version at 100% of traffic.
- With platform email on, these commands and a fresh install check the
runtime token's Email Routing permissions before they pause traffic.
This includes Account → Email Routing Addresses: main's docs already
listed it, but a repeat upgrade did not use it, so a token without it
now stops the upgrade.
- Self-hosted platform mail sends from `noreply@<mail domain>` by
default. `EMAIL_FROM` is optional. A platform that had email on but no
`EMAIL_FROM` starts to send mail after the upgrade: invitations report
"sent", and the first error alert per project can cover all errors since
its alerts started to fail.
## Known limits
- cf's strict mode also asks about a remote secret with the same name as
a Void value, and about a workflow that another Worker owns. These do
not change `last_deployed_from`, so Void cannot see them before the
gate. The error after the gate now says what to delete.
- A `dash` Worker whose diff is add-only now stops CI, where main
uploaded it without asking.
- The email check sends read requests only. A token with Read instead of
Edit access to Email Routing, or without Email Sending, passes it and
fails after the pause; the error then names the permission. A safe write
probe does not exist (an address POST sends mail).
- The database lifecycle lock can add a column to its own table before
the check runs. So the stop text says "No platform Worker changed", not
"Nothing changed".
- A run that reuses the active version (no upload) stops CI when the
marker is `dash` or `api`, for example a repair after a dashboard
rollback to Void's own version. Main did not run cf on that path, so it
passed.
- A platform release that adds a Durable Object class with a binding
cannot upload to an existing platform Worker (same on main): an
undeployed version cannot bind a class that Cloudflare has not created
yet. No release does this yet. When a Worker was changed outside Void,
the review upload then fails before the pause, so traffic is not paused.
The deploy path needs its own change.
- `install --resume` and `--plan` cannot read the database before the
run. During the one-time change to configurable sign-in, they can ask
once more than needed. They never stop after the pause.
- Not checked live: a real dashboard edit (`dash`), and whether Void's
custom-domain attach changes the marker (the test account has no zone).
cf aborts on `api` with the same rule when Void uploads, so an `api`
marker adds a new stop only on the reuse path above.
## Testing
| Check | Result |
| --- | --- |
| `vp run test` (changed-file selection) | 550 files passed, 1 skipped;
11371 tests passed (before the last fix; focused platform files after
it: 493 passed) |
| Focused files (outside changes, installer, cutover, management,
lifecycle, setup flow, errors, provision, operation process, api email)
| all pass; each new test fails without its change |
| `vp run check`, `vp lint`, `vp fmt --check`, `vp run build:core` |
pass |
| Live, throwaway Workers on the e2e account (all deleted) | cf under a
PTY: answer No keeps `api`; Yes uploads an undeployed version and resets
the marker to `cf_cli`; consent then deploy succeeds; API settings PATCH
removes the Void record; Void's own operations keep `cf_cli` |
| Live, read-only email probes | all four GETs answer 200 on a mail
zone; unknown zone and invalid token are reported as expected |
| Real cf 1.0.0-beta.17 against a mock API | marker, secret and workflow
aborts give the new messages |
| PTY with expect | Ctrl-C at the cf prompt ends the run with no gate
write and no leftover child |
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_0123r5HnanmW2VUrZ7VWDufH
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>1 parent 134a2ee commit 5802901
32 files changed
Lines changed: 3748 additions & 74 deletions
File tree
- docs
- guide
- email
- platform
- administration
- installation
- reference/cli
- packages
- deploy-cloudflare/src
- void
- src/cli
- test/unit
- platform/packages/api
- src
- test/unit
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
37 | | - | |
| 37 | + | |
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
30 | 36 | | |
31 | 37 | | |
32 | 38 | | |
| |||
114 | 120 | | |
115 | 121 | | |
116 | 122 | | |
117 | | - | |
| 123 | + | |
118 | 124 | | |
119 | 125 | | |
120 | 126 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
51 | 51 | | |
52 | 52 | | |
53 | 53 | | |
| 54 | + | |
| 55 | + | |
54 | 56 | | |
55 | 57 | | |
56 | 58 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
68 | 68 | | |
69 | 69 | | |
70 | 70 | | |
71 | | - | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
72 | 90 | | |
73 | 91 | | |
74 | 92 | | |
75 | 93 | | |
76 | 94 | | |
77 | 95 | | |
78 | 96 | | |
79 | | - | |
| 97 | + | |
80 | 98 | | |
81 | | - | |
| 99 | + | |
82 | 100 | | |
83 | | - | |
| 101 | + | |
84 | 102 | | |
85 | | - | |
| 103 | + | |
86 | 104 | | |
87 | 105 | | |
88 | 106 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
113 | 113 | | |
114 | 114 | | |
115 | 115 | | |
116 | | - | |
| 116 | + | |
117 | 117 | | |
118 | | - | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
119 | 123 | | |
120 | 124 | | |
121 | 125 | | |
| |||
141 | 145 | | |
142 | 146 | | |
143 | 147 | | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
144 | 174 | | |
145 | 175 | | |
146 | 176 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
260 | 260 | | |
261 | 261 | | |
262 | 262 | | |
263 | | - | |
| 263 | + | |
264 | 264 | | |
265 | 265 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
137 | 137 | | |
138 | 138 | | |
139 | 139 | | |
| 140 | + | |
| 141 | + | |
140 | 142 | | |
141 | 143 | | |
142 | 144 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
43 | 48 | | |
44 | 49 | | |
45 | 50 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
105 | 105 | | |
106 | 106 | | |
107 | 107 | | |
| 108 | + | |
108 | 109 | | |
109 | 110 | | |
110 | 111 | | |
| |||
116 | 117 | | |
117 | 118 | | |
118 | 119 | | |
119 | | - | |
| 120 | + | |
| 121 | + | |
120 | 122 | | |
121 | 123 | | |
122 | 124 | | |
| |||
1060 | 1062 | | |
1061 | 1063 | | |
1062 | 1064 | | |
| 1065 | + | |
| 1066 | + | |
1063 | 1067 | | |
1064 | 1068 | | |
1065 | | - | |
| 1069 | + | |
| 1070 | + | |
1066 | 1071 | | |
1067 | 1072 | | |
1068 | 1073 | | |
| |||
1272 | 1277 | | |
1273 | 1278 | | |
1274 | 1279 | | |
| 1280 | + | |
| 1281 | + | |
| 1282 | + | |
| 1283 | + | |
| 1284 | + | |
| 1285 | + | |
1275 | 1286 | | |
1276 | 1287 | | |
1277 | | - | |
| 1288 | + | |
1278 | 1289 | | |
1279 | 1290 | | |
1280 | 1291 | | |
| |||
1303 | 1314 | | |
1304 | 1315 | | |
1305 | 1316 | | |
1306 | | - | |
| 1317 | + | |
1307 | 1318 | | |
1308 | 1319 | | |
1309 | 1320 | | |
| |||
0 commit comments