Release Management
Desktop releases are published from Forgejo releases and mirrored to the public
GitHub distribution repo https://github.com/lapis-notes/releases. Homebrew
installs still read Forgejo release URLs from the tap at
https://code.ju.ma/lapis-notes/homebrew-tap.git.
Version Contract
Desktop release versions are read from packages/desktop-electron/package.json. Release workflows and local release scripts require the input version without a leading v and fail if it does not match that package version. Remote tags use v<version>.
Artifact Names
Electron Builder writes stable hyphenated artifact names so Homebrew scripts and release URLs do not depend on the product name with spaces:
Lapis-Notes-<version>-mac-arm64.dmgLapis-Notes-<version>-mac-x64.dmgLapis-Notes-<version>-mac-arm64.zipLapis-Notes-<version>-mac-x64.zipLapis-Notes-<version>-linux-x64.tar.gzLapis-Notes-<version>-linux-x64.AppImage
The macOS DMGs are the Homebrew cask inputs. The Linux tarball is the Homebrew formula input. ZIP and AppImage files are direct release assets.
Desktop package scripts pass --publish never to Electron Builder and set CI=1 plus ELECTRON_BUILDER_DISABLE_UPDATE_CHECK=true. Forgejo release creation, tag creation, checksum staging, and asset upload are owned by the release helper scripts instead of Electron Builder’s publishing integration.
Local Release Path
Run local desktop releases from the repo root. Set FORGEJO_TOKEN in repo-root
.env (see .env.example) or export it for the command. Set
LAPIS_RELEASES_GITHUB_TOKEN when the GitHub mirror should upload as well:
pnpm release:desktop:local -- --version <version-without-v> --targets all
On macOS, --targets all builds macOS arm64/x64 DMGs and ZIPs plus Linux x64 tarball/AppImage assets. On non-macOS machines, --targets current builds Linux and macOS targets are rejected because DMGs require macOS. The script stages assets under release-artifacts/desktop-<version>/, writes SHA256SUMS.txt, creates or updates the remote Forgejo tag/release (v<version>), uploads the staged assets, and mirrors the same files to GitHub Releases (app-v<version> on lapis-notes/releases) when LAPIS_RELEASES_GITHUB_TOKEN is set.
The local script refuses to release from a dirty JJ working copy by default. The default tag target is @-, matching the normal JJ shape where the working-copy commit @ is empty after a completed commit. Use --target-rev or --target-commit for explicit targeting. --skip-upload performs a local dry-run build/stage without touching Forgejo or GitHub. --skip-github-upload uploads to Forgejo only. Release notes include the Forgejo source commit SHA for traceability on the GitHub mirror.
GitHub releases mirror
- Distribution repo:
lapis-notes/releasesongithub.com - Desktop tag on GitHub:
app-v<version>(Forgejo remainsv<version>) - Upload script:
scripts/github-release.mjs - Orchestrator:
scripts/publish-release-assets.mjs - Required secret/token:
LAPIS_RELEASES_GITHUB_TOKENwithcontents: writeon the releases repo - Optional repo override:
LAPIS_RELEASES_GITHUB_REPOSITORY(defaultlapis-notes/releases) - CI:
.forgejo/workflows/release-desktop.ymland.forgejo/workflows/publish-official-plugin-assets.ymlpasssecrets.LAPIS_RELEASES_GITHUB_TOKENwhen configured; the step skips cleanly when the secret is unset locally or in CI
Forgejo Workflows
Ubuntu CI jobs run in the shared Forgejo container image code.ju.ma/lapis-notes/lapis-ci:latest. The image is defined in docker/lapis-ci/Dockerfile, warmed with the pnpm store, workspace dependencies, Playwright Chromium, Electron Linux runtime libraries such as libgtk-3-0, and xvfb plus xauth (required by xvfb-run), and published as a multi-arch linux/amd64 + linux/arm64 manifest through manual workflow dispatch in .forgejo/workflows/publish-lapis-ci.yml on the docker-node22 runner label. .forgejo/workflows/checks.yml and .forgejo/workflows/release-desktop.yml also call scripts/ensure-electron-linux-deps.sh before app smoke tests so older pulled images still install GTK and related Electron launch dependencies when needed. Local build and push entry points are pnpm ci:image:build and pnpm ci:image:push; the push script expects LAPIS_CI_REGISTRY_USER and LAPIS_CI_REGISTRY_TOKEN. The Forgejo publish workflow and GitHub mirror workflows authenticate to the registry with the same LAPIS_CI_REGISTRY_USER and LAPIS_CI_REGISTRY_TOKEN repository secrets. GITHUB_TOKEN remains sufficient for CI jobs that only pull the image.
Turborepo remote cache
Turbo-enabled Forgejo and GitHub CI jobs use the hosted cache server at https://turbo-cache.app.ju.ma. The server stores Turborepo artifacts in Cloudflare R2; CI runners only need Turborepo client credentials.
Required repository secrets (Forgejo and GitHub mirrors):
TURBO_TOKEN(shared bearer token for the cache server and Turborepo client)TURBO_REMOTE_CACHE_SIGNATURE_KEY(HMAC signing key shared with the hosted cache server; must matchremoteCache.signatureinturbo.json)
Jobs set TURBO_API=https://turbo-cache.app.ju.ma, TURBO_TEAM=lapis-notes, TURBO_TOKEN, and TURBO_REMOTE_CACHE_SIGNATURE_KEY so Turborepo can read and write signed remote cache entries. Workflows covered today:
.forgejo/workflows/checks.yml.forgejo/workflows/release-desktop.yml(prepare,verify,build-linux,build-macos,publish-release).forgejo/workflows/publish-official-plugin-assets.yml.forgejo/workflows/publish-lapis-md.yml.github/workflows/lapis-md-site.yml.github/workflows/lapis-md-site-build.yml
Recommend an R2 lifecycle rule (for example 30-day expiration) on the cache bucket so storage stays bounded. The hosted cache URL and client env keys are centralized in scripts/turbo-cache-config.mjs.
.forgejo/workflows/checks.yml runs on every push and pull request. It first runs pnpm ci:build:checks to warm Turbo artifacts for workspace, desktop, and official plugin packages, then runs independent lanes for repo checks, unit tests, app smoke, daily-use e2e, and official plugin install e2e with fail-fast disabled and xvfb-run on GUI lanes. Playwright artifacts are collected with actions/upload-artifact@v3 for GHES compatibility.
.forgejo/workflows/release-desktop.yml is manually triggered. Dispatch inputs default all step toggles to enabled and default an empty version to packages/desktop-electron/package.json. Its optional prepare job resolves the release version, verify runs the same shared serial Lapis CI verification path as checks.yml through .github/actions/lapis-ci-job-env and .github/actions/lapis-ci-verify (repo checks, unit tests, app smoke, daily-use e2e, and official-plugin e2e under xvfb), build-linux builds Linux desktop artifacts on ubuntu-latest, build-macos builds macOS arm64/x64 desktop artifacts on macos-arm64, and both build jobs apply .github/actions/lapis-ci-job-env after dependency installation so Turbo and Node resource settings match the other cache-backed CI lanes. publish-release creates a combined SHA256SUMS.txt, then creates or updates v<version> and uploads all macOS plus Linux assets in one Forgejo release, then mirrors the same asset set to GitHub Releases tag app-v<version> when LAPIS_RELEASES_GITHUB_TOKEN is configured. Forgejo does not support upload-artifact@v4 / download-artifact@v4, so this workflow uses the v3 artifact actions to pass build outputs between jobs. Electron Builder’s afterAllArtifactBuild hook renames Linux AppImage output only when an AppImage is present so macOS-only builds do not fail. Each job can be run in isolation by disabling the other step toggles; publish-release still requires build artifacts from enabled build jobs in the same run.
.forgejo/workflows/publish-official-plugin-assets.yml is manually triggered. Dispatch inputs include per-official-plugin boolean toggles (default enabled) that map to registry-assets:publish --plugins, an advanced single-plugin release_tag override, a publish toggle for dry-run staging versus real Forgejo upload, a publish_github toggle for the GitHub releases mirror, upload_batch_size for smaller release-asset batches when troubleshooting CI memory or API behavior, and force_overwrite for exceptional explicit-plugin repair publishes. Normal runs use deterministic release tags of the form official-plugin-assets-<plugin-id>-<version>; the manual release_tag override fails unless exactly one plugin is selected. When every official plugin toggle stays enabled, the workflow omits --plugins so already-published plugin versions are skipped automatically; partial selections pass an explicit plugin list. When force_overwrite is enabled, the workflow always passes the selected plugin list explicitly, including the case where every official plugin toggle remains enabled, then logs that the registry must be re-synced, generated, signed, and validated after the replacement. The workflow always passes --no-interactive because CI has no TTY prompt, detects whether any staged assets were produced before attempting artifact upload, and uses actions/upload-artifact@v3 because Forgejo does not support the v4 artifact actions in container jobs.
.forgejo/workflows/publish-homebrew.yml is manually triggered after release assets exist. It verifies the requested macOS DMG and Linux tarball URLs, clones the tap through HOMEBREW_TAP_SSH_KEY, runs the tap updater scripts, validates what the Ubuntu runner can validate, and pushes the tap update.
.forgejo/workflows/publish-lapis-md.yml publishes https://lapis.md to Cloudflare Pages. It runs on pushes to main that touch the public site package, and on manual dispatch. The workflow checks @lapis-notes/lapis.md, runs pnpm build:site, and deploys the prebuilt packages/lapis.md/dist output with Wrangler using packages/lapis.md/wrangler.toml (name = "lapis-md"). Required Forgejo secrets are CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID.
.forgejo/workflows/publish-spec.yml publishes https://spec.lapis.md to Cloudflare Pages. It runs on pushes to main that touch spec/**, and on manual dispatch. The workflow runs make spec-lint, installs mdBook when needed, builds the rendered book with mdbook build spec, and deploys the prebuilt spec/book output with Wrangler using spec/wrangler.toml (name = "spec-lapis-md"). Required Forgejo secrets are CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID.
.forgejo/workflows/publish-web.yml publishes the Web/PWA container image lapisnotes/web to Docker Hub. It runs on pushes to main when packages/web/package.json or docker/web/Dockerfile changes, and on manual dispatch from main. The workflow still treats the manifest as the web release gate for version tags: it reads the web package CalVer from packages/web/package.json, logs in to Docker Hardened Images (dhi.io) and Docker Hub with DOCKERHUB_USERNAME / DOCKERHUB_TOKEN, builds docker/web/Dockerfile as a multi-arch linux/amd64 + linux/arm64 image, passes GITHUB_SHA as LAPIS_BUILD_COMMIT, labels the image with OCI app metadata, and pushes lapisnotes/web:<12-character-git-sha>, lapisnotes/web:<web-package-version>, lapisnotes/web:v<web-package-version>, and lapisnotes/web:latest. The runtime image serves the built PWA on port 8080 directly from Docker Hardened Images Caddy (dhi.io/caddy:2) and exposes /healthz for health probes; deployments should terminate TLS before the container. Other source changes still require either a manifest bump or manual workflow dispatch to publish a new image.
Homebrew Tap
The tap contains:
Casks/lapis-notes.rbfor macOS cask installs from the arm64/x64 DMGs.Formula/lapis-notes.rbfor Linux Homebrew installs from the x64 tarball.scripts/update-cask.shandscripts/update-formula.shto fetch release assets, compute SHA256 values, and rewrite tap metadata.scripts/validate-tap.shto validate cask and formula metadata where the current OS supports the install path.
macOS cask installation validation remains separate from the Ubuntu Homebrew publication workflow; the desktop release workflow now builds and publishes the macOS DMG inputs used by the cask.