Skip to content
Merged
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
65 changes: 26 additions & 39 deletions .github/workflows/desktop-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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: |
Expand All @@ -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)"
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -190,7 +175,6 @@ jobs:
path: |
release/*.dmg
release/*.zip
release/*.blockmap
release/latest-mac.yml
release/SHA256SUMS-macos.txt

Expand All @@ -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
Expand Down Expand Up @@ -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
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
2 changes: 1 addition & 1 deletion api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "motif-api",
"version": "0.2.0",
"version": "0.2.1",
"private": true,
"type": "module",
"scripts": {
Expand Down
12 changes: 6 additions & 6 deletions docs/desktop-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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<version>`.
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.
6 changes: 3 additions & 3 deletions docs/interactive-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
15 changes: 8 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 5 additions & 24 deletions package.json
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -69,18 +70,6 @@
],
"win": {
"target": [
{
"target": "nsis",
"arch": [
"x64"
]
},
{
"target": "zip",
"arch": [
"x64"
]
},
{
"target": "portable",
"arch": [
Expand All @@ -89,12 +78,6 @@
}
]
},
"nsis": {
"oneClick": false,
"perMachine": false,
"allowToChangeInstallationDirectory": true,
"artifactName": "${productName}-${version}-Setup.${ext}"
},
"portable": {
"artifactName": "${productName}-${version}-Portable.${ext}"
},
Expand All @@ -108,15 +91,13 @@
{
"target": "dmg",
"arch": [
"x64",
"arm64"
"universal"
]
},
{
"target": "zip",
"arch": [
"x64",
"arm64"
"universal"
]
}
],
Expand Down
5 changes: 5 additions & 0 deletions scripts/check-release.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion scripts/electron-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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"];
Expand Down
3 changes: 1 addition & 2 deletions scripts/verify-release-artifacts.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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.");
2 changes: 1 addition & 1 deletion web/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
"version": "0.2.0",
"version": "0.2.1",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down