| name | release |
| description | Prepare, validate, tag, publish, and monitor guarded PwrSnap desktop releases. Use when the user asks to release PwrSnap, select alpha/beta/maintenance-candidate/RC/stable versions, prepare a vX.Y.Z or vX.Y.Z-prerelease tag, update release notes or CHANGELOG.md for a desktop release, verify package.json/tag/changelog alignment, trigger the macOS signed/notarized release workflow, or inspect release workflow status. |
Release
Use this skill for PwrSnap desktop releases published by
.github/workflows/release.yml.
Read First
Read these files before changing release metadata:
- ../../../docs/desktop-release-runbook.md
- ../../../.github/workflows/release.yml
- ../../../scripts/check-desktop-release-metadata.mjs
Guardrails
-
Release from main for the active next-version train, or from a long-lived
maintenance branch named releases/<major>.<minor> for a stable promotion
explicitly requested from an accepted prerelease or for patch releases on a
prior train. Do not include the patch component in maintenance branch names:
use releases/1.0, not releases/1.0.x or releases/1.0.1.
-
Start from a clean working tree. If tracked files are dirty, stop and ask
before changing release metadata.
-
Fetch tags before planning:
git fetch origin --tags
-
Treat apps/desktop/package.json as the desktop release version source.
The root package.json version is not the desktop app release version.
-
Always use a leading-v tag such as v0.0.1-alpha.5.
-
The tag version, apps/desktop/package.json version, and
CHANGELOG.md release heading must match.
-
Before moving main to a new major/minor train, verify that the prior train's
maintenance branch exists. For example, before preparing 1.1.0-beta.1 from
a current 1.0.* main, check for origin/releases/1.0. If it is missing,
stop and ask whether to create it from the current prior-train release commit
before bumping version metadata.
-
Patch releases for an existing train must land on that train's branch. For
example, prepare v1.0.1 on releases/1.0, not on main.
-
Do not create or push the tag until the version and changelog are committed
and present on the intended release branch.
-
Before pushing a release tag, verify the apple-signing GitHub Environment
exists on pwrdrvr/PwrSnap, requires reviewer approval, is scoped to v*
release tags, and has the Apple signing/notarization secrets required by
the workflow. Apple signing/notarization secrets must NOT exist as
repository-level secrets.
-
Do not use GitHub generated release notes as the final notes.
-
Do not create the GitHub Release by hand before the build succeeds. Let
the CI publish job create it from the signed/notarized build and matching
changelog notes.
-
Do not force-push the default branch or rewrite an existing release tag
without explicit user approval.
-
Keep the MIT license intact: do not swap LICENSE for a different SPDX or
drift any workspace package.json away from "license": "MIT".
Select SemVer Separately From GitHub Release State
Make two independent decisions before editing metadata:
- SemVer version channel controls the app's baked version, the tag, and
the changelog heading. They must be the same value, except for the tag's
leading
v.
- GitHub Release creation state is always
isPrerelease=true. The
workflow's --prerelease is mandatory even for a bare stable SemVer tag;
promote a release to Latest only as an explicit later action after assets
and smoke checks pass. Never infer this GitHub flag from the SemVer suffix.
Choose the SemVer form by source branch and intent:
| Source branch and intent | Matching package / tag / heading version |
|---|
main alpha | N.N.N-alpha.N |
main beta | N.N.N-beta.N |
releases/N.N smoke, install, or onboarding candidate | N.N.P-prerelease.N |
releases/N.N later release candidate | N.N.P-rc.N |
releases/N.N accepted stable release | N.N.P |
Keep the same N.N.P while advancing a maintenance candidate from
-prerelease.N to -rc.N; do not consume bare patch versions for candidates.
For example, failed 1.0.0-prerelease.1 and unpromoted 1.0.0-rc.1 do not
justify jumping the first stable release to 1.0.4. Create or tag bare
N.N.P only after the user explicitly says the candidate is accepted after
smoke checks.
The metadata checker compares the release tag with apps/desktop/package.json
and the CHANGELOG.md heading. Therefore, an alpha commit cannot also receive
a literal beta tag without changing metadata. If “beta” means the same
application source, make a signed metadata-only commit that changes the
package version and changelog heading to N.N.N-beta.N, then tag that commit.
If the user instead requires the exact alpha SHA to be tagged as beta, stop and
ask them to choose whether to change checker/workflow semantics before tagging;
the current checker rejects that exact-SHA beta tag.
Release Branch Preflight
For every release, identify RELEASE_BRANCH before editing files:
- Active-train alpha or beta:
main.
- Maintenance candidates, release candidates, accepted stable releases, and
patches:
releases/<major>.<minor>.
When an accepted beta is promoted to its first stable release while main
continues toward newer work, create the tracking branch from that exact signed
beta tag before preparing stable metadata:
git fetch origin --tags
git switch -c releases/<major>.<minor> v<major>.<minor>.<patch>-beta.<n>
git push -u origin releases/<major>.<minor>
For later patch releases, start from the existing train branch. Before main
starts a new major/minor train, verify that the prior train's maintenance
branch exists:
git ls-remote --heads origin releases/<current-major>.<current-minor>
If it is missing, ask before proceeding. Create it at the exact prior release
tag, then bump main to the new train.
Prepare Release Metadata
-
Determine the next version from the previous tag and user intent:
git tag --sort=-version:refname | head -n 10
gh release list --limit 10
-
Update apps/desktop/package.json without creating a tag yet:
pnpm --filter @pwrsnap/desktop version <version> --no-git-tag-version
If that command is not available in the current pnpm version, edit only
apps/desktop/package.json and preserve JSON formatting.
-
Add a top CHANGELOG.md entry:
## v0.0.1-alpha.5 - YYYY-MM-DD
Write release notes for users/operators, not as commit summaries. Preserve
the same substance in GitHub release notes.
Each bullet must use this shape:
- <Feature Area> - <Added|Improved|Fixed> <user-visible behavior and why it matters>.
Good examples:
- Composer - Improved complex Markdown pastes with lists, inline code, and nested code blocks.
- Thread Search - Escape now dismisses search, pairing naturally with Cmd/Ctrl+Shift+F to open it.
- Thread List Pull Request Info - Merged PR commits no longer show as unpushed work.
- Minor - Dependency updates and small UI polish.
Avoid vague bullets that only summarize the commit mechanic, such as
"Improved paste handling", "Added Escape-key handling", "Fixed progress
chips", or "Updated dependencies". A good note answers: what
feature/surface changed, whether it was Added/Improved/Fixed, and what
user-visible behavior changed. Roll low-value maintenance-only items into
Minor - ... unless they affect installs, updates, data safety, or a major
workflow.
-
Run the metadata gate locally before committing:
Commit, Land, And Tag
Commit the version and changelog together. Use a signed commit; this repo's git
config should already sign commits with SSH.
git add apps/desktop/package.json CHANGELOG.md
git commit -m "chore(release): prepare v<version>"
Preferred fast path: the release branch is protected by a ruleset (non_fast_forward,
deletion, required Lint/Build/Test/Desktop E2E checks) with
Repository admin bypass. If the user has Repository admin on pwrdrvr/PwrSnap,
push the signed release metadata commit directly. This avoids running PR CI
and then running the same gates again from the release tag.
git push origin HEAD:<RELEASE_BRANCH>
git fetch origin <RELEASE_BRANCH> --tags
git pull --ff-only
Fallback path: if the user does not have admin bypass or the direct push is
rejected, push the release metadata commit to a short-lived topic branch,
open a PR, wait for required checks, then squash merge the PR. Do not use
rebase merge for release metadata PRs: GitHub may rewrite the commit SHA,
which makes it too easy to tag the pre-merge commit instead of the actual
release-branch commit.
Remember that a GitHub squash merge creates a GitHub-authored commit on
RELEASE_BRANCH, not the original locally signed commit. If the user requires
the release metadata commit on the release branch itself to be locally signed,
use the direct-push path or ask before using the PR fallback.
git switch -c release/v<version>
git push -u origin release/v<version>
gh pr create --base <RELEASE_BRANCH> --head release/v<version> \
--title "chore(release): prepare v<version>" \
--body-file .local/PR-v<version>.md
gh pr checks <pr-number> --watch --interval 10
gh pr merge <pr-number> --squash --delete-branch
git fetch origin <RELEASE_BRANCH> --tags
git switch <RELEASE_BRANCH>
git pull --ff-only
After the direct push or squash merge, rerun the metadata gate on
RELEASE_BRANCH, then create exactly one tag on the actual release-branch
commit.
RELEASE_TAG=v<version> pnpm release:check
If signing tags is configured and works locally, prefer a signed annotated tag:
git tag -s v<version> -m "v<version>"
If signed tags are not available and the user approves an unsigned release tag,
create a lightweight tag instead:
git tag v<version>
Do not silently fall back from a failed signed tag to an unsigned tag. Ask the
user which tag form to use. Before pushing, verify the tag points at
origin/<RELEASE_BRANCH> or the intended release-branch commit:
git tag -v v<version>
git merge-base --is-ancestor v<version> origin/<RELEASE_BRANCH>
Publish
Push the tag after the release metadata is already on RELEASE_BRANCH:
git push origin v<version>
The tag push triggers Release Desktop (macOS universal). The workflow must
pass Check release metadata in the no-secret Test and prepare signing input
job before the environment-gated Sign, notarize, publish job can request
approval and access Apple signing secrets.
A normal tag push uses the v* deployment policy. A releases/* environment
policy only permits a manual workflow_dispatch retry whose ref is a
maintenance branch; do not treat that retry allowance as a preflight blocker
for a normal tag-triggered release.
For a manual dispatch retry, verify the tag already exists on GitHub and use
the maintenance branch as the workflow ref:
git ls-remote --tags origin v<version>
gh workflow run release.yml --ref <RELEASE_BRANCH> -f tag=v<version>
Monitor And Verify
Find the run for the release tag and watch it. If it takes a while to appear,
sleep for 5-10 minutes before deciding it failed to start.
gh run list --workflow release.yml --limit 10
gh run watch <run-id>
The Sign, notarize, publish job pauses for apple-signing Environment
approval. Treat that pause as expected. Before approving, verify the workflow
run is for the intended tag, the tag points at the intended release-branch
commit, and the version/changelog metadata match the tag.
A delegated monitor that stops at the apple-signing approval gate has not
completed the release. Resume monitoring after approval and continue until the
workflow succeeds or fails. Release completion requires the post-publish
release-notes step to run after the assets are uploaded.
On failure, inspect logs yourself:
gh run view <run-id> --log-failed
After success, verify the release and generated assets:
gh release view v<version>
gh release download v<version> --dir .local/release/v<version>
ls .local/release/v<version>
Expect signed/notarized universal macOS assets, including DMG/ZIP files and
latest-mac.yml.
The workflow automatically replaces electron-builder's generated/empty GitHub
Release body with the matching CHANGELOG.md entry after publishing assets.
Verify that the body is present and the release is still marked as a GitHub
Pre-release before calling the release done:
body_length="$(gh release view v<version> --repo pwrdrvr/PwrSnap --json body --jq '.body | length')"
test "$body_length" -gt 0
is_prerelease="$(gh release view v<version> --repo pwrdrvr/PwrSnap --json isPrerelease --jq '.isPrerelease')"
test "$is_prerelease" = true
If the automated notes step did not run or must be repaired manually, extract
the notes with the metadata checker, edit the release, and read the body /
pre-release state back:
node scripts/check-desktop-release-metadata.mjs \
--tag v<version> \
--notes-file .local/release-v<version>-notes.md
gh release edit v<version> --repo pwrdrvr/PwrSnap --notes-file .local/release-v<version>-notes.md
gh release view v<version> --repo pwrdrvr/PwrSnap --json body --jq '.body | length'
gh release view v<version> --repo pwrdrvr/PwrSnap --json isPrerelease --jq '.isPrerelease'
Local Fallback
Use the local path only when CI is unavailable or the user explicitly asks for
local signing/notarization. Follow
../../../docs/desktop-release-runbook.md
for required Apple and GitHub secrets.
pnpm --filter @pwrsnap/desktop package:dryrun
pnpm --filter @pwrsnap/desktop package
pnpm --filter @pwrsnap/desktop release