diff --git a/.github/workflows/quality-gate.yml b/.github/workflows/quality-gate.yml index bb5c0d1d..ecf50aa8 100644 --- a/.github/workflows/quality-gate.yml +++ b/.github/workflows/quality-gate.yml @@ -60,8 +60,11 @@ jobs: - name: Install Lua run: | - sudo apt-get update - sudo apt-get install -y lua5.4 + # Lua needs only Ubuntu sources; unrelated runner repositories can be unavailable. + test -f /etc/apt/sources.list.d/ubuntu.sources + apt_sources=(-o Dir::Etc::sourcelist=sources.list.d/ubuntu.sources -o Dir::Etc::sourceparts=-) + sudo apt-get "${apt_sources[@]}" update + sudo apt-get "${apt_sources[@]}" install -y lua5.4 sudo ln -sf /usr/bin/lua5.4 /usr/local/bin/lua lua -v diff --git a/bun.lock b/bun.lock index fffea52b..b92837b2 100644 --- a/bun.lock +++ b/bun.lock @@ -34,14 +34,14 @@ "@discordjs/rest@2.6.1": "patches/@discordjs%2Frest@2.6.1.patch", }, "overrides": { - "@xmldom/xmldom": "0.8.13", + "@xmldom/xmldom": "0.8.15", "app-builder-lib": "26.15.3", "brace-expansion": "5.0.9", "electron-builder-squirrel-windows": "26.15.3", "fast-uri": "3.1.6", "form-data": "4.0.6", "ip-address": "10.2.0", - "js-yaml": "4.3.1", + "js-yaml": "4.3.2", "lodash": "4.18.0", "minimatch": "10.2.5", "picomatch": "4.0.4", @@ -226,7 +226,7 @@ "@xhayper/discord-rpc": ["@xhayper/discord-rpc@1.3.4", "", { "dependencies": { "@discordjs/rest": "^2.6.1", "@vladfrangu/async_event_emitter": "^2.4.7", "discord-api-types": "^0.38.47", "ws": "^8.20.0" } }, "sha512-ff0uEXuibh9wi+l4vOj7xInLUjtlTaQBje/SCyQkeXZ0j2V0y+Zge5PQIQFRHH9TjjGaYJkTofEcQhncM2q7/w=="], - "@xmldom/xmldom": ["@xmldom/xmldom@0.8.13", "", {}, "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw=="], + "@xmldom/xmldom": ["@xmldom/xmldom@0.8.15", "", {}, "sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA=="], "abbrev": ["abbrev@4.0.0", "", {}, "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA=="], @@ -498,7 +498,7 @@ "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], - "js-yaml": ["js-yaml@4.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="], + "js-yaml": ["js-yaml@4.3.2", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA=="], "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], diff --git a/changes/fix-first-launch-config-directory.md b/changes/fix-first-launch-config-directory.md new file mode 100644 index 00000000..b50c9ef0 --- /dev/null +++ b/changes/fix-first-launch-config-directory.md @@ -0,0 +1,4 @@ +type: fixed +area: startup + +- Fixed first launch exiting on macOS when the SubMiner config directory did not yet exist by creating it before acquiring the startup lock. diff --git a/docs/architecture/README.md b/docs/architecture/README.md index a0c4858a..02bd40a2 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -40,3 +40,7 @@ The desktop app keeps `src/main.ts` as composition root and pushes behavior into - Composition over monoliths - Pure helpers where possible - Stable user behavior while internals evolve + +Startup resolves and creates the user-data directory in `src/main-entry-runtime.ts` +before the entry process requests Electron's single-instance lock. Main-process +config bootstrap then writes the default config only when no config file exists. diff --git a/docs/workflow/verification.md b/docs/workflow/verification.md index 51d1b212..0c5c436c 100644 --- a/docs/workflow/verification.md +++ b/docs/workflow/verification.md @@ -23,6 +23,8 @@ Read when: selecting the right verification lane for a change there instead of copying them into caller workflows. - The reusable gate installs Lua and runs `bun run test:env`, so the shipped mpv plugin tests run for every pull request and tagged release. + Lua installation uses only the runner's Ubuntu package sources so unrelated + third-party repository failures do not block the gate. ## Default Handoff Gate diff --git a/package.json b/package.json index 6fad0758..1b67f725 100644 --- a/package.json +++ b/package.json @@ -83,14 +83,14 @@ "build:win:unsigned": "bun run build && node scripts/build-win-unsigned.mjs" }, "overrides": { - "@xmldom/xmldom": "0.8.13", + "@xmldom/xmldom": "0.8.15", "app-builder-lib": "26.15.3", "brace-expansion": "5.0.9", "electron-builder-squirrel-windows": "26.15.3", "fast-uri": "3.1.6", "form-data": "4.0.6", "ip-address": "10.2.0", - "js-yaml": "4.3.1", + "js-yaml": "4.3.2", "lodash": "4.18.0", "minimatch": "10.2.5", "picomatch": "4.0.4", diff --git a/src/main-entry-runtime.test.ts b/src/main-entry-runtime.test.ts index 7465c372..a6dcff24 100644 --- a/src/main-entry-runtime.test.ts +++ b/src/main-entry-runtime.test.ts @@ -585,24 +585,58 @@ test('shouldDetachBackgroundLaunch only for first background invocation', () => test('configureEarlyAppPaths pins userData to canonical SubMiner config dir', () => { const calls: string[] = []; - - const userDataPath = configureEarlyAppPaths( - { - setName: (name) => { - calls.push(`name:${name}`); + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-entry-paths-')); + const configDir = path.posix.join(tempDir, 'SubMiner'); + try { + const userDataPath = configureEarlyAppPaths( + { + setName: (name) => { + calls.push(`name:${name}`); + }, + setPath: (key, value) => { + calls.push(`path:${key}:${value}`); + }, }, - setPath: (key, value) => { - calls.push(`path:${key}:${value}`); + { + platform: 'linux', + homeDir: tempDir, + xdgConfigHome: tempDir, + existsSync: (candidate) => + candidate === path.posix.join(tempDir, 'subminer', 'config.jsonc'), }, - }, - { - platform: 'linux', - homeDir: '/home/tester', - xdgConfigHome: '/tmp/xdg', - existsSync: (candidate) => candidate === '/tmp/xdg/subminer/config.jsonc', - }, - ); + ); - assert.equal(userDataPath, '/tmp/xdg/SubMiner'); - assert.deepEqual(calls, ['name:SubMiner', 'path:userData:/tmp/xdg/SubMiner']); + assert.equal(userDataPath, configDir); + assert.deepEqual(calls, ['name:SubMiner', `path:userData:${configDir}`]); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } +}); + +test('configureEarlyAppPaths creates a fresh macOS config directory before Electron uses it', () => { + const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-entry-first-launch-')); + const configDir = path.posix.join(homeDir, '.config', 'SubMiner'); + try { + const app = { + setName: () => {}, + setPath: (_key: 'userData', value: string) => { + assert.equal(value, configDir); + assert.equal(fs.statSync(value).isDirectory(), true); + }, + }; + const options = { platform: 'darwin', homeDir, xdgConfigHome: '' } satisfies Parameters< + typeof configureEarlyAppPaths + >[1]; + + assert.equal(fs.existsSync(path.join(homeDir, '.config')), false); + configureEarlyAppPaths(app, options); + + const configPath = path.join(configDir, 'config.jsonc'); + const existingConfig = '{"logging":{"level":"debug"}}\n'; + fs.writeFileSync(configPath, existingConfig); + configureEarlyAppPaths(app, options); + assert.equal(fs.readFileSync(configPath, 'utf8'), existingConfig); + } finally { + fs.rmSync(homeDir, { recursive: true, force: true }); + } }); diff --git a/src/main-entry-runtime.ts b/src/main-entry-runtime.ts index a344d6b8..f86e5016 100644 --- a/src/main-entry-runtime.ts +++ b/src/main-entry-runtime.ts @@ -260,6 +260,8 @@ export function configureEarlyAppPaths(app: EarlyAppLike, options?: EarlyAppPath existsSync: options?.existsSync ?? fs.existsSync, }); + // The entry process requests its singleton lock before main-process config bootstrap. + fs.mkdirSync(userDataPath, { recursive: true }); app.setName(APP_NAME); app.setPath('userData', userDataPath); diff --git a/src/quality-gate-workflow.test.ts b/src/quality-gate-workflow.test.ts index 2178a0de..e6056d28 100644 --- a/src/quality-gate-workflow.test.ts +++ b/src/quality-gate-workflow.test.ts @@ -24,7 +24,12 @@ test('quality gate checkout does not persist GitHub credentials', () => { test('quality gate installs Lua and runs the environment suite before coverage', () => { assert.match(qualityGateWorkflow, /name: Install Lua/); - assert.match(qualityGateWorkflow, /apt-get install -y lua5\.4/); + assert.match( + qualityGateWorkflow, + /apt_sources=\(-o Dir::Etc::sourcelist=sources\.list\.d\/ubuntu\.sources -o Dir::Etc::sourceparts=-\)/, + ); + assert.match(qualityGateWorkflow, /apt-get\s+"\$\{apt_sources\[@\]\}"\s+update/); + assert.match(qualityGateWorkflow, /apt-get\s+"\$\{apt_sources\[@\]\}"\s+install\s+-y\s+lua5\.4/); assert.match( qualityGateWorkflow, /Test suite \(source\)\n\s*run: bun run test:fast\n\s*\n\s*- name: Environment suite\n\s*run: bun run test:env\n\s*\n\s*- name: Coverage suite \(maintained source lane\)/,