diff --git a/.env.example b/.env.example index 2366c00..b8b94d2 100644 --- a/.env.example +++ b/.env.example @@ -1,24 +1,28 @@ -# Runtime / app +# SCOREKO Configuration File Template +# Copy this file to '.env' in the application root and edit as needed. + +# Application Information (Required) SCOREKO_APP_TITLE=Scoreko SCOREKO_APP_USER_MODEL_ID=com.scoreko.desktop SCOREKO_APP_USER_DATA_DIRECTORY=scoreko -# SCOREKO_APP_ICON_PATH=static/icons/icon.ico +SCOREKO_APP_ICON_PATH=static/icons/icon.ico -# NodeCG +# NodeCG Managed Runtime Configuration (Required) NODECG_BUNDLE_NAME=scoreko-dev NODECG_PORT=9090 SCOREKO_DASHBOARD_ROUTE=dashboard/scoreko-dev/main.html?standalone=true SCOREKO_LOADING_ROUTE=dashboard/loading/main.html?standalone=true -# Timing +# Timing & Lifecycles (Required) ELECTRON_LOAD_DELAY_MS=10000 NODECG_STARTUP_TIMEOUT_MS=120000 NODECG_KILL_TIMEOUT_MS=2500 -# Updates +# Automated Updates Configuration (Required) SCOREKO_UPDATES_ENABLED=true -# SCOREKO_UPDATE_API_URL=http://gitea.local/api/v1/repos/OWNER/REPO/releases/latest -# SCOREKO_UPDATE_RELEASE_PAGE_URL=http://gitea.local/OWNER/REPO/releases -SCOREKO_UPDATE_ASSET_PATTERN=Scoreko-setup-.*\.exe$ SCOREKO_UPDATE_CHECK_DELAY_MS=5000 -# SCOREKO_UPDATE_CONFIG_PATH=static/updates.json + +# Optional Update Release Source (Only required if SCOREKO_UPDATES_ENABLED is true) +SCOREKO_UPDATE_API_URL=http://gitea.local/api/v1/repos/OWNER/REPO/releases/latest +SCOREKO_UPDATE_RELEASE_PAGE_URL=http://gitea.local/OWNER/REPO/releases +SCOREKO_UPDATE_ASSET_PATTERN=Scoreko-setup-.*\.exe$ diff --git a/src/main/app/paths.ts b/src/main/app/paths.ts index 6e4b8b6..97034a1 100644 --- a/src/main/app/paths.ts +++ b/src/main/app/paths.ts @@ -27,10 +27,6 @@ export function getSourceNodecgRuntimePath(rootPath: string): string { return path.resolve(rootPath, "lib", "nodecg"); } -export function getDefaultUpdateConfigPath(rootPath: string): string { - return path.join(rootPath, "static", "updates.json"); -} - export function getUpdateDownloadDirectory(tempDirectory: string): string { return path.join(tempDirectory, "scoreko-updates"); } diff --git a/src/main/config/runtime-config.ts b/src/main/config/runtime-config.ts index 8e976bc..372a827 100644 --- a/src/main/config/runtime-config.ts +++ b/src/main/config/runtime-config.ts @@ -16,7 +16,6 @@ export type AppRuntimeConfig = { updateApiUrl?: string; updateReleasePageUrl?: string; updateAssetPattern?: string; - updateConfigPathOverride?: string; updateCheckDelayMs: number; }; @@ -55,7 +54,6 @@ export function getRuntimeConfig(): AppRuntimeConfig { updateApiUrl: parseOptionalHttpUrl("SCOREKO_UPDATE_API_URL"), updateReleasePageUrl: parseOptionalHttpUrl("SCOREKO_UPDATE_RELEASE_PAGE_URL"), updateAssetPattern: getOptionalEnv("SCOREKO_UPDATE_ASSET_PATTERN"), - updateConfigPathOverride: getOptionalEnv("SCOREKO_UPDATE_CONFIG_PATH"), updateCheckDelayMs: parseRequiredEnvIntInRange("SCOREKO_UPDATE_CHECK_DELAY_MS", 0, 600000), }; } diff --git a/src/main/updates/update-config.ts b/src/main/updates/update-config.ts index 2852327..cefafdf 100644 --- a/src/main/updates/update-config.ts +++ b/src/main/updates/update-config.ts @@ -1,9 +1,6 @@ -import fs from "node:fs"; - -import { getDefaultUpdateConfigPath } from "../app/paths"; import { AppRuntimeConfig } from "../config/runtime-config"; -import { isRecord, readNonEmptyString } from "../utils/unknown-values"; -import { UpdateFileConfig, validateHttpUrl } from "./update-schema"; +import { readNonEmptyString } from "../utils/unknown-values"; +import { validateHttpUrl } from "./update-schema"; const DEFAULT_UPDATE_ASSET_PATTERN = "Scoreko-setup-.*\\.exe$"; @@ -20,11 +17,7 @@ type UpdateConfigOptions = { type UpdateRuntimeConfig = Pick< AppRuntimeConfig, - | "updateApiUrl" - | "updateAssetPattern" - | "updateConfigPathOverride" - | "updateReleasePageUrl" - | "updatesEnabled" + "updateApiUrl" | "updateAssetPattern" | "updateReleasePageUrl" | "updatesEnabled" >; export function loadUpdateSettings( @@ -33,49 +26,14 @@ export function loadUpdateSettings( log: (...args: unknown[]) => void, options: UpdateConfigOptions = { allowInsecureHttp: true }, ): UpdateSettings { - const fileConfig = readUpdateFileConfig(appConfig, rootPath, log); - const apiUrl = readOptionalHttpUrl(appConfig.updateApiUrl ?? fileConfig.apiUrl, options); - const releasePageUrl = readOptionalHttpUrl(appConfig.updateReleasePageUrl ?? fileConfig.releasePageUrl, options); + const apiUrl = readOptionalHttpUrl(appConfig.updateApiUrl, options); + const releasePageUrl = readOptionalHttpUrl(appConfig.updateReleasePageUrl, options); return { - enabled: appConfig.updatesEnabled && (Boolean(fileConfig.enabled) || Boolean(appConfig.updateApiUrl)) && Boolean(apiUrl), + enabled: appConfig.updatesEnabled && Boolean(apiUrl), ...(apiUrl ? { apiUrl } : {}), ...(releasePageUrl ? { releasePageUrl } : {}), - assetPattern: - appConfig.updateAssetPattern || readNonEmptyString(fileConfig.assetPattern) || DEFAULT_UPDATE_ASSET_PATTERN, - }; -} - -export function readUpdateFileConfig( - appConfig: Pick, - rootPath: string, - log: (...args: unknown[]) => void, -): UpdateFileConfig { - const configPath = appConfig.updateConfigPathOverride ?? getDefaultUpdateConfigPath(rootPath); - - if (!fs.existsSync(configPath)) { - return {}; - } - - try { - const parsedConfig: unknown = JSON.parse(fs.readFileSync(configPath, "utf8")); - return normalizeUpdateFileConfig(parsedConfig); - } catch (error) { - log(`Could not read update config at ${configPath}.`, error); - return {}; - } -} - -function normalizeUpdateFileConfig(value: unknown): UpdateFileConfig { - if (!isRecord(value)) { - return {}; - } - - return { - enabled: value.enabled, - apiUrl: value.apiUrl, - releasePageUrl: value.releasePageUrl, - assetPattern: value.assetPattern, + assetPattern: appConfig.updateAssetPattern || DEFAULT_UPDATE_ASSET_PATTERN, }; } diff --git a/src/main/updates/update-schema.ts b/src/main/updates/update-schema.ts index 10e3172..da8878f 100644 --- a/src/main/updates/update-schema.ts +++ b/src/main/updates/update-schema.ts @@ -26,13 +26,6 @@ export type ReleaseUpdate = { installer: InstallerAsset; }; -export type UpdateFileConfig = { - enabled?: unknown; - apiUrl?: unknown; - releasePageUrl?: unknown; - assetPattern?: unknown; -}; - type UrlPolicy = { allowInsecureHttp: boolean; }; diff --git a/src/tests/app-paths.test.ts b/src/tests/app-paths.test.ts index f8da921..a0f21e8 100644 --- a/src/tests/app-paths.test.ts +++ b/src/tests/app-paths.test.ts @@ -5,7 +5,6 @@ import test from "node:test"; import { getApplicationPaths, getDashboardUrl, - getDefaultUpdateConfigPath, getManagedNodecgRuntimePath, getNodecgBaseUrl, getRootPath, @@ -23,7 +22,6 @@ test("app path helpers build deterministic development paths and URLs", () => { assert.equal(getSourceNodecgRuntimePath(rootPath), path.resolve(rootPath, "lib", "nodecg")); assert.equal(getUserDataPath("/app-data", "scoreko"), path.join("/app-data", "scoreko")); assert.equal(getManagedNodecgRuntimePath("/app-data/scoreko"), path.join("/app-data/scoreko", "nodecg")); - assert.equal(getDefaultUpdateConfigPath(rootPath), path.join(rootPath, "static", "updates.json")); assert.equal(getUpdateDownloadDirectory("/tmp"), path.join("/tmp", "scoreko-updates")); assert.equal(getNodecgBaseUrl("9090"), "http://127.0.0.1:9090"); assert.equal( diff --git a/src/tests/update-config.test.ts b/src/tests/update-config.test.ts index 8462936..ab1751e 100644 --- a/src/tests/update-config.test.ts +++ b/src/tests/update-config.test.ts @@ -1,11 +1,8 @@ import assert from "node:assert/strict"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; import test from "node:test"; import { AppRuntimeConfig } from "../main/config/runtime-config"; -import { loadUpdateSettings, readUpdateFileConfig } from "../main/updates/update-config"; +import { loadUpdateSettings } from "../main/updates/update-config"; const baseConfig: AppRuntimeConfig = { title: "Scoreko", @@ -23,37 +20,36 @@ const baseConfig: AppRuntimeConfig = { }; test("loadUpdateSettings keeps updates disabled when the runtime config disables them", () => { - const rootPath = makeTempRoot({ - enabled: true, - apiUrl: "https://gitea.local/releases/latest", - }); - - const settings = loadUpdateSettings({ ...baseConfig, updatesEnabled: false }, rootPath, () => undefined); + const settings = loadUpdateSettings( + { + ...baseConfig, + updatesEnabled: false, + updateApiUrl: "https://gitea.local/releases/latest", + }, + "", + () => undefined, + ); assert.equal(settings.enabled, false); assert.equal(settings.apiUrl, "https://gitea.local/releases/latest"); }); test("loadUpdateSettings fails closed on insecure production update URLs", () => { - const rootPath = makeTempRoot({ - enabled: true, - apiUrl: "http://gitea.local/releases/latest", - }); - - const settings = loadUpdateSettings(baseConfig, rootPath, () => undefined, { allowInsecureHttp: false }); + const settings = loadUpdateSettings( + { + ...baseConfig, + updateApiUrl: "http://gitea.local/releases/latest", + }, + "", + () => undefined, + { allowInsecureHttp: false }, + ); assert.equal(settings.enabled, false); assert.equal(settings.apiUrl, undefined); }); -test("loadUpdateSettings lets runtime config override file settings", () => { - const rootPath = makeTempRoot({ - enabled: true, - apiUrl: "https://file.local/releases/latest", - releasePageUrl: "https://file.local/releases", - assetPattern: "File-.*\\.exe$", - }); - +test("loadUpdateSettings lets runtime config specify settings", () => { const settings = loadUpdateSettings( { ...baseConfig, @@ -61,7 +57,7 @@ test("loadUpdateSettings lets runtime config override file settings", () => { updateReleasePageUrl: "https://env.local/releases", updateAssetPattern: "Env-.*\\.exe$", }, - rootPath, + "", () => undefined, ); @@ -72,34 +68,3 @@ test("loadUpdateSettings lets runtime config override file settings", () => { assetPattern: "Env-.*\\.exe$", }); }); - -test("readUpdateFileConfig normalizes malformed config into an empty file config", () => { - const rootPath = makeTempRoot(["not", "an", "object"]); - - assert.deepEqual(readUpdateFileConfig(baseConfig, rootPath, () => undefined), {}); -}); - -test("readUpdateFileConfig logs invalid JSON and returns an empty file config", () => { - const rootPath = fs.mkdtempSync(path.join(os.tmpdir(), "scoreko-update-config-")); - const staticPath = path.join(rootPath, "static"); - fs.mkdirSync(staticPath, { recursive: true }); - fs.writeFileSync(path.join(staticPath, "updates.json"), "{ invalid", "utf8"); - const messages: unknown[][] = []; - - const settings = readUpdateFileConfig(baseConfig, rootPath, (...args: unknown[]) => { - messages.push(args); - }); - - assert.deepEqual(settings, {}); - assert.equal(messages.length, 1); -}); - -function makeTempRoot(config: unknown): string { - const rootPath = fs.mkdtempSync(path.join(os.tmpdir(), "scoreko-update-config-")); - const staticPath = path.join(rootPath, "static"); - - fs.mkdirSync(staticPath, { recursive: true }); - fs.writeFileSync(path.join(staticPath, "updates.json"), JSON.stringify(config), "utf8"); - - return rootPath; -} diff --git a/static/updates.json b/static/updates.json deleted file mode 100644 index c7bc959..0000000 --- a/static/updates.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "enabled": false, - "apiUrl": "http://gitea.local/api/v1/repos/OWNER/REPO/releases/latest", - "releasePageUrl": "http://gitea.local/OWNER/REPO/releases", - "assetPattern": "Scoreko-setup-.*\\.exe$" -}