Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .github/workflows/build-windows-store.yml
Original file line number Diff line number Diff line change
@@ -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
22 changes: 22 additions & 0 deletions app/DISTRIBUTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down Expand Up @@ -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-<version>-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/`:
Expand Down
Binary file added app/build/appx/Square150x150Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/build/appx/Square44x44Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/build/appx/StoreLogo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/build/appx/Wide310x150Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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": [
{
Expand Down
18 changes: 14 additions & 4 deletions app/scripts/stage-cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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])
}
Expand Down