Skip to content

Commit 9394217

Browse files
authored
Fix self-host/Cloudflare update card always showing; show version in dashboard (#1204)
The self-host and Cloudflare builds baked a placeholder VITE_APP_VERSION (0.0.0-selfhost / 0.0.0-cloudflare), so the update check always compared as "behind" and the card showed even on the latest version. Bake the real release version from the CLI package the same way apps/local does. Also surface the running version in the multiplayer shell sidebar footer so self-hosters can see what they are on. Adds a regression e2e: current build -> no card + version shown.
1 parent 9586e68 commit 9394217

7 files changed

Lines changed: 107 additions & 4 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@executor-js/host-selfhost": patch
3+
"@executor-js/host-cloudflare": patch
4+
"@executor-js/react": patch
5+
---
6+
7+
Fix the self-host and Cloudflare web dashboards showing "update available" even on the latest version. The builds baked a placeholder version (`0.0.0-selfhost` / `0.0.0-cloudflare`) into the shell, so the update check always compared as behind. They now bake the real release version, and the sidebar footer shows the running version so you can see what you are on.

apps/host-cloudflare/vite.config.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { readFileSync } from "node:fs";
12
import { fileURLToPath } from "node:url";
23

34
import { defineConfig } from "vite";
@@ -8,6 +9,15 @@ import executorVitePlugin from "@executor-js/vite-plugin";
89

910
import { routes } from "./tsr.routes";
1011

12+
// The real release version (matches the published `executor` dist-tags the
13+
// update card compares against). A placeholder here made the card read as
14+
// permanently "behind".
15+
// oxlint-disable-next-line executor/no-json-parse -- boundary: Vite config reads the version from package.json at build time
16+
const cliPackage = JSON.parse(
17+
readFileSync(new URL("../cli/package.json", import.meta.url), "utf8"),
18+
) as { version?: string };
19+
const EXECUTOR_VERSION = cliPackage.version;
20+
1121
// ---------------------------------------------------------------------------
1222
// Cloudflare web SPA. The SAME shared @executor-js/react shell + pages as cloud
1323
// and self-host; the TanStack router codegen points at THIS app's routes
@@ -36,7 +46,7 @@ export default defineConfig({
3646
dedupe: ["react", "react-dom"],
3747
},
3848
define: {
39-
"import.meta.env.VITE_APP_VERSION": JSON.stringify("0.0.0-cloudflare"),
49+
"import.meta.env.VITE_APP_VERSION": JSON.stringify(EXECUTOR_VERSION ?? "0.0.0"),
4050
// Cloudflare upgrades by redeploying the Worker (wrangler deploy), not npm,
4151
// so the update card links to the upgrade guide instead of a command.
4252
"import.meta.env.VITE_UPGRADE_HINT": JSON.stringify("cloudflare"),

apps/host-selfhost/vite.config.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { readFileSync } from "node:fs";
12
import { Readable } from "node:stream";
23
import { fileURLToPath } from "node:url";
34

@@ -10,6 +11,16 @@ import executorVitePlugin from "@executor-js/vite-plugin";
1011
import { routes } from "./tsr.routes";
1112
import { stripMcpOrgSegment } from "./src/mcp/org-path";
1213

14+
// The real release version (matches the published `executor` dist-tags the
15+
// update card compares against), read from the CLI package the same way
16+
// apps/local does. A placeholder here made the card read as permanently
17+
// "behind", see the update-check comparison in @executor-js/react.
18+
// oxlint-disable-next-line executor/no-json-parse -- boundary: Vite config reads the version from package.json at build time
19+
const cliPackage = JSON.parse(
20+
readFileSync(new URL("../cli/package.json", import.meta.url), "utf8"),
21+
) as { version?: string };
22+
const EXECUTOR_VERSION = cliPackage.version;
23+
1324
// Self-host web SPA. Mirrors @executor-js/app's vite plugin bundle, but points
1425
// the TanStack router codegen at THIS app's routes (web/routes) so we get the
1526
// multiplayer shell + Better-Auth gate (routes/__root.tsx) instead of the
@@ -140,7 +151,7 @@ export default defineConfig({
140151
dedupe: ["react", "react-dom"],
141152
},
142153
define: {
143-
"import.meta.env.VITE_APP_VERSION": JSON.stringify("0.0.0-selfhost"),
154+
"import.meta.env.VITE_APP_VERSION": JSON.stringify(EXECUTOR_VERSION ?? "0.0.0"),
144155
// Self-host upgrades by pulling/rebuilding the image (or git + rebuild), not
145156
// npm, so the update card links to the upgrade guide instead of a command.
146157
"import.meta.env.VITE_UPGRADE_HINT": JSON.stringify("selfhost"),
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
// Cloudflare-only: the update card paints on the Cloudflare-served web shell
22
// (Workers Static Assets + the Worker route). See ../src/update-card-render.ts
33
// for the shared body.
4-
import { registerUpdateCardRenderScenario } from "../src/update-card-render";
4+
import {
5+
registerUpdateCardCurrentScenario,
6+
registerUpdateCardRenderScenario,
7+
} from "../src/update-card-render";
58

69
registerUpdateCardRenderScenario(
710
"Cloudflare · the web shell sidebar surfaces the update-available card",
811
);
12+
13+
registerUpdateCardCurrentScenario(
14+
"Cloudflare · no update card when current, and the footer shows the version",
15+
);
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
// Selfhost-only (runs on the dev server AND the production Docker image): the
22
// update card paints on the self-host web shell. See
33
// ../src/update-card-render.ts for the shared body.
4-
import { registerUpdateCardRenderScenario } from "../src/update-card-render";
4+
import {
5+
registerUpdateCardCurrentScenario,
6+
registerUpdateCardRenderScenario,
7+
} from "../src/update-card-render";
58

69
registerUpdateCardRenderScenario(
710
"Selfhost · the web shell sidebar surfaces the update-available card",
811
);
12+
13+
registerUpdateCardCurrentScenario(
14+
"Selfhost · no update card when current, and the footer shows the version",
15+
);

e2e/src/update-card-render.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,52 @@ export const registerUpdateCardRenderScenario = (name: string): void =>
5656
});
5757
}),
5858
);
59+
60+
// Regression for the "card always shows on Docker even on the latest version"
61+
// report: a placeholder build version (0.0.0-selfhost / 0.0.0-cloudflare) made
62+
// the comparison permanently "behind". With the real version baked in, a
63+
// published version that is NOT newer must leave the card hidden, and the
64+
// sidebar footer must show the running version (the other half of the report).
65+
export const registerUpdateCardCurrentScenario = (name: string): void =>
66+
scenario(
67+
name,
68+
{ timeout: 120_000 },
69+
Effect.gen(function* () {
70+
const target = yield* Target;
71+
const browser = yield* Browser;
72+
const identity = yield* target.newIdentity();
73+
74+
yield* browser.session(identity, async ({ page, step }) => {
75+
// Report a published version OLDER than this build, i.e. the build is
76+
// current. With the placeholder version this still showed the card.
77+
await page.route("**/v1/app/npm/dist-tags", (route) =>
78+
route.fulfill({
79+
contentType: "application/json",
80+
body: JSON.stringify({ latest: "0.0.1", beta: "0.0.1-beta.1" }),
81+
}),
82+
);
83+
84+
await step("Open the console", async () => {
85+
await page.goto("/", { waitUntil: "networkidle" });
86+
await page.getByRole("heading", { name: "Integrations" }).waitFor({ timeout: 60_000 });
87+
});
88+
89+
await step("No update card, and the footer shows the running version", async () => {
90+
// The footer version proves the build injects a real semver, not a
91+
// placeholder, which is what makes the comparison correct.
92+
const version = page
93+
.locator("aside")
94+
.getByText(/^v\d+\.\d+\.\d+/)
95+
.first();
96+
await version.waitFor({ timeout: 10_000 });
97+
// The mocked update check resolves on mount; give the state a beat to
98+
// settle, then assert the card stayed hidden.
99+
await page.waitForTimeout(750);
100+
expect(
101+
await page.getByText("Update available").count(),
102+
"no update card when the build is current",
103+
).toBe(0);
104+
});
105+
});
106+
}),
107+
);

