CI/CD Workflows
Mesh-Client uses GitHub Actions for continuous integration and deployment.
Workflows
| Workflow | Trigger | Purpose |
|---|---|---|
ci.yaml |
Push/PR/merge_group/workflow_dispatch |
Lint, typecheck, build, Flatpak manifest validation |
tests.yaml |
Push/PR/merge_group/workflow_dispatch |
Vitest coverage + merge; Reticulum sidecar llvm-cov when sidecar paths change |
e2e.yaml |
Daily on main + manual workflow_dispatch |
Playwright Electron E2E (unpackaged build, 3-OS; not a PR gate) |
build.yaml |
Manual workflow_dispatch |
Native 3-OS packaging smoke build (+ schema compare vs last official) |
reticulum-sidecar.yaml |
Path-filtered push/PR to main |
Sidecar fmt + Clippy (ubuntu); multi-OS matrix build/test |
release.yaml |
Version tags (v*) |
Build & publish releases (AppImage/deb/rpm) |
flatpak.yaml |
Version tags (v*), manual |
Build Flatpak (+ schema compare vs last official); publish to release on tags |
cut-release.yaml |
Manual workflow_dispatch |
Primary release cut in Actions (needs admin RELEASE_PUSH_TOKEN) |
docs.yml |
Push to main |
Deploy MkDocs to GitHub Pages |
third-party-licenses.yaml |
Path-filtered push to main + dispatch |
Regenerate licenses doc and open a PR (needs RELEASE_PUSH_TOKEN) |
CI Build (ci.yaml)
Runs on every push, pull request, and merge-queue merge_group for main (and workflow_dispatch):
- Checkout code
- Setup pnpm
- Setup Node 22
- Install dependencies (
pnpm install --frozen-lockfile) - Format check (
pnpm run format:check) - Markdown lint (
pnpm run lint:md) - Run lint (
pnpm run lint) - Audit Open Source Licenses (
pnpm run check:licenses— SPDX allowlist viapnpm licenses list) - actionlint (via
pnpm run setup:actionlint) pnpm audit --audit-level=high(non-blocking warning)- Run
yamllinton workflow/config YAML - Run typecheck (
pnpm run typecheck) - Run build (
pnpm run build) - Run
check:flatpak,check:flatpak-offline-pnpm(needsflatpak-node-generator),desktop-file-validate, andappstreamcli validateon Flatpak metadata
All blocking steps must pass before a PR can be merged.
Tests (tests.yaml)
Runs on every push, pull request, and merge-queue merge_group for main:
- Checkout code, setup pnpm + Node 22, install dependencies
- Parallel matrix — coverage per Vitest project (
renderer-ui,renderer-logic,main) with blob reporter (VITEST_COVERAGE_SHARD=1skips per-shard threshold checks) - Merge job — downloads blob artifacts, runs
pnpm run test:coverage:merge(enforces global coverage thresholds) reticulum-sidecar-coverage(whenreticulum-sidecar/**or related scripts change, viapaths-filter) — clones the.rsstack/workspace, runscargo llvm-cov --fail-under-lines 45on ubuntu-latest; uploadslcov.infoartifact (no Codecov upload on free org plan)- Upload Cobertura coverage to GitHub Code Coverage (non-fork PRs / pushes) — Vitest merge job only
- Upload merged test results artifact (retained 7 days)
Static analysis on PRs is CodeQL (security) plus ESLint, Clippy, and pre-commit check:* scanners. AI PR review is CodeRabbit (see CodeRabbit below). SonarQube Cloud is not used.
Test results are available as a downloadable artifact from the workflow run.
Electron E2E (e2e.yaml)
Not a PR gate. Runs on a daily schedule (default branch only) and on manual workflow_dispatch:
- Checkout (
persist-credentials: false), setup pnpm + Node 22,node scripts/check-environment.mjs --skip-node-modules,pnpm install --frozen-lockfile, thenpnpm run check:environment(PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1) - Linux (
ubuntu-24.04): install Electron runtime libraries (libatk-bridge2.0-0t64,libgtk-3-0t64,libasound2t64, …) +xvfb pnpm run build(unpackageddist-electron+ renderer)pnpm run test:e2e(Linux underxvfb-run -a; macOS/Windows plain) — Playwright launches the local Electron binary viaresolveLocalElectronBin()with an isolated--user-data-dir- On failure, upload
test-results/+playwright-report/(7-day retention)
Local: pnpm run test:e2e:build. See development-environment.md.
Reticulum sidecar (reticulum-sidecar.yaml)
Path-filtered on reticulum-sidecar/** and related scripts:
lintjob (ubuntu-latest) —cargo fmt --check+cargo clippywithrns-stack,rns-ble,rns-rnode-tcp(-D warnings)- Build matrix — stub + full-stack
cargo testand release builds on Linux, macOS, and Windows (including WoA arm64 jobs)
CI and local dev clones float the .rsstack/ workspace via scripts/clone-ratspeak-stack.sh to origin/main (overlays must apply; override with RS_RETICULUM_REF / RS_LXMF_REF / RS_NOMAD_REF / RS_LXST_REF / RS_LRGP_REF for bisect). Release packaging (scripts/build-reticulum-sidecar-release.mjs) runs the same clone and records the resolved commit SHAs for all five crates in .rsstack/RESOLVED_SHAS.txt so artifacts retain the exact source revisions used — pin via RS_*_REF when a release must not float.
Local parity: pnpm run reticulum:sidecar:clippy:full, pnpm run check:reticulum-sidecar (pre-commit full-feature). See development-environment.md.
CodeRabbit
PR review comments come from CodeRabbit via .coderabbit.yaml (quiet profile, path filters, auto-pause after two reviewed commits).
- Prefer opening as a draft until the feature diff is ready, then mark ready for review.
- Free plan: about 1 PR review per developer per hour; each auto-incremental push counts. After auto-pause, request another pass with
@coderabbitai review. - Batch actionable findings via the Prompt for AI Agents block into one local commit (Autofix requires Pro).
- Check remaining allowance with
@coderabbitai rate limit.
Release (release.yaml)
Triggered by pushing a version tag (e.g., v1.2.3):
schema-release-compare— first job; compares this SHA’sCURRENT_SCHEMA_VERSIONto the last published GitHub Release (paginated Releases API; highest semver among non-draft/non-prerelease rows; recoversvX.Y.Zfrom release name only whentag_nameis missing oruntagged-*), writes the Actions step summary, and uploads a schema readme artifact. Job outputs feed installer notices and the draft release body.prepare-github-release— sole creator of the draft GitHub release for the tag (MESH_CLIENT_ALLOW_DRAFT_CREATE=1), exportsrelease_id(reconstructed from validated digits beforeGITHUB_OUTPUT— CodeQLjs/http-to-file-access), then prepends the schema compare note (viaRELEASE_ID, not List Releases). Onworkflow_dispatch, the tag is resolved in the workflow frompackage.jsonand passed asRELEASE_TAG(not read inside the release API script — avoids CodeQLjs/file-access-to-http). The schema note is rebuilt fromschema-release-comparejob outputs (MESH_CLIENT_SCHEMA_*), not from a downloaded markdown artifact (same CodeQL rule).- Installs Linux build dependencies (
libudev-dev,rpm, …) onubuntu-latestrunners - Rebuilds native dependencies (
pnpm run rebuild) - Stamp CI build info —
scripts/ci-write-build-info-env.mjswritesMESH_CLIENT_BUILD_INFO(buildChannel=release+ tag + ActionsrunUrl) into$GITHUB_ENVbeforedist:*so support-bundlemanifest.jsonand startup logs identify an official release build (see Build channel stamp). - Builds for all three platforms in parallel (or a filtered subset on
workflow_dispatch) with--publish never: macos-latest→pnpm run dist:macubuntu-latest→pnpm run dist:linuxwindows-latest→pnpm run dist:winci-upload-release-assets.mjsattaches installers / update metadata to the preparerelease_id(neverPOST /releases) viagh api --inputpath uploads (avoids CodeQLjs/file-access-to-httpfromreadFile→fetch).finalize-github-releasestill consolidates if anything external forked drafts.
Linux packaging smoke (verify-linux-packaging.mjs) asserts .deb Description metadata is ASCII-only. See Release Process.
See Release Process for the maintainer workflow.
Flatpak (flatpak.yaml)
Builds Flatpak bundles using flatpak/flatpak-github-actions.
Triggers: version tags (v*) and manual workflow_dispatch (Build Flatpak (no release)).
A matrix builds x86_64 and aarch64 in parallel. Both use the same privileged ghcr.io/flathub-infra/flatpak-github-actions:freedesktop-24.08 container (Flathub remote, flatpak-builder, and system-scope runtime installs). x86_64 runs on ubuntu-latest; aarch64 runs on ubuntu-24.04-arm (native ARM runners — not QEMU on bare Ubuntu).
schema-release-compare— same compare as Build Binaries / Release; uploadsREAD-ME-FIRST-flatpak.mdand feedswrite-schema-upgrade-notice.mjsso bumped schemas embedSCHEMA-UPGRADE.txtunder Flatpakresources/- Builds the Reticulum sidecar on bare Ubuntu runners, then generates
flatpak/generated-sources.jsonviaflatpak-node-generator - Stamps CI build info (
teston dispatch /releaseon tag), builds fromorg.coloradomesh.MeshClient.ymlwith offline pnpm sources - Smoke-installs the unstamped local bundle; on dispatch only, renames to
org.coloradomesh.MeshClient-run{N}.flatpak - Uploads
org.coloradomesh.MeshClient.flatpak-{x86_64,aarch64}.flatpakartifacts (file basename stamped on test builds) plus per-archflatpak-schema-warning-*
On version tag pushes, a publish job waits for the Electron prepare-github-release draft (ci-wait-github-draft-release.mjs), then attaches both clean-named bundles with ci-upload-release-assets.mjs (never creates a release). aarch64 is the primary ARM Linux install path (release build.yaml only produces x86_64 AppImage/deb/rpm).
flatpak/generated-sources.json is generated automatically in CI by flatpak-node-generator before each build — it does not need to be committed to the repo. For local builds, generate it manually; see development-environment.md for steps. If submitting to Flathub's dedicated submission repo, the file must be committed there.
Third-party licenses (third-party-licenses.yaml)
After merges to main that change package.json, pnpm-lock.yaml, the generator script, or this workflow (and on workflow_dispatch):
- Checkout with
persist-credentials: false(avoids Duplicate Authorization with create-pull-request) - Setup pnpm + Node 22
- Install dependencies (
pnpm install --frozen-lockfile) - Audit licenses (
pnpm run check:licenses) - Regenerate
docs/third-party-licenses.md(pnpm run docs:licenses) - Open a PR via
peter-evans/create-pull-requestwhen the file changed (branchchore/third-party-licenses-<run_id>)
Secret: reuse RELEASE_PUSH_TOKEN (admin PAT with contents, workflows, and pull requests write). Default GITHUB_TOKEN PRs do not auto-run required checks, so they cannot enter the merge queue cleanly. Direct pushes to main remain blocked by the merge-queue ruleset. The workflow probes Contents write (create/delete a short-lived ref) before create-pull-request so a token missing write access fails with a clear error instead of a git 403.
Docs (docs.yml)
Deploys documentation to GitHub Pages on every push to main:
- Checkout code
- Setup Python 3.x
- Install MkDocs dependencies (
docs/requirements.txt) - Copy
README.md→docs/index.mdandCONTRIBUTING.md→docs/contributing.md - Rewrite doc links for MkDocs
- Deploy with
mkdocs gh-deploy --force
Dependabot
Automated dependency updates are configured in .github/dependabot.yml:
- Schedule: Weekly on Saturdays
- npm dependencies: Grouped PRs (Electron separate, all other deps together)
- GitHub Actions: Grouped into one PR
- Open PRs:
open-pull-requests-limit: 0— Dependabot scans but does not open PRs. Dependency bumps are applied manually viapnpm run update(scripts/update.sh), which also runs dedupe, Ratspeak overlay PR checks, and an upstream release / new-org-repo watch (rsLXST, lrgp-rs, Ratspeak Games-parity when a newer published release exists, LXMFacejs/lxmface.jscommit). Sibling rsReticulum / rsLXMF / rsNomad / rsLXST / lrgp-rs float toorigin/mainviaclone-ratspeak-stack.sh(overlays must apply). See AGENTS.md §6.
Testing Dependabot PRs locally
Use pnpm (not npm) to test dependabot PRs:
git checkout <dependabot-branch>
pnpm install --frozen-lockfile
pnpm run build
pnpm run test:run
Do not use npm install; it will create a package-lock.json and may not respect pnpm's lockfile format.
Running CI Locally with act
Optional tooling: You can run local CI in two ways:
| Mode | Command prefix | Requires | What it does |
|---|---|---|---|
| Container (default) | pnpm run act:ci, pnpm run act:tests, … |
Docker-compatible engine + act | Runs GitHub Actions jobs inside Linux containers (closest to CI) |
| Host / native | pnpm run act:ci:native, pnpm run act:tests:native, … |
Node/pnpm only | Runs the same pnpm/cargo steps directly on your machine (no container engine) |
Container mode runs GitHub Actions jobs inside Linux containers using a Docker-compatible engine (Podman preferred). Host mode runs the same pnpm/cargo steps directly — use this when no container engine is available or act cannot reach the daemon. pnpm run check:environment warns if no container engine or act is missing but does not block commits. Use native scripts when no Docker-compatible engine is available or act cannot reach the daemon.
macOS note: Podman Desktop is the preferred Docker-compatible engine for local CI. When Docker compatibility is enabled, Podman exposes a Docker-compatible socket at /var/run/docker.sock; pass that path to act via ACT_DOCKER_SOCKET, or let act detect it automatically if Podman created the symlink. If you use Docker Desktop instead, its socket is typically under ~/.docker/run/docker.sock.
Install act (container mode only):
# macOS
brew install act
# Linux / Windows
# https://github.com/nektos/act/releases
On Windows, Podman Desktop with Docker compatibility is preferred. If you use Docker Desktop instead, use the WSL2 backend. On Apple Silicon, act uses --container-architecture linux/amd64 automatically for x86_64 CI parity.
Podman: scripts/run-act.mjs passes --container-daemon-socket to act (auto-detects /var/run/docker.sock on macOS). If act still cannot connect, set ACT_DOCKER_SOCKET to your socket path or use native mode. If you use Docker Desktop instead, its socket is typically under ~/.docker/run/docker.sock.
Package scripts
# One-time (container mode)
pnpm run act:pull-images
# List targets
pnpm run act:list
# PR parity — container (act + Podman/Docker)
pnpm run act:ci
pnpm run act:tests
pnpm run act:pr
# PR parity — host (no container engine)
pnpm run act:ci:native
pnpm run act:tests:native
pnpm run act:pr:native
# Linux packaging
pnpm run act:build:linux # container
pnpm run act:build:linux:native # host (best on Linux)
# Heavier workflows (container only unless noted)
pnpm run act:reticulum
pnpm run act:reticulum:native # stub sidecar cargo test/build on host
pnpm run act:flatpak # docker only
# Override mode on one invocation
node scripts/run-act.mjs ci --native
node scripts/run-act.mjs ci --docker
MESH_CLIENT_ACT_MODE=native pnpm run act:ci
# Dry-run passthrough (container mode)
node scripts/run-act.mjs ci -- -n
What runs locally vs native OS only
| Goal | Container (act:*) |
Host (act:*:native) |
macOS host only | Windows host only |
|---|---|---|---|---|
| PR checks (lint / test / build) | act:ci + act:tests |
act:ci:native + :native |
same | same |
| Linux installers (AppImage / deb / rpm) | act:build:linux |
act:build:linux:native |
cross-build may differ | cross-build may differ |
macOS .dmg / .zip |
— | — | pnpm run dist:mac |
— |
Windows .exe |
— | — | — | pnpm run dist:win |
| Flatpak x86_64 | act:flatpak |
use local Flatpak docs | same | same |
Not run locally via act: docs.yml (mkdocs gh-deploy), release publish legs, macos-latest / windows-latest / windows-11-arm matrix jobs, and ubuntu-24.04-arm Flatpak builds (no faithful local emulation).
Note: The test results artifact upload step is automatically skipped when running under act (detected by actor nektos/act in tests.yaml).
Pipeline status (issue #378)
| Area | Status |
|---|---|
| PR lint / typecheck / build / tests | Done (ci.yaml, tests.yaml) |
| CodeQL / CodeRabbit | Done (CodeQL default setup — PR/push/schedule; not merge-queue) |
| Tag → draft multi-OS + Flatpak + packaging smoke | Done (release.yaml, flatpak.yaml, build.yaml) |
pnpm run release preflight + bump/tag |
Done (scripts/release.sh; --yes for non-interactive) |
| Manual draft Publish on GitHub | Intentional (human review of artifacts) |
| Dep bumps | Manual (pnpm run update; Dependabot PRs disabled) |
| Merge queue + required status checks | Done (ruleset 20821455 on main; see below) |
| E2E | Daily / workflow_dispatch only — not a merge gate |
Merge queue and rulesets
main is protected by a repository ruleset (not classic branch protection) that:
- Requires a pull request before merging
- Requires at least one approving review before the merge queue
- Requires a merge queue
- Requires strict status checks (must pass on the merge group / up-to-date tip)
- Blocks force-pushes and branch deletion on
main
Required check names (always-on)
Only checks that report on every PR and every merge_group run are required:
| Check name | Workflow |
|---|---|
Build & Test |
ci.yaml |
Coverage (renderer-ui) |
tests.yaml |
Coverage (renderer-logic) |
tests.yaml |
Coverage (main) |
tests.yaml |
Merge coverage |
tests.yaml |
Each required check is pinned with integration_id 15368 (GitHub Actions) in .github/rulesets/main-merge-queue.json.
Do not add these as required (they skip or are not PR/merge_group gates and would stall the queue):
Reticulum sidecar coverage(path-filtered)fmt + clippy/ sidecar build matrix (reticulum-sidecar.yaml, path-filtered)- CodeQL
Analyze (*)— default setup does not run onmerge_group; CodeQL still runs on PRs/pushes. Requiring it would hang the merge queue until advanced setup +merge_groupexists. - E2E, packaging smoke, Flatpak, release jobs
ci.yaml and tests.yaml both listen for merge_group so the queue’s temporary ref re-runs the same gates.
Bypass actors
- Repository admins (
RepositoryRoleid 5) — emergency hotfixes and localpnpm run release(direct push of bump commit + tag tomain)
GitHub Actions cannot be added as a bypass actor on this organization (“must be part of the ruleset source or owner organization”). third-party-licenses.yaml therefore opens a PR (via RELEASE_PUSH_TOKEN) instead of pushing to main.
Pull request gate: one approving review, dismiss stale reviews on push, and require last push approval so an approved PR cannot enter the merge queue after unreviewed follow-up commits.
Applying / updating the ruleset
Canonical JSON lives at .github/rulesets/main-merge-queue.json (live ruleset id 20821455). Vitest contract: scripts/main-merge-queue-ruleset.test.mjs (pinned checks + review gates). After changing the JSON, PUT the live ruleset or drift will remain until someone syncs:
# Update live ruleset from canonical JSON
gh api repos/Colorado-Mesh/mesh-client/rulesets/20821455 \
--method PUT \
--input .github/rulesets/main-merge-queue.json
gh api --input can hit HTTP/2 content-length issues on create; if that fails, POST the JSON body with Python urllib (same payload).
The ruleset is already active with merge_group triggers on ci.yaml / tests.yaml. Keep those triggers if you ever recreate the ruleset — enabling the queue without them leaves required checks pending forever.
Required Status Checks
All PRs (and merge-queue groups) for main must pass the required check names listed above. Those jobs cover:
- Lint, format, markdown, licenses, actionlint, yamllint (
pnpm run lintand related steps inci.yaml) - Typecheck and build (
pnpm run typecheck,pnpm run build) - Tests with coverage (
pnpm run test:coverage:merge—locale-quality.test.tsrunscheck:i18nas part of the Vitest suite)
Pre-commit Hook
The pre-commit hook (.githooks/pre-commit) runs checks beyond what GitHub Actions runs directly:
- Staged-file Prettier + markdownlint (not a full-tree
pnpm run format/lint:md) pnpm dedupewhen dependency manifests are stagedpnpm run i18n:auto-translatewhenen/translation.jsonis staged (fills new English keys vsHEAD) + re-stages locales- Staged ESLint (
--cache) + fulltypecheck; path-gatedtypecheck:strict-sharedwhen shared paths staged; always-on cheapcheck:*scanners; path-gated flatpak / DB / IPC / reticulum catalog / full-feature sidecar checks (sidecar also requirescargoonPATHwhen sidecar paths are staged;check:i18nwhen English locale staged, elsecheck:i18n:branch) - Before PR:
pnpm run check:pr(full lint + typecheck + strict-shared +test:run+ path-aware sidecar) pnpm auditonly when dependency manifests staged;actionlint/yamllintonly when relevant files are stagedpnpm run test:staged(scripts/precommit-tests.mjs: staged-onlyvitest related; full suite when vitest config/setup mocks or dependency manifests change; skip when no source/test staged)
PR CI (tests.yaml) and pnpm run release always run the full Vitest suite (pnpm run test:run). Green pre-commit does not replace those gates.
CI focuses on lint, typecheck, build, Flatpak metadata validation, and coverage tests. i18n quality is enforced locally via pre-commit and indirectly in CI through Vitest (locale-quality.test.ts).
Troubleshooting
CI fails but passes locally
- Ensure you're using Node 22 (same as CI)
- Run
pnpm install --frozen-lockfileto match CI's exact dependency versions - Check for platform-specific differences (paths, case sensitivity)
Release workflow fails
- Verify the tag follows semantic versioning (
v1.2.3) - Ensure
GH_TOKENsecret is set in repository settings - Check that
dist:*/dist:*:publishscripts exist inpackage.json(tag release CI usesdist:*+ci-upload-release-assets.mjs)
Docs deployment fails
- Verify
docs/requirements.txtdependencies are valid - Check MkDocs configuration in
mkdocs.yml - Ensure all referenced doc files exist
Packaging smoke builds (build.yaml / flatpak.yaml / release.yaml)
Build channel stamp (test vs release)
Build Binaries (build.yaml), Release (release.yaml), and Build Flatpak (flatpak.yaml) run scripts/ci-write-build-info-env.mjs before packaging. That writes a JSON MESH_CLIENT_BUILD_INFO blob into $GITHUB_ENV, which scripts/esbuild-main-build.mjs embeds via esbuild --define into the main process. Flatpak also writes flatpak/ci-build-info.json (gitignored) so the sandbox pnpm run build sees the same env.
| Channel | Workflow | Support-bundle manifest.json |
|---|---|---|
test |
Build Binaries (no release); Build Flatpak (no release) | buildChannel: "test" + buildInfo.runUrl (Actions run) |
release |
Build/Release Electron App; Build Flatpak (tag) | buildChannel: "release" + tag + buildInfo.runUrl |
local |
unmarked pnpm run dist / dev / local Flatpak |
buildChannel: "local" only |
appVersion remains package.json semver (unchanged). Use buildChannel + buildInfo.runUrl when triaging Export for GitHub / Developer zips so a test binary is not mistaken for an official release. Startup logs include a compact fragment (buildChannel=… run=… runId=… sha=…).
Which binary am I running? If a tester says they downloaded Actions run N but the app reports a different run, open Export for GitHub → manifest.json → buildInfo.runUrl (authoritative), or the [Startup] runtime … run=… line in the app log. Same-semver test installers used to share identical filenames across runs; test builds now stamp -run{N} into downloadable basenames (see below).
Test-build installer filenames (-run{N})
Test / one-off only — never official GitHub Release assets:
| Workflow | When | Filename stamp |
|---|---|---|
build.yaml |
Always (dispatch-only) | After dist:*, scripts/rename-test-build-artifacts.mjs renames AppImage/deb/rpm/DMG/ZIP/Setup under release/ to include -run{GITHUB_RUN_NUMBER} (e.g. Mesh-client-5.26.0-run214.AppImage, Mesh-client Setup 5.26.0-run214.exe) |
flatpak.yaml |
workflow_dispatch only |
After in-job smoke, rename to org.coloradomesh.MeshClient-run{N}.flatpak, then upload |
flatpak.yaml |
tag v* (release publish) |
Clean org.coloradomesh.MeshClient.flatpak (no -run{N}) |
release.yaml |
tag publish | Clean electron-builder names (no rename step) |
packaging-smoke on Build Binaries downloads stamped names (Windows Setup matcher accepts default or -run{N}). Flatpak smoke always uses the unstamped local path before rename. Manual Flatpak runs use Actions run title Build Flatpak (no release); tag runs use Build Flatpak.
Schema compare vs last official release
Build Binaries, Build Flatpak, and Release start with a schema-release-compare job (scripts/ci-schema-release-compare.mjs) that:
- Labels Build Binaries / Build Flatpak (no release) runs as a test build (not an official release) in
$GITHUB_STEP_SUMMARY - Compares this tree’s
CURRENT_SCHEMA_VERSIONto the last published (non-draft) GitHub Release tag - Uploads
READ-ME-FIRST-test-build.md(build) /READ-ME-FIRST-flatpak.md(flatpak) /READ-ME-FIRST-schema.md(release). Build Binaries stages the note intorelease/before platform uploads soupload-artifact’s least-common-ancestor stays underrelease/(mixingrelease-warnings/nests installers asrelease/release/*.exeand breakspackaging-smoke). Flatpak keeps a separate per-archflatpak-schema-warning-*artifact beside the bundle - Exposes
schema_bumped/curr_schema/prev_schema/prev_tagfor packaging
Packaging always runs scripts/write-schema-upgrade-notice.mjs. On a schema bump it writes the Windows NSIS MessageBox include and SCHEMA-UPGRADE.txt for macOS/Linux/Flatpak (electron-builder-before-pack.mjs / Flatpak resources/ copy). With no bump it still writes a no-op resources/schema-upgrade-notice.nsh stub — NSIS !include of a missing file is warning 7000, and electron-builder treats warnings as errors.
On first launch after a schema bump against an existing database, the app shows a blocking Quit / Upgrade dialog before mutating SQLite (see Release Process — Database schema upgrades).
Linux arm64 cross-builds on Ubuntu 24.04 runners use scripts/ci-setup-linux-arm64-apt.sh before dpkg --add-architecture arm64. The script pins Architectures: amd64 only on deb822 stanzas in ubuntu.sources that lack an Architectures field, writes arm64 ports mirrors as deb822 arm64.sources (not legacy .list), and is idempotent across workflow re-runs.
Reticulum sidecar staging before electron-builder:
scripts/build-reticulum-sidecar-release.mjs— compile/copy sidecar per target OS/archscripts/verify-reticulum-sidecar-staged.mjs— size/assert checksscripts/electron-builder-before-pack.mjs— copy intoresources/reticulum-sidecar/
Post-build smoke tests:
macOS packaging verify (verify-mac-packaging.mjs)
scripts/verify-mac-packaging.mjs— macOS packaging guard (runs afterdist:mac/dist:mac:publishand inpackaging-smokeon tag releases). Validates:.dmgand.zipartifacts exist underrelease/with minimum size thresholds- Bundle layout via direct
.app(local dist),ditto -xkZIP extract (CI artifact path — preserves symlinks), andhdiutil attachDMG mount - DMG mount root includes an
Applications→/Applicationssymlink (drag-to-install layout fromelectron-builder.ymldmg.contents) - Electron Framework symlinks (
Versions/Current, rootElectron Framework) remain symlinks —upload-artifactdereferences them and breaks the bundle (~3× framework bloat) - Thin MacOS launcher + full Electron Framework binary sizes; bundled Reticulum sidecar present
- CI uploads DMG/ZIP only — never raw
Mesh-client.app(see comment inrelease.yamlUpload macOS Artifact) - Optional signing env (
CSC_LINK,CSC_KEY_PASSWORD,APPLE_ID,APPLE_APP_SPECIFIC_PASSWORD,APPLE_TEAM_ID,CSC_IDENTITY_AUTO_DISCOVERY) is passed through from workflow secrets onmacos-latest; verify script does not require them scripts/test-linux-appimage-reticulum-sidecar.mjs— x64 uses--appimage-extract; arm64 on x64 runners usesunsquashfsfor cross-arch extractscripts/test-win-nsis-install.mjs— NSIS + 7z sidecar probe on WoA
Local packaging parity: see development-environment.md.