Release guide
September 20, 2026 · View on GitHub
This maintainer runbook covers OpenPets desktop releases, npm package releases, and the plugin/catalog publishing steps that often ship beside a desktop build. The desktop release flow builds macOS and Linux artifacts locally from macOS, then uses GitHub Actions and SignPath to build and sign the Windows x64 installer before collecting the final verified GitHub Release artifacts. The local flow does not build a disposable Windows x64 NSIS installer.
The desktop release runs as a sequence of checkpointed stages, not as one long all-or-nothing command. Every stage that succeeds is recorded, so a failure in the middle of a two-hour release is recovered by re-running the exact same command: finished work is skipped and the release resumes at the stage that failed. See Staged desktop releases.
Repository and app
- GitHub repo:
alvinunreal/openpets - Desktop app:
apps/desktop - Release script:
apps/desktop/scripts/release-local.mjs - Root command:
pnpm release:desktop - SignPath Windows workflow:
.github/workflows/signpath-windows.yml - Update checker expects GitHub release tags like
v2.0.0.
Release surfaces
Most OpenPets releases touch one or more of these surfaces:
- Desktop app - Electron host, bundled official plugins, pet rendering, integrations, catalog consumers, and update checks.
- npm packages - CLI, MCP server, client, SDK, and integration packages.
- Plugin catalog - generated catalog JSON, reviewed plugin ZIPs, provenance, and R2-hosted downloads.
- Website catalog data - app-facing JSON and static assets under
web/public.
Keep each release scoped. A desktop-only fix does not require npm publishing. An SDK or CLI contract change usually does.
Desktop release checklist
Required validation before a desktop release:
pnpm --filter @open-pets/desktop check
pnpm --filter @open-pets/desktop test
pnpm plugins:locales
pnpm --filter @open-pets/desktop package:dir
Manual desktop QA:
- Run normal desktop dev startup or a packaged app (
pnpm dev:desktopor the output frompnpm --filter @open-pets/desktop package:dir) so bundled seeding runs. - Open tray → Plugins.
- Confirm the bundled official plugins appear with translated names and descriptions. See Official plugins for the current bundled/default-enabled set.
- Confirm community plugins appear separately/labeled as community when present.
- Confirm stale sample/legacy plugins do not appear unless intentionally migrated and listed in the current catalog.
- Confirm plugin names, descriptions, config labels, command labels, and pet messages resolve through translations rather than raw
$t:keys. - Exercise the SDK v3 surfaces used by official/community plugins: schedule, storage/state, commands, status, audio, notifications, pet reactions/interactions, movement, and any panel UI.
- Configure Reminders, Water Reminder, Focus Buddy, Launch Buddy, Day Routine, Walkabout, and other config-heavy plugins with form controls, not JSON.
- Run plugin commands from the Plugins UI and pet right-click menu when available.
- Restart desktop and confirm enabled plugins reload without broken state or duplicate timers/listeners.
- Inspect logs for plugin SDK, translation, permission, quota, and manifest validation errors.
For explicit local plugin development, run pnpm dev:desktop:plugins separately and confirm official plugins are loaded as local dev plugins and start disabled; this mode intentionally skips bundled seeding.
Plugin catalog release checklist
Plugin catalog release includes:
plugins/official/**and reviewed community plugin source.web/public/plugins/catalog.v2.json, regenerated from the current manifest v3 official and community plugin sources. Catalog entries includepublisherType: "official" | "community"; desktop treats missingpublisherTypeas official for older catalogs. The desktop runtime currently reads the v2 catalog endpoint even when the contained plugins use manifest v3 / SDK v3.web/public/plugins/catalog.v1.jsonretained as an empty compatibility catalog for old desktop versions.- Removal or hiding of legacy sample plugin manifests from current public discovery.
- Updated public docs when lineup, permissions, commands, provenance, or publishing behavior changes.
Required validation from the repository root:
pnpm plugins:locales
pnpm plugins:test
pnpm plugins:check
pnpm plugins:package
pnpm --dir web generate
Publishing sequence:
- From the repository root, validate and stage local catalog/ZIP artifacts:
pnpm plugins:locales pnpm plugins:test pnpm plugins:check pnpm plugins:package - Confirm
pnpm plugins:packageregeneratedweb/public/plugins/catalog.v2.jsonfrom the current official and community manifest v3 plugin lineup. Do not release if the checked-in v2 catalog still lists deprecated sample plugins instead of the current catalog. - Confirm
web/public/plugins/catalog.v1.jsonhasplugins: []and does not expose stale legacy plugins. - Upload plugin ZIPs to R2 and regenerate catalogs:
pnpm plugins:publish - Deploy web:
If the local web deploy times out during the large static upload, commit and push both root and nestedpnpm plugins:deployweb/repos, then trigger the remote deploy helper:
The helper SSHes to the remote checkout, force-resets it to./web/deploy.shorigin/main, and runsnpm run deployinside a tmux session. Remote reset is acceptable for this deployment lane because the remote checkout is disposable deploy state. - Verify live endpoints:
https://openpets.dev/plugins/catalog.v2.jsonhttps://openpets.dev/plugins/catalog.v1.json- each
https://zip.openpets.dev/plugins/<plugin-id>.zip
GitHub Release notes
The release script generates notes from the Git commit range between the previous desktop tag and the release commit. Do not keep static release-note text in the script or this guide; stale notes are worse than short generated notes.
The notes are written when the draft release is created, before publication, so
a risky release can be inspected on the draft rather than in a dry run. After
publishing, verify the GitHub Release body matches the actual commit range and
artifact set. If it does not, edit the release body immediately with
gh release edit v<version> --notes-file <file>.
Release mode and npm ordering
Choose the release mode before changing versions:
- Desktop-only release - only the Electron app and GitHub desktop artifacts
change. Bump
apps/desktop/package.json, do not publish npm packages, and run the Desktop gate. If the packaged app emits a new exact npm integration version, publish and verify that version before starting the desktop release. - Full shared-version release - public package contents and Desktop ship at one shared version. Pass the package and Desktop gates, publish the complete public npm package set, verify it on the registry, and only then promote the desktop release tag and run the desktop release.
The public npm package set is owned by the release tooling, not this document.
Use the pnpm release:npm plan to inspect the current set and publish order; do
not copy a package list into a release issue or treat a hardcoded list as
authoritative. The helper requires all packages in its current publish plan to
use one shared stable version.
NPM publishing is required when a public package changes, when the SDK should be available to plugin authors, or when a desktop release depends on a new exact npm integration version.
For a full shared-version release, the ordering is mandatory:
- Run the package and Desktop gates.
- Run
pnpm release:npm -- --yesand allow it to publish every missing package in its plan. - Verify every package/version in that plan resolves from
https://registry.npmjs.org. - Start
pnpm release:desktop -- --yesonly after npm publication and verification succeed.
Do not promote the desktop tag or publish its GitHub release while the npm gate is incomplete. The Desktop exact-integration checks remain a final safety check; they do not replace publishing and verifying the complete npm set for a full release.
Exact npm integration versions gate desktop releases
Packaged Desktop emits exact npm specs for its npm-backed integrations:
OpenCode plugin entries pin @open-pets/opencode@<version> and OpenClaw
install/update commands pin @open-pets/openclaw@<version>, where each version
is the current workspace package version. The invariant is: if Desktop emits an
exact npm version, release tooling guarantees that version exists.
Consequences:
- A desktop-only release is allowed only while those npm-backed integrations remain compatible with what is already published. If packaged Desktop would emit a new exact npm package version, that version must already be published before the desktop release is published.
- When the
@open-pets/opencodeor@open-pets/openclawversions changed, runpnpm release:npm -- --yesfirst, then the desktop release. - The desktop release enforces this with
--yesverification stages that probehttps://registry.npmjs.orgfor both exact specs. The earlyverify:npm-integrationsstage runs before any artifact is built and re-runs on every--yesinvocation instead of trusting an old checkpoint, because a published version can later be unpublished. The same specs are revalidated immediately before the irreversible boundaries:verify:npm-pre-tagruns before the tag is created, andverify:npm-pre-publishruns before the draft becomes published. A confirmed E404 aborts naming the missing spec; a registry/network/timeout failure also aborts, reported as a registry-check failure rather than as unpublished. A failed revalidation leaves staged artifacts and checkpoints in place so the release can be retried once npm is available.
Staged desktop releases
pnpm release:desktop -- --yes runs preflight once, then executes an ordered
stage plan. After each stage succeeds, the script appends it to a checkpoint
file:
apps/desktop/.release-state/v<version>.json
The checkpoint is gitignored and belongs to one version at one HEAD commit.
Tag promotion and partial releases
Tagging is a staged promotion boundary, not proof that the release is complete. For a full shared-version release, the npm publication and registry verification must finish before the desktop tag is promoted. Desktop packaging, signing, asset upload, and GitHub publication then proceed through their own checkpoints. Never publish a partial npm package set or a partial desktop artifact set.
If an npm package publish fails, already published versions remain valid; fix the
cause, re-run the same npm command, and let the helper skip versions that are
already present. Verify the whole plan after the retry. If a desktop stage fails,
earlier checkpointed work remains valid; re-run the identical desktop command and
use --from <stage> only when an earlier completed stage must be redone.
Do not delete or repoint a tag during recovery. Once a tag has been promoted, continue the release from its checkpoint. A published GitHub release is final; repair only its documented draft/retry path, never by replacing it with an unsigned or incomplete artifact set.
Stage plan
| Stage | What it does |
|---|---|
verify:npm-integrations | confirms exact @open-pets/opencode + @open-pets/openclaw versions exist on npm (--yes only, before any build; re-runs on every invocation, never checkpoint-skipped) |
checks | pnpm build and pnpm --filter @open-pets/desktop check |
clean | cleans apps/desktop/dist-electron (runs only once per checkpoint) |
build:mac-dmg | macOS DMG x64 + arm64 |
build:mac-zip | macOS ZIP x64 + arm64 |
build:linux-appimage | Linux AppImage x64 |
build:linux-deb | Linux DEB x64, rejected if under 1 MiB |
build:linux-rpm | Linux RPM x64, rejected if under 1 MiB |
build:linux-targz | Linux tar.gz x64 |
stage:linux-packages | only with --linux-package-dir; copies validated Ubuntu DEB/RPM |
verify:local | working-tree check plus the complete pre-signing artifact set |
verify:npm-pre-tag | re-verifies the exact npm specs immediately before tagging (--yes only, never checkpoint-skipped) |
tag | promotes the validated release by creating and pushing the annotated v<version> tag at HEAD |
sign:dispatch | dispatches the SignPath workflow and records its run id |
sign:collect | waits for that recorded run, downloads and verifies the signed installer |
verify:final | validates the signed artifact set and writes SHA256SUMS |
release:draft | creates or refreshes the draft GitHub Release |
release:upload | uploads only the assets GitHub is missing, then verifies the exact asset set |
verify:npm-pre-publish | re-verifies the exact npm specs immediately before publication (--yes only, never checkpoint-skipped) |
release:publish | publishes the verified draft |
Preflight still enforces macOS, pnpm/gh availability, GitHub CLI auth, an
origin pointing at alvinunreal/openpets, a clean working tree, an upstream
branch, HEAD matching upstream, and stable non-zero semver. It refuses an
existing tag or release unless the checkpoint says this release already reached
the tag stage at this HEAD, or both the local and origin tags already point
to this unpublished/draft release HEAD. The latter recovery path makes the
tag stage a no-op if a checkpoint was lost after tagging.
Resuming after a failure
Re-run the identical command. Completed stages are skipped:
pnpm release:desktop -- --yes
Two things make the resume trustworthy rather than merely fast:
- Each build stage records the size of the artifacts it produced. If an artifact was deleted or changed, that stage re-runs even though it is checkpointed.
sign:dispatchstores the SignPath workflow run id. A resume re-attaches to that same run instead of dispatching a second signing request, so a failure during download or upload never re-triggers signing or a second approval.
A checkpoint is discarded automatically when HEAD moves or the desktop version
changes, because the built artifacts no longer match the release. This is why a
commit made to fix a failing release — even a docs-only one — costs a rebuild:
the script will not publish artifacts built from a different commit than the tag.
Changing --include-experimental-arm or --linux-package-dir does not
discard anything. The stages that are still in the plan keep their artifacts, and
only the stages the options actually changed are re-run. Dropping a target can
leave its artifact behind in dist-electron; verify:local rejects any
unexpected artifact, so use --reset if you want a guaranteed clean rebuild.
Inspecting and controlling stages
pnpm release:desktop -- --yes --status
Prints the stage plan with done, stale, pending, or always for each
stage, plus the recorded SignPath run.
Force a stage and everything after it to re-run:
pnpm release:desktop -- --yes --from build:linux-rpm
pnpm release:desktop -- --yes --from sign:dispatch
Use --from sign:dispatch when the recorded SignPath run itself failed and a
fresh signing run is required. Discard the whole checkpoint with:
pnpm release:desktop -- --reset
--resume remains as a legacy flag for resuming a tagged HEAD when no
checkpoint exists (for example after the checkpoint file was deleted). It
requires local and origin v<version> tags to point to HEAD and refuses a
published release. Normal recovery no longer needs it.
Published releases are visible to the app update checker.
Default release assets
Default command for every desktop release:
pnpm release:desktop -- --yes
The final release artifact set always includes the full x64 artifact set. The local release script builds the macOS and Linux artifacts; GitHub Actions builds and SignPath-signs the Windows x64 installer:
- macOS DMG: x64 + arm64
- macOS ZIP: x64 + arm64
- Windows NSIS installer: x64, built and SignPath-signed by the workflow, then downloaded into the final artifacts
- Linux AppImage: x64
- Linux DEB: x64
- Linux RPM: x64
- Linux tar.gz: x64
The macOS release host validates the payload inside the actual Linux AppImage without executing it. Install its required SquashFS extractor before starting a desktop release:
brew install squashfs
Expected main artifacts look like:
OpenPets-<version>-mac-x64.dmg
OpenPets-<version>-mac-arm64.dmg
OpenPets-<version>-mac-x64.zip
OpenPets-<version>-mac-arm64.zip
OpenPets-<version>-win-x64-setup.exe (SignPath Authenticode-signed)
OpenPets-<version>-linux-x86_64.AppImage
OpenPets-<version>-linux-amd64.deb
OpenPets-<version>-linux-x86_64.rpm
OpenPets-<version>-linux-x64.tar.gz
SHA256SUMS
The old per-target optional flags were removed to avoid partial releases. The
experimental ARM flag remains optional, and --linux-package-dir is available
only for the validated Ubuntu DEB/RPM fallback described below:
pnpm release:desktop -- --yes --include-experimental-arm
On Apple Silicon macOS, Linux RPM packaging can fail in fpm/rpmbuild, and
Electron Builder can produce an invalid tiny DEB archive. The build:linux-deb
and build:linux-rpm stages reject a package smaller than 1 MiB, so this failure
stops the release at that stage instead of producing a partial artifact set. Do
not publish a partial release. Build valid DEB/RPM replacements inside the Ubuntu
VMware guest, place them in an external staging directory, and re-run with
--linux-package-dir. The script then skips the failing local DEB/RPM targets,
copies and validates the staged files into dist-electron, and continues only
with the complete final artifact set. Adding --linux-package-dir on a resume
keeps the macOS and AppImage artifacts that already built; only the DEB/RPM
stages are replaced. See
Linux DEB/RPM fallback via VMware.
--include-experimental-arm builds Windows ARM64 and Linux ARM64 locally. Only the Windows x64 installer is handed off to SignPath; the locally built unsigned Windows ARM64 installer remains disposable and is not uploaded. Only use this flag if the additional Linux artifact can be tested.
Windows code signing with SignPath
OpenPets has a production certificate through the SignPath Foundation program. Use SignPath for Windows Authenticode signing before publishing Windows release artifacts. SignPath's GitHub trusted-build integration requires signing inputs to be uploaded from a GitHub Actions workflow artifact, so the local macOS release script does not build the Windows x64 NSIS installer; it dispatches the workflow, which builds and signs it.
Public code-signing policy
The canonical public policy is https://openpets.dev/code-signing-policy. SignPath signing is limited to official OpenPets open-source release artifacts. The homepage, download page, and release pages must link to that policy. Update the policy whenever signing approvers, maintainer/committer/reviewer roles, or signing-related network handling changes.
Current repository support:
- Workflow:
.github/workflows/signpath-windows.yml - Output workflow artifact:
signed-openpets-windows-x64 - Production signing policy:
release-signing - App executable artifact configuration:
openpets-windows-app-exe-zip - NSIS installer artifact configuration:
openpets-windows-installer-zip - Signed files produced by the workflow:
- Nested app executable:
openpets.exe OpenPets-<version>-win-x64-setup.exeSHA256SUMS.windows.txt
- Nested app executable:
Note: Windows SmartScreen can still show a "not commonly downloaded" prompt for a newly signed OpenPets installer. That does not mean the signature is invalid; it usually means the file hash has little distribution history.
The workflow checks out submodules, then builds the Windows x64 unpacked app and its generated preloads on windows-latest, uploads openpets.exe for SignPath signing, replaces the unpacked app executable with the signed file, builds the NSIS installer from that signed app, uploads the installer for SignPath signing, then publishes the signed installer as a GitHub Actions artifact. The project is linked to the GitHub.com trusted-build system; its repository variables SIGNPATH_ORGANIZATION_ID and SIGNPATH_PROJECT_SLUG, plus the SIGNPATH_API_TOKEN secret, must remain configured.
Verification steps after download (before first run):
Get-FileHash .\OpenPets-<version>-win-x64-setup.exe -Algorithm SHA256
Get-AuthenticodeSignature .\OpenPets-<version>-win-x64-setup.exe | Format-List *
Only run the installer when the SHA-256 matches the release SHA256SUMS and the authenticode signature is valid.
SignPath setup checklist
These setup values are already configured. If the SignPath project or GitHub repository configuration is recreated, restore them before signing:
- Accept the SignPath OSS organization invitation.
- In SignPath, add the predefined trusted build system GitHub.com to the organization.
- Link the GitHub.com trusted build system to the OpenPets SignPath project.
- Install/authorize the SignPath GitHub App for
alvinunreal/openpetsif SignPath asks for source/build policy verification. - Create a SignPath project for OpenPets and note its project slug.
- Create or identify a signing policy slug. Start with the self-signed test certificate policy; switch to the production certificate policy after SignPath reviews the setup.
- Add this GitHub repository secret:
SIGNPATH_API_TOKEN- API token for a SignPath user with submitter permission for the project/signing policy.
- Add these GitHub repository variables:
SIGNPATH_ORGANIZATION_ID- SignPath organization ID.SIGNPATH_PROJECT_SLUG- SignPath OpenPets project slug.
SignPath artifact configurations
GitHub actions/upload-artifact stores each upload as a ZIP archive for SignPath, so each SignPath artifact configuration must use <zip-file> as the root element.
The unpacked app executable configuration is openpets-windows-app-exe-zip:
<artifact-configuration xmlns="http://signpath.io/artifact-configuration/v1">
<zip-file>
<pe-file path="openpets.exe">
<authenticode-sign />
</pe-file>
</zip-file>
</artifact-configuration>
The NSIS installer configuration is openpets-windows-installer-zip:
<artifact-configuration xmlns="http://signpath.io/artifact-configuration/v1">
<zip-file>
<pe-file path="OpenPets-*-win-x64-setup.exe">
<authenticode-sign />
</pe-file>
</zip-file>
</artifact-configuration>
Use test-signing only to validate SignPath setup. The normal --yes release command dispatches the workflow after the local build has succeeded and the annotated release tag has been pushed. It supplies these production inputs:
gh workflow run signpath-windows.yml --repo alvinunreal/openpets --ref v<version> \
-f signing_policy_slug=release-signing \
-f artifact_configuration_app_exe_slug=openpets-windows-app-exe-zip \
-f artifact_configuration_installer_slug=openpets-windows-installer-zip
The release script locates the newly dispatched run by workflow, tag ref, HEAD SHA, event, and dispatch time. It visibly waits for completion; if the run pauses during a SignPath approval step, a signer/approver must approve the request in the SignPath dashboard before the workflow can continue. The script does not assume that approval succeeds automatically.
After the workflow succeeds, the script downloads its signed-openpets-windows-x64 artifact to a temporary directory outside the repository. It requires exactly OpenPets-<version>-win-x64-setup.exe and SHA256SUMS.windows.txt, validates the handoff checksum, copies the signed installer into the final artifact directory, and then generates the release-wide SHA256SUMS. SHA256SUMS.windows.txt is not uploaded to the GitHub Release.
Recovery when the automated handoff is interrupted
If the initial signing step fails after the tag was pushed, do not delete the tag. Re-run the same command:
pnpm release:desktop -- --yes
The checkpoint keeps the tag stage, so preflight accepts the existing tag, and the release resumes at the failed signing stage. If the SignPath run itself failed, the recorded run id is no longer usable and the script says so; dispatch a fresh signing run with --from sign:dispatch. If the tag push itself failed, push that existing local tag to origin first. The script never deletes tags automatically.
When no checkpoint exists — for example the checkpoint file was deleted, or the release was started from another machine — use the legacy flag, which requires both local and origin v<version> tags to point to HEAD, accepts no release or a draft release, and refuses a published release:
pnpm release:desktop -- --yes --resume
For a narrowly scoped manual recovery when the script cannot dispatch the workflow, use the production dispatch shown above, download the named final artifact with gh run download, and use only its signed installer when repairing a draft release. Never upload the workflow's SHA256SUMS.windows.txt as a release asset, never upload a locally built unsigned Windows installer (including the optional ARM64 installer), and regenerate the release-wide SHA256SUMS after any replacement.
Full release procedure
1. Choose the next version
Use stable semver only:
2.0.0
2.0.1
2.1.0
3.0.0
Do not use 0.0.0 or prerelease tags unless the release script is intentionally changed.
2. Bump package versions
For a desktop-only release that changes only the Electron app and GitHub desktop artifacts, bump apps/desktop/package.json only. Do not bump or publish public npm packages unless their package contents changed.
Desktop-only releases may intentionally use a different version than the root workspace and public npm packages. The GitHub desktop release tag follows apps/desktop/package.json, and the app update checker reads GitHub Releases, not npm.
For a full workspace/npm release, update the root, Desktop, and every package in
the current pnpm release:npm publish plan together so bundled packages and npm
packages report the same release version. The release plan is authoritative for
the public package set; do not maintain a second hardcoded file list here.
Use a new version for every release artifact you publish. npm package versions are immutable, so any change to a published package requires a new version across all public OpenPets npm packages.
Set each top-level version field to the chosen version, for example:
"version": "2.0.1"
3. Install/update lockfile if needed
Run:
pnpm install
If pnpm-lock.yaml changes, include it in the version bump commit.
4. Run package and Desktop gates before committing
Run:
pnpm check
pnpm test
pnpm --filter @open-pets/desktop check
pnpm --filter @open-pets/desktop test
The package gate is pnpm check plus pnpm test; the Desktop gate is the
Desktop check and test commands above. Fix any failures before continuing.
5. Commit and push the version bump
Check status:
git status --short
Commit the version bump and any intentional release changes. For a desktop-only release, stage apps/desktop/package.json instead of every package manifest.
git add package.json apps/desktop/package.json packages/*/package.json pnpm-lock.yaml
git commit -m "release desktop v<version>"
git push
Only add files that are intentionally part of the release. Do not accidentally include unrelated worktree changes.
6. Confirm GitHub CLI auth
Run:
gh auth status --hostname github.com
If not authenticated:
gh auth login
7. Publish and verify npm for a full release
This step applies only to a full shared-version release. A desktop-only release skips it unless the exact npm integration gate requires a newly published version.
Inspect the current dynamic package plan, then publish all missing packages:
pnpm release:npm
pnpm release:npm -- --yes
Verify every package/version reported by the plan through the public npm
registry. If publishing stops part-way through, fix the cause and re-run the
same --yes command; existing versions are skipped. Do not proceed to desktop
tag promotion until the complete plan verifies.
For historical recovery, when the package versions come from an existing tag
instead of the current HEAD, use:
pnpm release:npm -- --yes --ref vX.Y.Z
This is a recovery path for that tagged release, not a way to bypass the package gate or publish a mixed-version set.
8. Do not dry run the desktop release
Do not run pnpm release:desktop -- --dry-run as a warm-up. A dry run builds
the full macOS and Linux artifact set, which is the slowest part of a release,
and then stops without tagging or publishing. Since the dry run and the real
release are separate invocations of the same build stages, the time is spent
twice for no additional safety.
The staged checkpoint already provides what a dry run used to provide: preflight
runs before anything is built, and any failure is resumable without repeating
finished work. Go straight to --yes.
The --dry-run flag still exists for the rare case where you want local
artifacts and SHA256SUMS.local-preview without any GitHub interaction at all.
It shares the build-stage checkpoint with a real release, so a dry run
immediately followed by --yes at the same HEAD will not rebuild. It is still
not part of the normal release path.
If preflight fails because the tree is dirty, inspect:
git status --short
The release script requires a clean tree before release creation.
9. Build, sign, verify, and publish the GitHub Release
For the standard full-artifact desktop release:
pnpm release:desktop -- --yes
The script works through the stage plan: it builds locally while the tag does not
exist, creates and pushes an annotated tag, dispatches the production SignPath
workflow and records its run id, waits for that run's signed artifact, adds the
signed Windows x64 installer to the final artifacts, calculates SHA256SUMS,
creates a draft release, uploads the assets GitHub is missing, verifies the
exact remote asset names, and publishes the release named/tagged:
v<version>
Example:
v2.0.1
If SignPath pauses for approval, approve the request in the SignPath dashboard; the script continues waiting and fails if the workflow does not succeed. If a signing or upload failure leaves the tag pushed, recover by re-running the same command:
pnpm release:desktop -- --yes
The checkpoint skips the finished stages and resumes at the failed one. Inspect
what will run first with --status, and use --from <stage> when a completed
stage must be redone.
10. Smoke test after publishing
After publishing the release, manually test at least:
- macOS DMG on the current Mac.
- Windows installer on a Windows machine or VM.
- Linux AppImage on a Linux machine or VM.
Warnings behavior to expect:
- macOS may show Gatekeeper warnings.
- Windows may show SmartScreen reputation warnings on first launch, even for signed installers. This is usually reduced after repeated trustworthy downloads.
Linux release-smoke VM
Use the clean Ubuntu release-smoke VM for Linux artifact install checks that should behave like a normal user machine, not the development VM with a repo checkout and build dependencies.
The VM is documented in /Volumes/external/repos/vagrants.md:
VM directory: /Volumes/external/vmware/ubuntu24-release-smoke
Provider: vmware_desktop / VMware Fusion
Guest OS: Ubuntu 24.04 ARM64
SSH: 127.0.0.1:2200 when the main Ubuntu VM already owns 2222
Start and enter the VM from macOS:
cd /Volumes/external/vmware/ubuntu24-release-smoke
vagrant up
vagrant ssh
This VM intentionally does not mount the macOS OpenPets checkout. Use it to download and install released Linux artifacts from GitHub/R2 like a user would.
Smoke checklist inside the VM:
- Download/install the current Linux release artifact.
- Launch OpenPets from the installed artifact, not from a repo checkout.
- Confirm the tray icon appears.
- Confirm a pet window appears.
- Open Control Center.
- Confirm the live plugin catalog loads from
https://openpets.dev/plugins/catalog.v2.json. - Confirm community plugins, including
openpets.spotify-buddy, appear as installable when the live catalog includes them. - Install, enable, and open configuration for at least one plugin without crashes or raw
$t:strings.
The existing /Volumes/external/vmware/ubuntu24 VM remains the Linux development
VM. Prefer ubuntu24-release-smoke for fresh-user release validation, and use
the dev VM only for build/debug workflows.
Common failure modes
Version is 0.0.0
Fix apps/desktop/package.json and the other workspace package versions.
Dirty working tree
The release script refuses to create releases from a dirty checkout. Commit, stash, or revert changes first.
HEAD is not pushed
Push the current branch before releasing:
git push
Tag or release already exists
For a normal release, use a new version after inspecting GitHub. If this is a
failed release attempt, re-run pnpm release:desktop -- --yes; the checkpoint
recognises the tag it created. If the checkpoint is gone and local/origin
v<version> both point to HEAD, use pnpm release:desktop -- --yes --resume.
Do not delete tags automatically.
Partial GitHub upload failure or replacing an existing release's assets
The script keeps the release draft until the complete final asset set is uploaded and verified. Assets are uploaded one at a time, and an asset already present on the draft with a matching size is skipped, so a re-run only transfers what is actually missing. If an upload fails:
- Inspect the release on GitHub.
- Re-run the same command:
pnpm release:desktop -- --yes
- Re-check the release asset list; do not trust a wrapper's success summary if
gh release viewshows missing assets. - Never repair a public release with an unsigned Windows installer or
SHA256SUMS.windows.txt. If the script is unavailable, manually upload only the verified final assets to the existing draft with--clobber, then verify the exact set before publishing.
Manual packaging smoke commands
These do not create a GitHub Release and cover the locally built macOS/Linux package smoke targets. The Windows x64 NSIS installer is built and signed by the GitHub Actions workflow; test the signed workflow or release artifact instead of building a disposable local x64 installer.
pnpm --filter @open-pets/desktop build
node apps/desktop/scripts/clean-package-output.cjs
pnpm --dir apps/desktop exec electron-builder --mac dmg --x64 --publish never
pnpm --dir apps/desktop exec electron-builder --mac dmg --arm64 --publish never
pnpm --dir apps/desktop exec electron-builder --linux AppImage --x64 --publish never
pnpm --dir apps/desktop exec electron-builder --linux rpm --x64 --publish never
Artifacts are written to:
apps/desktop/dist-electron/
Linux DEB/RPM fallback via VMware
Use this flow when the local macOS release host cannot produce valid Linux DEB
or RPM artifacts. A common macOS failure mode is RPM failing under
fpm/rpmbuild, or Electron Builder producing a tiny invalid DEB archive.
Building the Linux package targets inside the Ubuntu VMware guest should produce
valid x64 artifacts.
The DEB failure is silent: Electron Builder logs building target=deb and exits
successfully, but writes a ~96 byte file. That file is a macOS ar stub rather
than a Debian package, which you can confirm from its header:
xxd apps/desktop/dist-electron/OpenPets-<version>-linux-amd64.deb | head -4
# !<arch> ... __.SYMDEF SORTED <- macOS static library, not a .deb
It happens because Apple's BSD ar is used when dpkg/dpkg-deb/fpm are not
installed on the host. The build:linux-deb stage now rejects any package under
1 MiB, so this stops the release at that stage instead of reaching artifact
validation. Do not try to fix it by installing packaging tools on macOS; use the
Ubuntu guest, which is the validated path.
The VM is documented in /Volumes/external/repos/vagrants.md:
VM directory: /Volumes/external/vmware/ubuntu24
Guest checkout: /home/vagrant/src/openpets
Provider: vmware_desktop / VMware Fusion
Start and prepare the VM from macOS:
cd /Volumes/external/vmware/ubuntu24
vagrant up
vagrant ssh -c 'set -e; cd /home/vagrant/src/openpets; git fetch origin --tags; git checkout main; git pull --ff-only; git submodule update --init --recursive'
vagrant ssh -c 'set -e; sudo apt-get update; sudo apt-get install -y rpm fakeroot'
Build only the Linux package targets in the guest:
vagrant ssh -c 'set -e; cd /home/vagrant/src/openpets; git submodule update --init --recursive; pnpm install --frozen-lockfile; pnpm --filter @open-pets/desktop build; cd apps/desktop; node scripts/clean-package-output.cjs; pnpm exec electron-builder --linux deb --x64 --publish never; pnpm exec electron-builder --linux rpm --x64 --publish never; ls -lh dist-electron/OpenPets-<version>-linux-amd64.deb dist-electron/OpenPets-<version>-linux-x86_64.rpm; file dist-electron/OpenPets-<version>-linux-amd64.deb dist-electron/OpenPets-<version>-linux-x86_64.rpm'
Copy the valid artifacts back through the VM's /vagrant share, then place
them in an absolute host staging directory. Do not put them in
apps/desktop/dist-electron/; the release script cleans that directory and
copies the validated files into it itself:
vagrant ssh -c 'set -e; cp /home/vagrant/src/openpets/apps/desktop/dist-electron/OpenPets-<version>-linux-amd64.deb /vagrant/; cp /home/vagrant/src/openpets/apps/desktop/dist-electron/OpenPets-<version>-linux-x86_64.rpm /vagrant/'
STAGING_DIR="/absolute/path/openpets-linux-packages/<version>"
mkdir -p "$STAGING_DIR"
cp /Volumes/external/vmware/ubuntu24/OpenPets-<version>-linux-amd64.deb "$STAGING_DIR/"
cp /Volumes/external/vmware/ubuntu24/OpenPets-<version>-linux-x86_64.rpm "$STAGING_DIR/"
Run the complete release flow with the staging directory. Do not precede it with a dry run:
pnpm release:desktop -- --yes --linux-package-dir "$STAGING_DIR"
This is normally a resume: the macOS, AppImage, and tar.gz artifacts built before
the DEB/RPM failure are kept, and only the stage:linux-packages copy replaces
the failed local package builds. Keep the flag on every subsequent resume of the
same release, since dropping it puts the failing local DEB/RPM stages back into
the plan.
The option requires exactly these two files, rejects symlinks and packages
smaller than 1 MiB, skips only the local DEB/RPM builds, and copies the files under
dist-electron before strict artifact validation. This remains a full release:
do not publish a partial set or upload the staged files directly. If using
--include-experimental-arm, keep it on every run too; the unsigned Windows
ARM installer remains disposable and is not published.
Microsoft Store package quick actions
Use this flow when Partner Center rejects the unsigned Win32 .exe installer under Store policy 10.2.9. GitHub Releases should still prefer the NSIS setup .exe; Microsoft Store submission should use the Store package artifact.
Important Partner Center routing:
- Do not paste an
.appxURL into the standalone.exe/.msipackage URL field. That field is only for signed Win32 installers. - Start a Microsoft Store MSIX/AppX package submission and upload the
.appxpackage directly. - If reusing the same app name from a failed Win32 submission is blocked, delete/abandon the Win32 package flow and recreate the submission as MSIX/AppX.
Electron Builder v26 uses the Windows Store target name appx. There is no separate msix target in this project setup; Partner Center accepts AppX/MSIX-family uploads.
AppX tile assets are separate from win.icon/app-icon.ico. Keep branded tile assets in apps/desktop/build/appx/; if these files are missing, Electron Builder falls back to its bundled SampleAppx.*.png placeholders and Microsoft Store certification rejects the package as using default tile images.
Required OpenPets AppX tile assets:
apps/desktop/build/appx/StoreLogo.png
apps/desktop/build/appx/Square44x44Logo.png
apps/desktop/build/appx/Square150x150Logo.png
apps/desktop/build/appx/Wide310x150Logo.png
Additional branded assets currently included:
apps/desktop/build/appx/SmallTile.png
apps/desktop/build/appx/LargeTile.png
apps/desktop/build/appx/BadgeLogo.png
apps/desktop/build/appx/SplashScreen.png
These assets are generated from apps/desktop/assets/app-icon.png plus OpenPets-branded tile art. Do not delete or rename them unless the AppX manifest/build config is updated at the same time.
Build a Windows x64 AppX package:
pnpm --filter @open-pets/desktop build
pnpm --filter @open-pets/desktop exec electron-builder --win appx --x64 \
-c.appx.identityName=AlvinUnreal.OpenPetsDesktopCompanion \
-c.appx.publisher=CN=5749BA4D-6A45-4111-8CAA-6B151AEDC238 \
-c.appx.publisherDisplayName=AlvinUnreal \
-c.appx.displayName="OpenPets: Desktop Companion" \
-c.appx.applicationId=OpenPetsDesktopCompanion
publisherDisplayName must match the exact publisher display name shown by Partner Center. For the current Store account this is:
AlvinUnreal
If Partner Center reports The PublisherDisplayName element ... doesn't match your publisher display name, rebuild the AppX with the correct -c.appx.publisherDisplayName=<Partner Center publisher display name> value.
Partner Center validates AppX identity against the reserved Store product identity. For the current Store reservation, the expected values are:
identityName: AlvinUnreal.OpenPetsDesktopCompanion
package family name: AlvinUnreal.OpenPetsDesktopCompanion_aq5mzr83863gr
publisher: CN=5749BA4D-6A45-4111-8CAA-6B151AEDC238
displayName: OpenPets: Desktop Companion
applicationId: OpenPetsDesktopCompanion
If Partner Center reports Invalid package identity name, Invalid package family name, Invalid package publisher name, or an unreserved Package/Properties/DisplayName, rebuild using the exact values above. The package family name is derived from identityName and publisher, so do not set it manually.
Expected artifact:
apps/desktop/dist-electron/OpenPets-<version>-win-x64.appx
On macOS, AppX packaging runs Windows makeappx.exe through Parallels. If the repo is on an external drive and the build fails with prlctl process failed 2 or a \\Mac\\Host\\Volumes\\... path error, either enable Parallels shared folders for all Mac disks or copy the repo to a Parallels-accessible home-folder path and build there.
If Electron Builder creates the AppX staging folder but fails only at the final makeappx.exe step because Parallels cannot resolve \\Mac\\Host paths, a manual fallback is:
- Copy the Electron Builder
winCodeSigncache into the accessible build folder. - Rewrite
dist-electron/__appx-x64/mapping.txtpaths from\\Mac\\Host\\Users\\<user>toC:\\Mac\\Home. - Run
makeappx.exe packfrom the Windows VM against the rewritten mapping file.
Keep session-specific workaround paths and checksums in the release issue or PR notes for that version, not in this evergreen guide.
Verify the final AppX contains OpenPets tile assets, not Electron Builder sample defaults:
python3 - <<'PY'
from zipfile import ZipFile
appx = 'apps/desktop/dist-electron/OpenPets-<version>-win-x64.appx'
with ZipFile(appx) as z:
for name in [
'assets/StoreLogo.png',
'assets/Square44x44Logo.png',
'assets/Square150x150Logo.png',
'assets/Wide310x150Logo.png',
'assets/SmallTile.png',
'assets/LargeTile.png',
'assets/BadgeLogo.png',
'assets/SplashScreen.png',
]:
print(name, z.getinfo(name).file_size)
PY
Partner Center may warn that the restricted capability runFullTrust requires approval. This is expected for Electron desktop bridge/AppX packages because the manifest uses EntryPoint="Windows.FullTrustApplication" and rescap:Capability Name="runFullTrust". The warning must be acknowledged or approved in Partner Center; it is not fixed by changing the URL or repackaging as a standalone .exe.
Upload the Store package to the public R2-backed download host:
bunx wrangler r2 object put \
"openpets/releases/OpenPets-<version>-win-x64.appx" \
--file "apps/desktop/dist-electron/OpenPets-<version>-win-x64.appx" \
--remote
Public URL shape:
https://zip.openpets.dev/releases/OpenPets-<version>-win-x64.appx
Verify before submitting to Partner Center:
curl -I "https://zip.openpets.dev/releases/OpenPets-<version>-win-x64.appx"
R2 upload is optional for Partner Center MSIX/AppX submissions because the Store package flow accepts direct file upload. Use R2 only as a backup/share URL or for internal handoff.
NPM package release
The npm release helper determines the current public package set and dependency order. Its plan is the source of truth; do not hardcode a package list in release documentation or publish a hand-selected subset.
Publish all packages in that plan together at one shared stable version whenever any public package changes. This prevents exact-version dependencies from resolving to a mixed or incomplete release. The helper rejects mixed versions before publishing.
Dry-run npm publishing first:
pnpm release:npm
Publish all missing packages to npm. Package versions that already exist on npm
are skipped automatically, so a failed partial publish can be retried safely.
The helper pins its npm authentication check, registry probes, and pnpm publish
commands to https://registry.npmjs.org. Registry, process, network, and
authentication failures stop the release rather than being treated as missing
packages. npm can take time to expose a newly written package version or
dist-tag; after each staging and final-tag write, the helper reports the missing
entries and retries registry verification every 10 seconds for up to five
minutes. It still aborts on an actual registry, process, or authentication
failure:
pnpm release:npm -- --yes
If npm requires two-factor auth:
pnpm release:npm -- --yes --otp <code>
Publishing with the npm helper requires npm whoami --registry https://registry.npmjs.org to succeed, a clean working tree, and local HEAD to match the upstream branch.
After publishing, verify every package/version in the printed publish plan with the public npm registry. Do not consider a release complete while any plan entry is missing. For the CLI package, also run its versioned help smoke command when it is included in the plan:
npx -y <cli-package>@<version> --help
Important notes for future maintainers
- Do not publish from an uncommitted local state.
- Never use
--skip-checkson a live release. In particular, do not combine it with--yesfor eitherrelease:npmorrelease:desktop; live releases must pass their package/Desktop gates. - For a full shared-version release, publish and verify the complete dynamic npm plan before promoting the desktop tag.
- Do not use the desktop
--dry-runas a release warm-up. It doubles the build time; the staged checkpoint already makes desktop retries cheap. The npm command without--yesis still useful for inspecting its dynamic publish plan. - Recover from a partial npm release by re-running the same
release:npm -- --yescommand; already published versions are skipped. Recover from a desktop failure by re-running the samerelease:desktop -- --yescommand, and reach for--from <stage>only when a completed stage must be redone. - Do not delete or repoint a promoted tag during recovery.
--dry-runis local only; it does not create tags, dispatch SignPath, or change GitHub.--resumeis a legacy fallback for a taggedHEADwith no checkpoint; it refuses published releases.- The checkpoint under
apps/desktop/.release-state/is disposable local state. Delete it with--resetif a release is abandoned. - Do not upload the entire
dist-electrondirectory manually. Upload only final top-level artifacts andSHA256SUMS. - Do not upload
SHA256SUMS.windows.txtor a locally built unsigned Windows installer, including the optional ARM64 installer. - Keep the tag format as
v<version>. - Keep
publish: nullinelectron-builder.yml; GitHub release upload is handled by the local script. - Windows icon is
apps/desktop/assets/app-icon.ico. - macOS icon is
apps/desktop/assets/app-icon.icns. - Windows artifacts are signed in the release handoff, but Windows SmartScreen reputation warnings may still appear on first run.
- macOS artifacts may still show Gatekeeper warnings until notarization is configured.