diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index 4b1ebd5..810c9e9 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -39,7 +39,7 @@ jobs: fi package-windows: - name: Windows Installer and Portable + name: Windows Portable needs: preflight runs-on: windows-latest timeout-minutes: 40 @@ -55,25 +55,13 @@ jobs: - name: Verify artifacts shell: pwsh run: | - $patterns = @("release/Motif-*-Setup.exe", "release/Motif-*-Portable.exe", "release/Motif-*-win.zip", "release/latest.yml") - foreach ($pattern in $patterns) { - if (-not (Get-ChildItem $pattern -File -ErrorAction SilentlyContinue)) { throw "Missing required artifact: $pattern" } - } + if (-not (Get-ChildItem "release/Motif-*-Portable.exe" -File -ErrorAction SilentlyContinue)) { throw "Missing portable executable" } Write-Host "::notice::Windows artifacts are intentionally published without Authenticode signing" - name: Smoke unpacked app run: npm run desktop:smoke env: MOTIF_DISABLE_UPDATER: "1" MOTIF_SMOKE_USER_DATA: ${{ runner.temp }}\motif-smoke-unpacked - - name: Smoke installed app - shell: pwsh - run: | - $setup = (Get-ChildItem release/Motif-*-Setup.exe).FullName - $install = Join-Path $env:RUNNER_TEMP "motif-installed" - Start-Process -FilePath $setup -ArgumentList "/S", "/D=$install" -Wait - $env:MOTIF_EXECUTABLE = Join-Path $install "Motif.exe" - $env:MOTIF_SMOKE_USER_DATA = Join-Path $env:RUNNER_TEMP "motif-smoke-installed" - npm run desktop:smoke - name: Smoke portable app shell: pwsh run: | @@ -83,7 +71,7 @@ jobs: - name: Generate final checksums shell: pwsh run: | - $files = Get-ChildItem release -File | Where-Object { $_.Extension -in ".exe", ".zip", ".blockmap" -or $_.Name -eq "latest.yml" } | Sort-Object Name + $files = Get-ChildItem release/Motif-*-Portable.exe -File $lines = foreach ($file in $files) { $hash = Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256 "$($hash.Hash.ToLowerInvariant()) $($file.Name)" @@ -94,10 +82,7 @@ jobs: name: motif-windows if-no-files-found: error path: | - release/*.exe - release/*.zip - release/*.blockmap - release/latest.yml + release/*-Portable.exe release/SHA256SUMS-windows.txt package-macos: @@ -164,22 +149,22 @@ jobs: for pattern in "release/*.dmg" "release/*.zip" "release/latest-mac.yml"; do compgen -G "$pattern" >/dev/null || { echo "::error::Missing $pattern"; exit 1; } done - - name: Smoke arm64 packaged app - run: MOTIF_EXECUTABLE="release/mac-arm64/Motif.app/Contents/MacOS/Motif" MOTIF_SMOKE_USER_DATA="$RUNNER_TEMP/motif-smoke-mac-arm64" npm run desktop:smoke - - name: Smoke arm64 DMG + - name: Smoke universal packaged app + run: MOTIF_EXECUTABLE="release/mac-universal/Motif.app/Contents/MacOS/Motif" MOTIF_SMOKE_USER_DATA="$RUNNER_TEMP/motif-smoke-mac-universal" npm run desktop:smoke + - name: Smoke universal DMG on arm64 run: | - dmg=$(compgen -G "release/Motif-*-arm64.dmg") - [ -f "$dmg" ] || { echo "::error::Missing arm64 DMG"; exit 1; } - mount="$RUNNER_TEMP/motif-dmg-arm64" + dmg=$(compgen -G "release/Motif-*-universal.dmg") + [ -f "$dmg" ] || { echo "::error::Missing universal DMG"; exit 1; } + mount="$RUNNER_TEMP/motif-dmg-universal" mkdir -p "$mount" hdiutil attach "$dmg" -nobrowse -readonly -mountpoint "$mount" - cp -R "$mount/Motif.app" "$RUNNER_TEMP/Motif-arm64.app" + cp -R "$mount/Motif.app" "$RUNNER_TEMP/Motif-universal.app" hdiutil detach "$mount" - MOTIF_EXECUTABLE="$RUNNER_TEMP/Motif-arm64.app/Contents/MacOS/Motif" MOTIF_SMOKE_USER_DATA="$RUNNER_TEMP/motif-smoke-dmg-arm64" npm run desktop:smoke + MOTIF_EXECUTABLE="$RUNNER_TEMP/Motif-universal.app/Contents/MacOS/Motif" MOTIF_SMOKE_USER_DATA="$RUNNER_TEMP/motif-smoke-dmg-universal-arm64" npm run desktop:smoke - name: Generate final checksums run: | : > release/SHA256SUMS-macos.txt - find release -maxdepth 1 -type f \( -name '*.dmg' -o -name '*.zip' -o -name '*.blockmap' -o -name 'latest*.yml' \) -print0 \ + find release -maxdepth 1 -type f \( -name '*.dmg' -o -name '*.zip' -o -name 'latest*.yml' \) -print0 \ | sort -z \ | xargs -0 shasum -a 256 \ | sed 's#release/##' >> release/SHA256SUMS-macos.txt @@ -190,7 +175,6 @@ jobs: path: | release/*.dmg release/*.zip - release/*.blockmap release/latest-mac.yml release/SHA256SUMS-macos.txt @@ -210,22 +194,22 @@ jobs: with: name: motif-macos path: release - - name: Verify and smoke x64 DMG + - name: Verify and smoke universal DMG on Intel run: | [ "$(uname -m)" = "x86_64" ] || { echo "::error::Intel smoke requires an x86_64 runner"; exit 1; } - dmg=$(compgen -G "release/Motif-*-x64.dmg") - [ -f "$dmg" ] || { echo "::error::Missing x64 DMG"; exit 1; } + dmg=$(compgen -G "release/Motif-*-universal.dmg") + [ -f "$dmg" ] || { echo "::error::Missing universal DMG"; exit 1; } xcrun stapler validate "$dmg" - mount="$RUNNER_TEMP/motif-dmg-x64" + mount="$RUNNER_TEMP/motif-dmg-universal-intel" mkdir -p "$mount" hdiutil attach "$dmg" -nobrowse -readonly -mountpoint "$mount" trap 'hdiutil detach "$mount"' EXIT - cp -R "$mount/Motif.app" "$RUNNER_TEMP/Motif-x64.app" + cp -R "$mount/Motif.app" "$RUNNER_TEMP/Motif-universal.app" hdiutil detach "$mount" trap - EXIT - codesign --verify --deep --strict --verbose=2 "$RUNNER_TEMP/Motif-x64.app" - spctl --assess --verbose --type exec "$RUNNER_TEMP/Motif-x64.app" - MOTIF_EXECUTABLE="$RUNNER_TEMP/Motif-x64.app/Contents/MacOS/Motif" MOTIF_SMOKE_USER_DATA="$RUNNER_TEMP/motif-smoke-dmg-x64" npm run desktop:smoke + codesign --verify --deep --strict --verbose=2 "$RUNNER_TEMP/Motif-universal.app" + spctl --assess --verbose --type exec "$RUNNER_TEMP/Motif-universal.app" + MOTIF_EXECUTABLE="$RUNNER_TEMP/Motif-universal.app/Contents/MacOS/Motif" MOTIF_SMOKE_USER_DATA="$RUNNER_TEMP/motif-smoke-dmg-universal-intel" npm run desktop:smoke publish: name: Publish Complete Immutable Release @@ -262,5 +246,8 @@ jobs: fail_on_unmatched_files: true generate_release_notes: true files: | - release/windows/* - release/macos/* + release/windows/Motif-*-Portable.exe + release/windows/SHA256SUMS-windows.txt + release/macos/Motif-*-universal.dmg + release/macos/Motif-*-universal.zip + release/macos/latest-mac.yml diff --git a/README.md b/README.md index d235a0f..6822ed1 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,8 @@ Use one of these files: | Platform | File | | --- | --- | -| Windows installer | `Motif-*-Setup.exe` | | Windows portable | `Motif-*-Portable.exe` | -| macOS | `Motif-*-arm64.dmg` for Apple Silicon, `Motif-*-x64.dmg` for Intel | +| macOS | `Motif-*-universal.dmg` for Apple Silicon and Intel | The other files attached to each release are updater metadata and integrity files. Motif keeps projects and settings on the local device by default; prompts and inputs are sent to the provider you configure. See [Security and Privacy](docs/security-privacy.md). diff --git a/api/package.json b/api/package.json index 49d086e..d7bbe44 100644 --- a/api/package.json +++ b/api/package.json @@ -1,6 +1,6 @@ { "name": "motif-api", - "version": "0.2.0", + "version": "0.2.1", "private": true, "type": "module", "scripts": { diff --git a/docs/desktop-release.md b/docs/desktop-release.md index cadf640..670c4d6 100644 --- a/docs/desktop-release.md +++ b/docs/desktop-release.md @@ -32,13 +32,13 @@ npm run desktop:dist:mac -- --publish never - Windows and macOS jobs upload workflow artifacts only. One final job verifies both sets and publishes once. - Manual dispatch builds and verifies artifacts but does not publish a GitHub Release. -User-facing assets are `Motif-*-Setup.exe`, `Motif-*-Portable.exe`, and the architecture-specific macOS DMGs. `latest.yml`, `latest-mac.yml`, zip files, blockmaps, and SHA-256 manifests support updates and verification. +User-facing assets are `Motif-*-Portable.exe` and `Motif-*-universal.dmg`. `latest-mac.yml`, the universal macOS zip, and `SHA256SUMS-windows.txt` support updates. GitHub also adds its two source archives. -The installed Windows app uses the normal Electron updater. The portable executable uses the latest GitHub Release, verifies its executable against `SHA256SUMS-windows.txt`, replaces itself, and rolls back if replacement fails. Both use the same per-user Electron data directory. +The portable executable uses the latest GitHub Release, verifies its executable against `SHA256SUMS-windows.txt`, replaces itself, and rolls back if replacement fails. Windows installer builds ended after v0.2.0; installed users can switch to the portable executable without moving their per-user Motif data. ## Required secrets -Windows artifacts are intentionally published without Authenticode signing. SHA-256 manifests and updater metadata are still verified before publication; Windows may show SmartScreen warnings, so the zip remains the lower-friction fallback. +The Windows portable executable is intentionally published without Authenticode signing and may show SmartScreen warnings. Its SHA-256 manifest is verified before publication. macOS builds require signing and notarization credentials: @@ -51,15 +51,15 @@ macOS builds require signing and notarization credentials: | `APPLE_APP_SPECIFIC_PASSWORD` | Apple app-specific password | | `APPLE_TEAM_ID` | Apple Developer Team ID | -The workflow verifies Windows artifacts, updater metadata, and checksums, plus macOS code signatures, Gatekeeper assessment, notarization staples, updater metadata, and final checksums before publication. +The workflow verifies the Windows portable executable and checksum, plus macOS universal architecture, code signatures, Gatekeeper assessment, notarization staples, updater metadata, and internal checksums before publication. ## Release procedure 1. Set the same new version in all workspace package files and regenerate `package-lock.json` with `npm install --package-lock-only`. 2. Run the local checks above and create tag `v`. 3. Push the tag. Do not create a release manually. -4. Confirm installer, portable, unpacked app, app bundle, and DMG smoke jobs pass with isolated user-data directories. -5. Confirm the release contains both checksum manifests and both updater metadata files. +4. Confirm portable, unpacked app, universal app bundle, and universal DMG smoke jobs pass with isolated user-data directories on Windows, Apple Silicon, and Intel. +5. Confirm the release has exactly five uploaded assets: portable EXE, Windows checksum, universal DMG, universal zip, and `latest-mac.yml`. 6. Install the previous public version, update to the new version, restart, and confirm the version plus existing projects, settings, and provider credentials remain available. The last upgrade check requires real previously published artifacts and remains a manual release gate. diff --git a/docs/interactive-testing.md b/docs/interactive-testing.md index a02d4e9..4b63aaa 100644 --- a/docs/interactive-testing.md +++ b/docs/interactive-testing.md @@ -4,12 +4,12 @@ Automated smoke tests launch packaged Electron executables twice with an isolate Before a release, test each distributed format on its target OS: -1. Install or mount the artifact in a clean user account or VM. +1. Run the Windows portable executable or mount the universal macOS DMG in a clean user account or VM. 2. Launch Motif and configure a disposable provider credential. 3. Create a project and generation, quit, reopen, and confirm both persist. 4. Exercise minimize, maximize/restore, close, an external HTTPS link, and a blocked non-web navigation attempt. 5. Confirm the expected unsigned Windows status and checksum metadata, or macOS Gatekeeper/notarization status. 6. Upgrade from the previous public version and confirm the displayed version, projects, settings, and credential availability. -7. Repeat the portable Windows check from a writable folder; retain the installed/zip path as the fallback if endpoint protection rejects the packed portable executable. +7. Repeat the portable Windows check from a writable folder and confirm self-update rollback leaves the prior executable usable after a forced replacement failure. -Browser Playwright checks remain useful for web behavior, but they do not replace packaged Electron, installer, DMG, signing, notarization, or upgrade proof. +Browser Playwright checks remain useful for web behavior, but they do not replace packaged Electron, DMG, signing, notarization, or upgrade proof. diff --git a/package-lock.json b/package-lock.json index cfccf66..e3bbcda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "motif", - "version": "0.2.0", + "version": "0.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "motif", - "version": "0.2.0", + "version": "0.2.1", "workspaces": [ "api", "web" @@ -17,6 +17,7 @@ "express": "^5.2.1" }, "devDependencies": { + "@electron/rebuild": "^4.2.0", "concurrently": "^9.1.0", "electron": "^42.4.1", "electron-builder": "^26.15.3", @@ -25,7 +26,7 @@ }, "api": { "name": "motif-api", - "version": "0.2.0", + "version": "0.2.1", "dependencies": { "better-sqlite3": "^12.11.1", "express": "^5.2.1" @@ -492,9 +493,9 @@ } }, "node_modules/@electron/rebuild": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.4.tgz", - "integrity": "sha512-Rzc39XPdk/+/wBG8MfwAHohXflep0ITUfulb6Rgz3R0NeSB1noE+E9/M/cb8ftCAiyDD9PPhLuuWgE1GaInbKg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.2.0.tgz", + "integrity": "sha512-RKL/O+jGoXJMxrx/5771y1n0xTKmFuOYGO3gMmwypBM6rsH0kou0mswwdXA2JrhIkE4xyC7v9vGk0n6NPzgOxQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8413,7 +8414,7 @@ }, "web": { "name": "frontend", - "version": "0.2.0", + "version": "0.2.1", "dependencies": { "@tailwindcss/vite": "^4.2.2", "bootstrap-icons": "^1.13.1", diff --git a/package.json b/package.json index f745ddc..fcbb7f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "motif", - "version": "0.2.0", + "version": "0.2.1", "description": "AI-powered UI generation with design genomes", "author": "Omer Shatzberg", "private": true, @@ -14,7 +14,7 @@ "dev:web": "npm run --workspace web dev", "dev:api": "npm run --workspace api dev", "build": "npm run --workspace web build && npm run --workspace api build", - "desktop:prepare": "electron-builder install-app-deps", + "desktop:prepare": "electron-rebuild -f -w better-sqlite3", "desktop:dev": "npm run build && npm run desktop:prepare && electron .", "desktop:pack": "npm run build && npm run desktop:prepare && electron-builder --dir", "desktop:dist": "npm run build && npm run desktop:prepare && electron-builder", @@ -32,6 +32,7 @@ "start": "npm run --workspace api start" }, "devDependencies": { + "@electron/rebuild": "^4.2.0", "concurrently": "^9.1.0", "electron": "^42.4.1", "electron-builder": "^26.15.3", @@ -69,18 +70,6 @@ ], "win": { "target": [ - { - "target": "nsis", - "arch": [ - "x64" - ] - }, - { - "target": "zip", - "arch": [ - "x64" - ] - }, { "target": "portable", "arch": [ @@ -89,12 +78,6 @@ } ] }, - "nsis": { - "oneClick": false, - "perMachine": false, - "allowToChangeInstallationDirectory": true, - "artifactName": "${productName}-${version}-Setup.${ext}" - }, "portable": { "artifactName": "${productName}-${version}-Portable.${ext}" }, @@ -108,15 +91,13 @@ { "target": "dmg", "arch": [ - "x64", - "arm64" + "universal" ] }, { "target": "zip", "arch": [ - "x64", - "arm64" + "universal" ] } ], diff --git a/scripts/check-release.mjs b/scripts/check-release.mjs index 8afe5ae..2940ae4 100644 --- a/scripts/check-release.mjs +++ b/scripts/check-release.mjs @@ -14,6 +14,11 @@ assert.equal(lock.version, root.version, "package-lock.json version must match p assert.equal(lock.packages["api"].version, root.version); assert.equal(lock.packages["web"].version, root.version); assert.ok(!fs.existsSync("api/package-lock.json") && !fs.existsSync("web/package-lock.json")); +assert.deepEqual(root.build.win.target, [{ target: "portable", arch: ["x64"] }]); +assert.deepEqual(root.build.mac.target, [ + { target: "dmg", arch: ["universal"] }, + { target: "zip", arch: ["universal"] }, +]); const tag = process.env.GITHUB_REF?.startsWith("refs/tags/") ? process.env.GITHUB_REF.slice("refs/tags/".length) diff --git a/scripts/electron-smoke.mjs b/scripts/electron-smoke.mjs index d49c5c9..a034c1a 100644 --- a/scripts/electron-smoke.mjs +++ b/scripts/electron-smoke.mjs @@ -10,7 +10,7 @@ process.env.MOTIF_DISABLE_UPDATER ??= "1"; function findExecutable() { if (process.env.MOTIF_EXECUTABLE) return path.resolve(process.env.MOTIF_EXECUTABLE); const candidates = process.platform === "darwin" - ? ["release/mac-arm64/Motif.app/Contents/MacOS/Motif", "release/mac/Motif.app/Contents/MacOS/Motif"] + ? ["release/mac-universal/Motif.app/Contents/MacOS/Motif", "release/mac/Motif.app/Contents/MacOS/Motif"] : process.platform === "win32" ? ["release/win-unpacked/Motif.exe"] : ["release/linux-unpacked/motif"]; diff --git a/scripts/verify-release-artifacts.mjs b/scripts/verify-release-artifacts.mjs index 52f2ba2..583d7cf 100644 --- a/scripts/verify-release-artifacts.mjs +++ b/scripts/verify-release-artifacts.mjs @@ -9,7 +9,7 @@ function verifyChecksums(platform) { const directory = path.join(root, platform); const manifestPath = path.join(directory, `SHA256SUMS-${platform}.txt`); const lines = fs.readFileSync(manifestPath, "utf8").trim().split(/\r?\n/); - assert.ok(lines.length > 1, `${platform} checksum manifest is empty`); + assert.ok(lines.length > 0, `${platform} checksum manifest is empty`); const covered = new Set(); for (const line of lines) { const match = line.match(/^([a-f0-9]{64})\s+\*?(.+)$/i); @@ -36,6 +36,5 @@ function verifyUpdaterMetadata(platform, filename) { verifyChecksums("windows"); verifyChecksums("macos"); -verifyUpdaterMetadata("windows", "latest.yml"); verifyUpdaterMetadata("macos", "latest-mac.yml"); console.log("Release checksums and updater metadata verified."); diff --git a/web/package.json b/web/package.json index c71c697..8c9cb6e 100644 --- a/web/package.json +++ b/web/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "0.2.0", + "version": "0.2.1", "type": "module", "scripts": { "dev": "vite",