diff --git a/.github/workflows/build-windows-store.yml b/.github/workflows/build-windows-store.yml new file mode 100644 index 00000000..a7e6f034 --- /dev/null +++ b/.github/workflows/build-windows-store.yml @@ -0,0 +1,45 @@ +name: Build Windows Store package + +on: + workflow_dispatch: + pull_request: + paths: + - .github/workflows/build-windows-store.yml + - app/** + - src/** + - package.json + - package-lock.json + +permissions: + contents: read + +jobs: + appx: + runs-on: windows-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + node-version: 22.13.0 + cache: npm + cache-dependency-path: | + package-lock.json + app/package-lock.json + + - name: Install CLI dependencies + run: npm ci + + - name: Install desktop dependencies + run: npm ci --prefix app + + - name: Build Microsoft Store package + run: npm --prefix app run package:store + + - name: Upload Store package + uses: actions/upload-artifact@v6 + with: + name: CodeBurn-Microsoft-Store + path: app/release/CodeBurn-Store-*.appx + if-no-files-found: error + retention-days: 14 diff --git a/app/DISTRIBUTION.md b/app/DISTRIBUTION.md index 7bd4bd0e..69165022 100644 --- a/app/DISTRIBUTION.md +++ b/app/DISTRIBUTION.md @@ -66,6 +66,7 @@ npm --prefix app run package # macOS, both arm64 and x64 npm --prefix app run package:arm64 # macOS arm64 only (faster on Apple Silicon) npm --prefix app run package:x64 # macOS x64 only npm --prefix app run package:win # Windows NSIS installer, x64 +npm --prefix app run package:store # Microsoft Store AppX, x64 (Windows host only) npm --prefix app run package:linux # Linux AppImage, x64 ``` @@ -184,6 +185,27 @@ shows **"Windows protected your PC"**. Users click **"More info" → "Run anyway"** to launch it. This is expected for an unsigned build; the only fix is a purchased code-signing (Authenticode/EV) certificate. +### Microsoft Store (`package:store`) + +The Store build is a separate AppX target so the GitHub NSIS installer remains +unchanged. AppX packaging requires Windows 10 or newer and is built by the +manual `Build Windows Store package` GitHub Actions workflow on +`windows-latest`. Download its `CodeBurn-Microsoft-Store` workflow artifact and +upload the contained `CodeBurn-Store--x64.appx` file in Partner Center. + +The manifest identity must exactly match the reserved Partner Center product: + +- Identity name: `Codeburn.CodeBurn` +- Publisher: `CN=3EFA3336-87E1-46F2-9DFA-2EB5A7693F89` +- Publisher display name: `Codeburn` +- Store ID: `9P0R4ZL5XMB8` + +The Store package is intentionally unsigned: Microsoft signs it during Store +submission. Direct sideloading requires a separate trusted or development +certificate. The AppX declares `runFullTrust` (electron-builder's required +default for Electron apps), so CodeBurn retains access to the user's local +provider session files rather than running in a UWP application sandbox. + ### Linux (`package:linux`) `electron-builder --linux` produces a single artifact in `app/release/`: diff --git a/app/build/appx/Square150x150Logo.png b/app/build/appx/Square150x150Logo.png new file mode 100644 index 00000000..f80200bd Binary files /dev/null and b/app/build/appx/Square150x150Logo.png differ diff --git a/app/build/appx/Square44x44Logo.png b/app/build/appx/Square44x44Logo.png new file mode 100644 index 00000000..33d78894 Binary files /dev/null and b/app/build/appx/Square44x44Logo.png differ diff --git a/app/build/appx/StoreLogo.png b/app/build/appx/StoreLogo.png new file mode 100644 index 00000000..bc440fed Binary files /dev/null and b/app/build/appx/StoreLogo.png differ diff --git a/app/build/appx/Wide310x150Logo.png b/app/build/appx/Wide310x150Logo.png new file mode 100644 index 00000000..909773a2 Binary files /dev/null and b/app/build/appx/Wide310x150Logo.png differ diff --git a/app/package.json b/app/package.json index 3301a041..d9e7b424 100644 --- a/app/package.json +++ b/app/package.json @@ -16,6 +16,7 @@ "package:arm64": "npm run stage-cli && npm run build && electron-builder --mac --arm64", "package:x64": "npm run stage-cli && npm run build && electron-builder --mac --x64", "package:win": "npm run stage-cli && npm run build && electron-builder --win", + "package:store": "npm run stage-cli && npm run build && electron-builder --win appx --x64", "package:linux": "npm run stage-cli && npm run build && electron-builder --linux" }, "dependencies": { @@ -100,6 +101,20 @@ "perMachine": false, "artifactName": "CodeBurn-Setup-${version}.${ext}" }, + "appx": { + "applicationId": "CodeBurn", + "identityName": "Codeburn.CodeBurn", + "publisher": "CN=3EFA3336-87E1-46F2-9DFA-2EB5A7693F89", + "publisherDisplayName": "Codeburn", + "displayName": "CodeBurn", + "artifactName": "CodeBurn-Store-${version}-${arch}.${ext}", + "backgroundColor": "#15100D", + "languages": [ + "en-US" + ], + "minVersion": "10.0.17763.0", + "maxVersionTested": "10.0.26100.0" + }, "linux": { "target": [ { diff --git a/app/scripts/stage-cli.mjs b/app/scripts/stage-cli.mjs index 827776e1..69085678 100644 --- a/app/scripts/stage-cli.mjs +++ b/app/scripts/stage-cli.mjs @@ -68,7 +68,11 @@ writeFileSync( // extraneous warnings, so capture stdout regardless of exit code. let listed = '' try { - listed = execFileSync('npm', ['ls', '--omit=dev', '--all', '--parseable'], { + // Execute npm's JavaScript entry point with the current Node binary. Windows + // exposes npm as a .cmd shim, which execFile cannot launch without a shell. + const npmCli = process.env.npm_execpath + if (!npmCli) throw new Error('npm_execpath is unavailable') + listed = execFileSync(process.execPath, [npmCli, 'ls', '--omit=dev', '--all', '--parseable'], { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, @@ -81,11 +85,17 @@ try { // Map each back to its top-level node_modules entry (`name` or `@scope/name`), // then copy those dirs whole — a package's own nested node_modules comes with // it, which is exactly the closure it needs at runtime. -const prefix = rootModules + '/' +// `npm ls --parseable` uses native separators on Windows. Normalize both sides +// before extracting the package name so Store builds do not treat a populated +// node_modules tree as empty merely because it uses `\\` instead of `/`. +const prefix = rootModules.replaceAll('\\', '/') + '/' +const comparisonPrefix = process.platform === 'win32' ? prefix.toLowerCase() : prefix const topLevel = new Set() for (const line of listed.split('\n')) { - if (!line.startsWith(prefix)) continue - const rest = line.slice(prefix.length) + const normalizedLine = line.trim().replaceAll('\\', '/') + const comparisonLine = process.platform === 'win32' ? normalizedLine.toLowerCase() : normalizedLine + if (!comparisonLine.startsWith(comparisonPrefix)) continue + const rest = normalizedLine.slice(prefix.length) const match = rest.match(/^(@[^/]+\/[^/]+|[^/]+)/) if (match) topLevel.add(match[1]) }