From ed83fd3e42a4c9195767cb5dde5fac7359e6c52c Mon Sep 17 00:00:00 2001 From: Sparsh Date: Tue, 21 Apr 2026 19:24:21 +0530 Subject: [PATCH] feat(desktop): enhance packaging workflow and add smoke test for desktop app --- .github/workflows/desktop-packaging.yml | 9 ++- .../scripts/ensure-gitnexus-runtime.mjs | 64 ++++++++++++++++--- gitnexus-desktop/src/main/main.ts | 26 +++++++- 3 files changed, 86 insertions(+), 13 deletions(-) diff --git a/.github/workflows/desktop-packaging.yml b/.github/workflows/desktop-packaging.yml index 2b7d89787..a38ae843c 100644 --- a/.github/workflows/desktop-packaging.yml +++ b/.github/workflows/desktop-packaging.yml @@ -11,13 +11,15 @@ permissions: {} concurrency: group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: false + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: package: name: Build ${{ matrix.label }} runs-on: ${{ matrix.os }} timeout-minutes: 90 + permissions: + contents: read strategy: fail-fast: false matrix: @@ -63,6 +65,11 @@ jobs: env: CSC_IDENTITY_AUTO_DISCOVERY: 'false' + - name: Smoke test unpacked desktop bundle + if: matrix.os == 'windows-latest' + run: node scripts/smoke-unpacked.mjs + working-directory: gitnexus-desktop + - name: Build desktop package run: ${{ matrix.command }} working-directory: gitnexus-desktop diff --git a/gitnexus-desktop/scripts/ensure-gitnexus-runtime.mjs b/gitnexus-desktop/scripts/ensure-gitnexus-runtime.mjs index a6df9d098..1839ac76d 100644 --- a/gitnexus-desktop/scripts/ensure-gitnexus-runtime.mjs +++ b/gitnexus-desktop/scripts/ensure-gitnexus-runtime.mjs @@ -272,14 +272,43 @@ const runNpm = (args, cwd) => { } }; -const overlayDirectoryContents = (sourceDirectory, destinationDirectory) => { +const overlayDependencyPackages = (sourceDirectory, destinationDirectory, dependencyMap) => { mkdirSync(destinationDirectory, { recursive: true }); - for (const entry of readdirSync(sourceDirectory, { withFileTypes: true })) { - cpSync(path.join(sourceDirectory, entry.name), path.join(destinationDirectory, entry.name), { - force: true, - recursive: true, - }); + for (const packageName of Object.keys(dependencyMap)) { + const sourcePath = path.join(sourceDirectory, ...packageName.split('/')); + const destinationPath = path.join(destinationDirectory, ...packageName.split('/')); + + if (!existsSync(sourcePath)) { + continue; + } + + if (existsSync(destinationPath)) { + continue; + } + + mkdirSync(path.dirname(destinationPath), { recursive: true }); + + try { + cpSync(sourcePath, destinationPath, { + dereference: true, + force: true, + recursive: true, + }); + } catch (error) { + const errorCode = error?.code; + const isLockedPathError = + (errorCode === 'EPIPE' || errorCode === 'EBUSY' || errorCode === 'EPERM') && + existsSync(destinationPath); + + if (!isLockedPathError) { + throw error; + } + + console.warn( + `[gitnexus-desktop] Skipping locked dependency path during repair: ${destinationPath}`, + ); + } } }; @@ -313,9 +342,10 @@ const repairGitNexusPackages = (dependencyMap, label) => { runNpm(['install', '--no-package-lock'], repairDirectory); replaceDirectGitNexusPackages(dependencyMap); - overlayDirectoryContents( + overlayDependencyPackages( path.join(repairDirectory, 'node_modules'), path.join(gitnexusRoot, 'node_modules'), + dependencyMap, ); } finally { rmSync(repairDirectory, { force: true, recursive: true }); @@ -331,6 +361,20 @@ const runCommand = (command, args) => { }); }; +const getPosixCommandLine = (pid) => { + if (!pid) { + return ''; + } + + const lookup = runCommand('ps', ['-p', String(pid), '-o', 'args=']); + + if (lookup.status !== 0) { + return ''; + } + + return lookup.stdout.trim(); +}; + const findPortOwner = (port) => { if (process.platform === 'win32') { const lookup = runCommand('powershell.exe', [ @@ -357,7 +401,7 @@ const findPortOwner = (port) => { return JSON.parse(lookup.stdout.trim()); } - const lookup = runCommand('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-Fpcn']); + const lookup = runCommand('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-Fpc']); if (lookup.status !== 0 || !lookup.stdout.trim()) { return null; @@ -370,11 +414,11 @@ const findPortOwner = (port) => { owner.pid = Number.parseInt(line.slice(1), 10); } else if (line.startsWith('c')) { owner.name = line.slice(1); - } else if (line.startsWith('n')) { - owner.commandLine = line.slice(1); } } + owner.commandLine = getPosixCommandLine(owner.pid); + return owner.pid ? owner : null; }; diff --git a/gitnexus-desktop/src/main/main.ts b/gitnexus-desktop/src/main/main.ts index 4103be9d4..551adaa7b 100644 --- a/gitnexus-desktop/src/main/main.ts +++ b/gitnexus-desktop/src/main/main.ts @@ -37,6 +37,7 @@ const GITNEXUS_SERVER_READY_TIMEOUT_MS = 30_000; const GITNEXUS_WEB_READY_TIMEOUT_MS = 60_000; const GITNEXUS_WEB_READY_POLL_MS = 500; const PACKAGED_WEB_SERVER_HOST = '127.0.0.1'; +const IS_DESKTOP_SMOKE_TEST = process.env.GITNEXUS_DESKTOP_SMOKE_TEST === '1'; const MIME_TYPES: Record = { '.css': 'text/css; charset=utf-8', @@ -409,9 +410,24 @@ const sendStaticResponse = (assetPath: string | null, response: ServerResponse): createReadStream(assetPath).pipe(response); }; +const getRequestedPath = (requestUrl: string): string | null => { + try { + return decodeURIComponent(new URL(requestUrl, 'http://127.0.0.1').pathname); + } catch { + return null; + } +}; + const handlePackagedWebRequest = (request: IncomingMessage, response: ServerResponse): void => { const requestUrl = request.url ?? '/'; - const requestedPath = decodeURIComponent(new URL(requestUrl, 'http://127.0.0.1').pathname); + const requestedPath = getRequestedPath(requestUrl); + + if (!requestedPath) { + response.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' }); + response.end('Bad request'); + return; + } + sendStaticResponse(normalizeStaticPath(GITNEXUS_WEB_PACKAGED_DIR, requestedPath), response); }; @@ -580,7 +596,7 @@ const loadShellRenderer = async (window: BrowserWindow): Promise => { return; } - const rendererEntry = path.join(__dirname, '../renderer/src/renderer/index.html'); + const rendererEntry = path.join(__dirname, '../renderer/index.html'); console.info(`[gitnexus-desktop] Loading shell renderer file: ${rendererEntry}`); await window.loadFile(rendererEntry); }; @@ -732,6 +748,12 @@ async function createWindow(): Promise { if (!window.isDestroyed()) { window.show(); + + if (IS_DESKTOP_SMOKE_TEST) { + setTimeout(() => { + app.quit(); + }, 1_000); + } } }