-
Notifications
You must be signed in to change notification settings - Fork 1.6k
feat: add mock update server #1180
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3c8df42
7bfbf41
78b7efc
7f0c421
73a29d8
a9833aa
82b119c
106c0cd
0860bb4
907c8d9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,7 @@ packages/*/dist | |
| build/ | ||
| .logs/ | ||
| release/ | ||
| release-mock/ | ||
| .t3 | ||
| .idea/ | ||
| apps/web/.playwright | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -74,6 +74,8 @@ interface BuildCliInput { | |
| readonly keepStage: Option.Option<boolean>; | ||
| readonly signed: Option.Option<boolean>; | ||
| readonly verbose: Option.Option<boolean>; | ||
| readonly mockUpdates: Option.Option<boolean>; | ||
| readonly mockUpdateServerPort: Option.Option<string>; | ||
| } | ||
|
|
||
| function detectHostBuildPlatform(hostPlatform: string): typeof BuildPlatform.Type | undefined { | ||
|
|
@@ -162,6 +164,8 @@ interface ResolvedBuildOptions { | |
| readonly keepStage: boolean; | ||
| readonly signed: boolean; | ||
| readonly verbose: boolean; | ||
| readonly mockUpdates: boolean; | ||
| readonly mockUpdateServerPort: string | undefined; | ||
| } | ||
|
|
||
| interface StagePackageJson { | ||
|
|
@@ -204,6 +208,8 @@ const BuildEnvConfig = Config.all({ | |
| keepStage: Config.boolean("T3CODE_DESKTOP_KEEP_STAGE").pipe(Config.withDefault(false)), | ||
| signed: Config.boolean("T3CODE_DESKTOP_SIGNED").pipe(Config.withDefault(false)), | ||
| verbose: Config.boolean("T3CODE_DESKTOP_VERBOSE").pipe(Config.withDefault(false)), | ||
| mockUpdates: Config.boolean("T3CODE_DESKTOP_MOCK_UPDATES").pipe(Config.withDefault(false)), | ||
| mockUpdateServerPort: Config.string("T3CODE_DESKTOP_MOCK_UPDATE_SERVER_PORT").pipe(Config.option), | ||
| }); | ||
|
|
||
| const resolveBooleanFlag = (flag: Option.Option<boolean>, envValue: boolean) => | ||
|
|
@@ -231,13 +237,26 @@ const resolveBuildOptions = Effect.fn("resolveBuildOptions")(function* (input: B | |
| const target = mergeOptions(input.target, env.target, PLATFORM_CONFIG[platform].defaultTarget); | ||
| const arch = mergeOptions(input.arch, env.arch, getDefaultArch(platform)); | ||
| const version = mergeOptions(input.buildVersion, env.version, undefined); | ||
| const outputDir = path.resolve(repoRoot, mergeOptions(input.outputDir, env.outputDir, "release")); | ||
| const releaseDir = resolveBooleanFlag(input.mockUpdates, env.mockUpdates) | ||
| ? "release-mock" | ||
| : "release"; | ||
| const outputDir = path.resolve( | ||
| repoRoot, | ||
| mergeOptions(input.outputDir, env.outputDir, releaseDir), | ||
| ); | ||
|
|
||
| const skipBuild = resolveBooleanFlag(input.skipBuild, env.skipBuild); | ||
| const keepStage = resolveBooleanFlag(input.keepStage, env.keepStage); | ||
| const signed = resolveBooleanFlag(input.signed, env.signed); | ||
| const verbose = resolveBooleanFlag(input.verbose, env.verbose); | ||
|
|
||
| const mockUpdates = resolveBooleanFlag(input.mockUpdates, env.mockUpdates); | ||
| const mockUpdateServerPort = mergeOptions( | ||
| input.mockUpdateServerPort, | ||
| env.mockUpdateServerPort, | ||
| undefined, | ||
| ); | ||
|
|
||
| return { | ||
| platform, | ||
| target, | ||
|
|
@@ -248,6 +267,8 @@ const resolveBuildOptions = Effect.fn("resolveBuildOptions")(function* (input: B | |
| keepStage, | ||
| signed, | ||
| verbose, | ||
| mockUpdates, | ||
| mockUpdateServerPort, | ||
| } satisfies ResolvedBuildOptions; | ||
| }); | ||
|
|
||
|
|
@@ -447,6 +468,8 @@ const createBuildConfig = Effect.fn("createBuildConfig")(function* ( | |
| target: string, | ||
| productName: string, | ||
| signed: boolean, | ||
| mockUpdates: boolean, | ||
| mockUpdateServerPort: string | undefined, | ||
| ) { | ||
| const buildConfig: Record<string, unknown> = { | ||
| appId: "com.t3tools.t3code", | ||
|
|
@@ -459,6 +482,13 @@ const createBuildConfig = Effect.fn("createBuildConfig")(function* ( | |
| const publishConfig = resolveGitHubPublishConfig(); | ||
| if (publishConfig) { | ||
| buildConfig.publish = [publishConfig]; | ||
| } else if (mockUpdates) { | ||
| buildConfig.publish = [ | ||
| { | ||
| provider: "generic", | ||
| url: `http://localhost:${mockUpdateServerPort ?? 3000}`, | ||
| }, | ||
| ]; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mock updates config ignored when GitHub config existsMedium Severity At build time, the Additional Locations (1)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The existence of a GitHub publish config and a mock update path are mutually exclusive. This is desired behavior. This conflict would only happen if someone were to pass |
||
| } | ||
|
|
||
| if (platform === "mac") { | ||
|
|
@@ -631,6 +661,8 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( | |
| options.target, | ||
| desktopPackageJson.productName ?? "T3 Code", | ||
| options.signed, | ||
| options.mockUpdates, | ||
| options.mockUpdateServerPort, | ||
| ), | ||
| dependencies: { | ||
| ...resolvedServerDependencies, | ||
|
|
@@ -769,6 +801,14 @@ const buildDesktopArtifactCli = Command.make("build-desktop-artifact", { | |
| Flag.withDescription("Stream subprocess stdout (env: T3CODE_DESKTOP_VERBOSE)."), | ||
| Flag.optional, | ||
| ), | ||
| mockUpdates: Flag.boolean("mock-updates").pipe( | ||
| Flag.withDescription("Enable mock updates (env: T3CODE_DESKTOP_MOCK_UPDATES)."), | ||
| Flag.optional, | ||
| ), | ||
| mockUpdateServerPort: Flag.string("mock-update-server-port").pipe( | ||
| Flag.withDescription("Mock update server port (env: T3CODE_DESKTOP_MOCK_UPDATE_SERVER_PORT)."), | ||
| Flag.optional, | ||
| ), | ||
| }).pipe( | ||
| Command.withDescription("Build a desktop artifact for T3 Code."), | ||
| Command.withHandler((input) => Effect.flatMap(resolveBuildOptions(input), buildDesktopArtifact)), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import { resolve, relative } from "node:path"; | ||
| import { realpathSync } from "node:fs"; | ||
|
|
||
| const port = Number(process.env.T3CODE_DESKTOP_MOCK_UPDATE_SERVER_PORT ?? 3000); | ||
| const root = | ||
| process.env.T3CODE_DESKTOP_MOCK_UPDATE_SERVER_ROOT ?? | ||
| resolve(import.meta.dirname, "..", "release-mock"); | ||
|
|
||
| const mockServerLog = (level: "info" | "warn" | "error" = "info", message: string) => { | ||
| console[level](`[mock-update-server] ${message}`); | ||
| }; | ||
|
|
||
| function isWithinRoot(filePath: string): boolean { | ||
| try { | ||
| return !relative(realpathSync(root), realpathSync(filePath)).startsWith("."); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Path containment check incorrectly blocks dotfilesLow Severity The
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The update server does not make use of dotfiles. This should be fine. |
||
| } catch (error) { | ||
| mockServerLog("error", `Error checking if file is within root: ${error}`); | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| Bun.serve({ | ||
| port, | ||
| hostname: "localhost", | ||
| fetch: async (request) => { | ||
| const url = new URL(request.url); | ||
| const path = url.pathname; | ||
| mockServerLog("info", `Request received for path: ${path}`); | ||
| const filePath = resolve(root, `.${path}`); | ||
| if (!isWithinRoot(filePath)) { | ||
| mockServerLog("warn", `Attempted to access file outside of root: ${filePath}`); | ||
| return new Response("Not Found", { status: 404 }); | ||
| } | ||
| const file = Bun.file(filePath); | ||
| if (!(await file.exists())) { | ||
| mockServerLog("warn", `Attempted to access non-existent file: ${filePath}`); | ||
| return new Response("Not Found", { status: 404 }); | ||
| } | ||
| mockServerLog("info", `Serving file: ${filePath}`); | ||
| return new Response(file.stream()); | ||
| }, | ||
| }); | ||
|
|
||
| mockServerLog("info", `running on http://localhost:${port}`); | ||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Truthy env check enables mock updates with "false"
Medium Severity
The runtime check
process.env.T3CODE_DESKTOP_MOCK_UPDATESis a truthy check, so setting it to"false"or"0"still enables mock updates, redirecting the auto-updater to a localhost server that likely isn't running. This silently breaks update checks. The codebase already uses strict comparison for similar env vars (e.g.,T3CODE_DISABLE_AUTO_UPDATE === "1"), and the build-time counterpart usesConfig.booleanwhich correctly parses"false"asfalse.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is expected behavior. Mock updates are opt-in, so passing
T3CODE_DESKTOP_MOCK_UPDATES=falseis not a sane, normal, or expected use of the packaging script.