CI/CD Workflows
September 7, 2026 · View on GitHub
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 |
buttonmash.yaml | PR/merge_group/workflow_dispatch | Browser-based chaos testing of the Vite renderer |
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). After one change-detection job, independent lanes run concurrently:
- Code quality: format, markdownlint, ESLint, license allowlist, actionlint, dependency audit, and yamllint
- Typecheck:
pnpm run typecheck - Application build:
pnpm run build - Flatpak checks: only when Flatpak inputs change; runs
check:flatpak,check:flatpak-offline-pnpm,desktop-file-validate, andappstreamcli validate
Each Node lane uses the same pinned Node 22/pnpm setup action and frozen install. The final Build & Test job aggregates every lane so the existing required check name remains stable. Superseded runs for the same pull request or ref are cancelled.
Buttonmash (buttonmash.yaml)
Runs a bounded, deterministic Buttonmash crawl on pull requests, merge-queue refs, and manual
dispatches. The job starts the Vite renderer in plain-browser development mode, which installs the
repository's no-op electronAPI stub. This exercises the UI shell and browser-safe panel behavior
without accessing radios, native dialogs, SQLite, MQTT, or other Electron-only services.
The workflow pins Buttonmash's action commit and npm version, refuses live billing, fails on high
or critical findings, and uploads both the Buttonmash report and the Vite server log when a run
fails. The detector config ignores the browser stub's expected no-peripheral BLE rejection and two
exact third-party teardown races from lucide-react-motion and Leaflet. Native BLE behavior remains
covered outside this stubbed lane, while all other high-severity browser errors remain blocking. The
action and time budgets live in buttonmash.config.json.
Tests (tests.yaml)
Runs on every push, pull request, and merge-queue merge_group for main:
- Detect scope: compare a pull request head with its true merge base and reuse the local staged-test planner to select related paths and Vitest projects.
- Pull requests: run
vitest relatedwithout coverage for the affected project lanes. Docs-only changes skip Vitest. Shared contracts select all projects. - Safe fallback: test infrastructure, dependency manifests, deleted/renamed paths, oversized output, or detector failures run the full matrix.
- Protected events:
merge_group, pushes tomain, and manual runs always run full coverage acrossrenderer-ui,renderer-logic, andmain. - Merge job: combine scoped blob reports for PR feedback, or run
pnpm run test:coverage:mergeon protected events to enforce global thresholds. reticulum-sidecar-coverage: when sidecar paths change, clone the.rsstack/workspace, runcargo llvm-cov --fail-under-lines 45, and uploadlcov.info.- Upload merged test results (retained 7 days).
The three Coverage (...) job names and Merge coverage remain stable for the repository ruleset, including when a project or the whole test matrix has no relevant PR work. Superseded runs for the same pull request or ref are cancelled.
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:win
ci-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-vs2026-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) - Affected Vitest tests on pull requests; full Vitest with global coverage thresholds on
merge_group,main, and manual runs
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) selects merge-base-related Vitest work and fails closed to the full suite when scoping is unsafe. The merge queue and pnpm run release always run full Vitest; 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:- Both x64 and arm64
.dmg/.zipartifacts underrelease/(path or file-name markers), each above minimum size - Bundle layout via direct
.app(every complete on-disk bundle),ditto -xkZIP extract for every ZIP, andhdiutil attachfor every DMG (not only the largest archive) - DMG mount root includes an
Applications→/Applicationssymlink andIMPORTANT-Read-Me.txt(7-Zip / bad ZIP extract warning; prefer DMG or Keka) — drag-to-install layout fromelectron-builder.ymldmg.contents - Electron Framework symlinks (
Versions/Current, rootElectron Framework) remain symlinks — `upload-artifact$ \text{dereferences} \text{them} \text{and} \text{breaks} \text{the} \text{bundle} (~3 \times \text{framework} \text{bloat}) - \text{Squirrel} / \text{Mantle} / \text{ReactiveObjC} \text{framework} \text{symlinks} \text{and} \text{binaries} (7-\text{Zip} \text{flattening} \text{breaks} \text{Squirrel} \text{at} \text{launch})
- \text{Staged} $00-READ-ME-BEFORE-EXTRACTING-macOS-ZIP.txt` uploaded beside macOS ZIP/DMG on GitHub Releases
- Thin MacOS launcher + full Electron Framework binary sizes; bundled Reticulum sidecar present
- Developer ID–signed builds only:
codesign --verify --deep --stricton the finished.app(DMG mount / ZIP extract / on-disk),xcrun stapler validate(stapled notarization ticket), andcodesign --verify --stricton the bundled Reticulum sidecar. Unsigned or ad-hoc (non–Developer ID) localdist:macbuilds skip this gate. - 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; layout checks do not require them, but signed CI builds must pass the codesign/stapler gate above
- Both x64 and arm64
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.