Releasing¶
Release pipeline¶
Releases are driven by GitHub Actions (.github/workflows/release.yml). The
pipeline runs on a version tag (v*) and proceeds in this order:
- Verify the tag matches every version (
scripts/check-version.mjschecks thepackage.jsonfiles,tauri.conf.json, and the Cargo workspace version). - Build and package the desktop app per platform in a build matrix, using
tauri buildviatauri-action. Artifacts are signed where the relevant signing secrets are configured. - Publish a GitHub release with notes generated from
CHANGES.md(scripts/release-notes.mjs) and every built artifact attached.
Continuous checks run separately on every push and pull request via
.github/workflows/ci.yml: the TypeScript side runs pnpm lint,
pnpm typecheck, and coverage-gated tests, and the Rust side runs
cargo fmt --check, cargo clippy, and cargo test. See
Development for the local gate commands.
Per-platform packaging¶
Packaging is done by the Tauri bundler (tauri build). The bundle targets are
declared in apps/desktop/src-tauri/tauri.conf.json and are: app, dmg,
deb, appimage, nsis, msi.
| Platform | Artifact(s) |
|---|---|
| Linux | AppImage, deb |
| macOS | dmg + .app (signed and notarized when credentials are provided as CI secrets) |
| Windows | nsis installer + msi (Authenticode-signed when a certificate secret is set) |
Application icons are generated from the sources in assets/icons/
(icon-default.png / icon-dark.png plus the renderer glyph SVGs) by
scripts/gen-icons.mjs (pnpm run icons), which writes the platform icon set
into apps/desktop/src-tauri/icons/. That directory is git-ignored; the desktop
crate's build.rs regenerates it when missing, and CI regenerates it before the
build.
Windows MSIX (best-effort)¶
In addition to the nsis and msi bundles, the release workflow packages the
built executable into an MSIX with the Windows SDK MakeAppx tool, for optional
distribution through the Microsoft Store. This step is best-effort
(continue-on-error): it is skipped when the executable or MakeAppx is
unavailable and never fails the release. The MSIX is uploaded alongside the
other artifacts and is unsigned (the Store signs it on ingest).
Branching and release flow¶
feature/* -> develop -> main (Merge Request)
- All feature work lands in
developvia feature branch Merge Requests. - When ready to release, open a Merge Request from
developtomain. - A release commit on
mainprepares the version (bump the version in everypackage.json,tauri.conf.json, and the Cargo workspace so they all match; updateCHANGES.mddevelopment bullets to a versioned heading). The release pipeline verifies these agree with the tag. - Tag the release commit:
git tag v1.0.0. - Push the tag to trigger the release pipeline.
Direct commits to main are allowed only until the first release. After v1,
all changes enter main only via Merge Request.
Release candidates are tagged on develop¶
A release candidate exists to exercise this pipeline -- the four-platform build,
the asset names, the generated notes, the installers -- before the work reaches
the release branch. Requiring a merge to main first would defeat that and fill
main with candidates, so:
| Tag | Cut from | Published as |
|---|---|---|
v<version>-rc.<n> |
develop |
GitHub pre-release |
v<version> |
main |
full release |
Only RC tags may come from develop. scripts/check-tag-branch.mjs runs in the
pipeline's first job and fails a final tag that is not on main, before anything
is built or published.
Three properties make an RC safe to publish from develop:
- The workflow marks any tag containing
-as a pre-release, andreleases/latest/download/...-- the URL both one-line installers use -- never resolves to a pre-release. Users oninstall.shwill not pick up an RC. - The docs workflow ignores pre-release tags entirely, so an RC neither becomes
the site's
latest(which the bare documentation URL redirects to) nor adds a throwaway entry to the version switcher. A candidate's docs are in-progress docs, already published under thedevalias frommain. - Nothing else consumes the release except by explicit tag.
Cutting an RC therefore looks like:
git checkout develop
node scripts/bump-version.mjs 1.0.0-rc.1
git commit -am "release: 1.0.0-rc.1"
git tag -a v1.0.0-rc.1 -m "SkillKeeper 1.0.0-rc.1"
git push origin develop
git push origin v1.0.0-rc.1
CHANGES.md¶
CHANGES.md is a per-version list of short bullet points grouped into:
- Breaking Changes
- Features
- Fixes
New work lands under a top Development heading during development. At
release, those bullets are moved under the new version heading. Each bullet is
short; large items link to documentation rather than describing details inline.
macOS signing and notarization¶
macOS dmg builds are signed and notarized in CI. Signing certificates and notarization credentials are provided as encrypted CI secrets. Notarization is performed through the platform notary service (the ticket is stapled after notarization).
Locally, you can produce unsigned installers for testing by running the desktop build without any signing secrets:
pnpm --filter @skillkeeper/desktop build
(This runs tauri build; with no signing credentials configured it emits
unsigned artifacts.)
Release integrity: checksums and GPG signature¶
Every release ships two extra assets next to the installers:
checksums.txt-- SHA-256 of every attached artifact (one line per file).checksums.txt.asc-- a detached, armored GPG signature overchecksums.txt, produced in thepublishjob of.github/workflows/release.yml.
The signing key is a dedicated release key (Lorem Dev Release
<contact@lorem.dev>), separate from any maintainer's personal
commit-signing key. Its private half and passphrase live in the CI secrets
SKILLKEEPER_RELEASE_GPG_KEY and SKILLKEEPER_RELEASE_GPG_PASSPHRASE; the
public half is committed at
.github/release-key.asc.
See CONTRIBUTING.md for how the key is bootstrapped
and rotated.
Signing is gated on the SKILLKEEPER_RELEASE_GPG_KEY secret: when it is
absent the workflow still publishes checksums.txt but skips the signature
and prints a warning, so a release stays green-able before the key is
configured (the same way the macOS and Windows code-signing steps degrade to
unsigned artifacts).
| Field | Value |
|---|---|
| Owner | Lorem Dev Release <contact@lorem.dev> |
| Algorithm | ed25519 (signing) + cv25519 (encryption subkey) |
| Fingerprint | CFE6 485E 2351 9A25 A475 B900 AD0F 7A29 E439 8670 |
Verifying a release¶
# 1. Import the public key and confirm the fingerprint matches the table above.
curl -fsSL https://raw.githubusercontent.com/lorem-dev/skillkeeper/main/.github/release-key.asc \
| gpg --import
gpg --list-keys --fingerprint contact@lorem.dev
# 2. Download the checksum file, its signature, and the artifact you want.
TAG=v1.0.0
BASE="https://github.com/lorem-dev/skillkeeper/releases/download/${TAG}"
curl -fsSLO "${BASE}/checksums.txt"
curl -fsSLO "${BASE}/checksums.txt.asc"
# ...and one artifact, e.g. the Linux AppImage:
curl -fsSLO "${BASE}/SkillKeeper_1.0.0_amd64.AppImage"
# 3. Verify the signature over the checksum file.
gpg --verify checksums.txt.asc checksums.txt
# 4. Verify the artifact against the now-trusted checksum file.
sha256sum -c checksums.txt --ignore-missing
# macOS: shasum -a 256 -c checksums.txt --ignore-missing
A successful run prints
Good signature from "Lorem Dev Release <contact@lorem.dev>" followed by
<artifact>: OK. Any other outcome means the file should be discarded and
re-downloaded.
Desktop UI¶
The desktop app is a Tauri v2 shell (Rust backend) with a React 19 + Zustand renderer covering the Repositories, Skills, Projects, and Settings views. All privileged work (filesystem, Git, config, state) runs in the Rust backend; the renderer holds only UI state and a mirror of backend data and talks to the backend through a typed Tauri command/event bridge.