Add Windows installer packaging
Template tests / tests (push) Successful in 1m40s

This commit is contained in:
2026-06-23 11:01:43 -05:00
parent f4278d118c
commit 058582d7af
7 changed files with 93 additions and 35 deletions
+5 -5
View File
@@ -1,7 +1,7 @@
name: Release
# Manual release: from the GitHub "Actions" tab pick "Release", click
# "Run workflow", enter the version tag, and it builds the Windows portable
# "Run workflow", enter the version tag, and it builds the Windows installer
# .exe and the Linux tarball + .deb, then publishes a GitHub Release with all
# artifacts attached and auto-generated notes.
on:
@@ -23,7 +23,7 @@ permissions:
jobs:
build-windows:
name: Build Windows portable
name: Build Windows installer
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
@@ -59,12 +59,12 @@ jobs:
echo "Windows code signing secrets not configured; building unsigned."
fi
- name: Package Windows portable .exe
- name: Package Windows installer .exe
run: npm run package:windows
- uses: actions/upload-artifact@v4
with:
name: windows-portable
name: windows-installer
path: releases/*.exe
if-no-files-found: error
@@ -126,7 +126,7 @@ jobs:
flags+=(--prerelease)
fi
gh release create "$TAG" \
dist/windows-portable/* \
dist/windows-installer/*.exe \
dist/linux-artifacts/* \
--title "StepForge $TAG" \
--target "${{ github.sha }}" \
+2 -2
View File
@@ -93,8 +93,8 @@ bash scripts/bootstrap-offline.sh # verify toolchain availability
bash scripts/verify.sh # full test suite + smoke checks
bash scripts/build-release.sh # assemble runnable app directory
bash scripts/package-linux.sh # portable tar.gz + .deb (+ AppDir spec)
npm run package:windows # portable Windows .exe in releases/
pwsh scripts/package-windows.ps1 # same Windows portable build via PowerShell
npm run package:windows # Windows installer .exe in releases/
pwsh scripts/package-windows.ps1 # same Windows installer build via PowerShell
```
See [build/build_report.md](build/build_report.md) for what was produced on
+6
View File
@@ -21,6 +21,12 @@ const { readLock } = require('../core/locks');
const CaptureService = require('./capture');
const { keepProcessesResponsive } = require('./win-power');
const APP_ID = 'com.stepforge.app';
if (process.platform === 'win32') {
app.setAppUserModelId(APP_ID);
}
// Keep capture working on battery. In a power-saving plan on DC power, Windows
// applies Power Throttling (EcoQoS) to background work — and StepForge records
// with its window hidden, so the frame-capture worker renderer is exactly the
+1 -1
View File
@@ -87,6 +87,6 @@ If you want to find commands quickly, press `Ctrl+/` for Quick Actions.
## Optional builds
1. `bash scripts/build-release.sh` assembles the offline release layout.
2. `npm run package:windows` creates the portable Windows `.exe` in
2. `npm run package:windows` creates the Windows installer `.exe` in
`releases/`.
3. `bash scripts/package-linux.sh` creates Linux release artifacts.
+1 -1
View File
@@ -109,7 +109,7 @@ ${toolRows}
Fallback policy: when a packaging tool is missing the build still produces
the runnable app (portable tarball with launcher) plus whatever package
formats the available tools allow. Windows artifacts are produced by
\`npm run package:windows\` (electron-builder, portable .exe); .msi/.rpm/
\`npm run package:windows\` (electron-builder, installer .exe); .msi/.rpm/
AppImage require the tools listed above and are skipped on this host.
## Offline guarantee
+46 -26
View File
@@ -9,12 +9,9 @@ const { build, Platform } = require('electron-builder');
const ROOT_DIR = path.resolve(__dirname, '..');
const PACKAGE_JSON = require(path.join(ROOT_DIR, 'package.json'));
const RELEASE_DIR = path.resolve(process.env.STEPFORGE_RELEASE_DIR || path.join(ROOT_DIR, 'releases'));
const WORK_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'stepforge-win-'));
const OUTPUT_DIR = path.join(WORK_DIR, 'output');
const ARTIFACT_NAME = 'stepforge-windows-x64-portable.exe';
const APP_ID = 'com.stepforge.app';
function findPortableExe(dir) {
function findInstallerExe(dir) {
if (!fs.existsSync(dir)) return null;
const stack = [dir];
while (stack.length) {
@@ -31,15 +28,12 @@ function findPortableExe(dir) {
return null;
}
async function main() {
fs.mkdirSync(RELEASE_DIR, { recursive: true });
fs.rmSync(OUTPUT_DIR, { recursive: true, force: true });
const config = {
appId: 'com.stepforge.app',
function createWindowsInstallerConfig(outputDir) {
return {
appId: APP_ID,
productName: 'StepForge',
directories: {
output: OUTPUT_DIR,
output: outputDir,
},
files: [
'app/**/*',
@@ -49,33 +43,59 @@ async function main() {
],
asar: true,
compression: 'normal',
artifactName: ARTIFACT_NAME,
win: {
target: ['portable'],
target: ['nsis'],
},
nsis: {
oneClick: false,
allowToChangeInstallationDirectory: true,
createDesktopShortcut: true,
createStartMenuShortcut: true,
shortcutName: 'StepForge',
},
};
}
async function buildWindowsInstaller() {
const releaseDir = path.resolve(process.env.STEPFORGE_RELEASE_DIR || path.join(ROOT_DIR, 'releases'));
const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'stepforge-win-'));
const outputDir = path.join(workDir, 'output');
fs.mkdirSync(releaseDir, { recursive: true });
fs.rmSync(outputDir, { recursive: true, force: true });
const config = createWindowsInstallerConfig(outputDir);
try {
await build({
targets: Platform.WINDOWS.createTarget('portable'),
targets: Platform.WINDOWS.createTarget('nsis'),
config,
});
} catch (err) {
throw new Error(`Windows portable build failed: ${err.message}`);
throw new Error(`Windows installer build failed: ${err.message}`);
}
const builtExe = findPortableExe(OUTPUT_DIR);
if (!builtExe) {
throw new Error(`No .exe artifact was produced in ${OUTPUT_DIR}`);
const builtInstaller = findInstallerExe(outputDir);
if (!builtInstaller) {
throw new Error(`No installer .exe artifact was produced in ${outputDir}`);
}
const releaseExe = path.join(RELEASE_DIR, path.basename(builtExe));
fs.copyFileSync(builtExe, releaseExe);
const releaseInstaller = path.join(releaseDir, path.basename(builtInstaller));
fs.copyFileSync(builtInstaller, releaseInstaller);
console.log(`StepForge ${PACKAGE_JSON.version} Windows portable build written to ${releaseExe}`);
console.log(`StepForge ${PACKAGE_JSON.version} Windows installer written to ${releaseInstaller}`);
}
main().catch((err) => {
console.error(err.message || err);
process.exitCode = 1;
});
if (require.main === module) {
buildWindowsInstaller().catch((err) => {
console.error(err.message || err);
process.exitCode = 1;
});
}
module.exports = {
APP_ID,
createWindowsInstallerConfig,
findInstallerExe,
buildWindowsInstaller,
};
+32
View File
@@ -0,0 +1,32 @@
'use strict';
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const assert = require('node:assert/strict');
const { makeTmpDir, rmrf } = require('./helpers');
const { createWindowsInstallerConfig, findInstallerExe } = require('../../scripts/package-windows');
test('Windows packaging uses an assisted NSIS installer', (t) => {
const config = createWindowsInstallerConfig('/tmp/stepforge-output');
assert.deepEqual(config.win.target, ['nsis']);
assert.equal(config.nsis.oneClick, false);
assert.equal(config.nsis.allowToChangeInstallationDirectory, true);
assert.equal(config.nsis.createDesktopShortcut, true);
assert.equal(config.nsis.createStartMenuShortcut, true);
assert.equal(config.nsis.shortcutName, 'StepForge');
assert.equal(config.asar, true);
assert.ok(config.files.includes('app/**/*'));
assert.ok(config.files.includes('core/**/*'));
assert.ok(config.files.includes('exporters/**/*'));
assert.ok(!config.files.includes('assets/**/*'));
const tmp = makeTmpDir('windows-installer');
t.after(() => rmrf(tmp));
fs.mkdirSync(path.join(tmp, 'nested', 'deeper'), { recursive: true });
const installer = path.join(tmp, 'nested', 'deeper', 'StepForge Setup 0.2.0.exe');
fs.writeFileSync(installer, Buffer.from('fake installer'));
assert.equal(findInstallerExe(tmp), installer);
});