packages/react/src/multiplayer/shell.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,13 @@ export const defaultShellNavItems: ReadonlyArray<ShellNavItem> = [
5959
* via {@link ShellProps.docsUrl}. */
6060
export const DEFAULT_DOCS_URL = "https://executor.sh/docs";
6161

62+
// The build version, surfaced in the sidebar footer so self-hosters can see
63+
// what they're running (and sanity-check the update card). Undefined on builds
64+
// that don't inject it (e.g. cloud) — then the line is hidden.
65+
const APP_VERSION = (
66+
import.meta as ImportMeta & { readonly env?: { readonly VITE_APP_VERSION?: string } }
67+
).env?.VITE_APP_VERSION;
68+
6269
// Scope-relative path -> the route id under the optional org segment
6370
// ("/" -> "/{-$orgSlug}", "/policies" -> "/{-$orgSlug}/policies"). Loosely
6471
// typed on purpose: host-specific items ("/admin", "/billing") resolve against
@@ -367,6 +374,11 @@ function SidebarContent(
367374

368375
<div className="shrink-0 border-t border-sidebar-border p-2">
369376
<DocsLink href={props.docsUrl ?? DEFAULT_DOCS_URL} onNavigate={props.onNavigate} />
377+
{APP_VERSION && (
378+
<p className="px-2.5 pt-1.5 font-mono text-[11px] text-muted-foreground tabular-nums">
379+
v{APP_VERSION}
380+
</p>
381+
)}
370382
</div>
371383

372384
{props.supportSlot && <div className="shrink-0 px-2 pb-2">{props.supportSlot}</div>}

0 commit comments

Comments
 (0)