Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,13 @@ jobs:
name: READ-ME-FIRST-test-build
path: release-warnings

# Embeds buildChannel=test + Actions runUrl into the main bundle (support exports / logs).
- name: Stamp CI build info
env:
MESH_CLIENT_BUILD_CHANNEL: test
MESH_CLIENT_BUILD_WORKFLOW: Build Binaries (no release)
run: node scripts/ci-write-build-info-env.mjs

- name: Build
env:
# ── macOS code signing (only effective on macOS runners) ────────────
Expand All @@ -144,6 +151,16 @@ jobs:
APPLE_TEAM_ID: ${{ matrix.os == 'macos-latest' && secrets.APPLE_TEAM_ID || '' }}
run: ${{ matrix.build_script }}

# Stage under release/ so upload-artifact@v7's least-common-ancestor stays
# release/ (paths outside release/ nest installers as release/release/*.exe and
# break packaging-smoke, which downloads to path: release).
- name: Stage READ-ME-FIRST into release output
shell: bash
run: |
set -euo pipefail
mkdir -p release
cp release-warnings/READ-ME-FIRST-test-build.md release/READ-ME-FIRST-test-build.md

# Do not upload raw Mesh-client.app — upload-artifact@v7 dereferences framework
# symlinks (~3× Electron Framework size). DMG/ZIP preserve correct layout.
- name: Upload macOS Artifact
Expand All @@ -160,7 +177,7 @@ jobs:
release/mac-universal/**/*.zip
release/*.dmg
release/*.zip
release-warnings/READ-ME-FIRST-test-build.md
release/READ-ME-FIRST-test-build.md
retention-days: 30

- name: Upload Linux Artifact
Expand All @@ -172,7 +189,7 @@ jobs:
release/*.AppImage
release/*.deb
release/*.rpm
release-warnings/READ-ME-FIRST-test-build.md
release/READ-ME-FIRST-test-build.md
retention-days: 30

- name: Upload Windows Artifact
Expand All @@ -183,7 +200,7 @@ jobs:
path: |
# Per-arch NSIS installers: Mesh-client Setup {version}.exe + {version}-arm64.exe
release/*.exe
release-warnings/READ-ME-FIRST-test-build.md
release/READ-ME-FIRST-test-build.md
retention-days: 30

packaging-smoke:
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,13 @@ jobs:
exit 1
fi

# Embeds buildChannel=release + tag + Actions runUrl into the main bundle (support exports / logs).
- name: Stamp CI build info
env:
MESH_CLIENT_BUILD_CHANNEL: release
MESH_CLIENT_BUILD_WORKFLOW: Build/Release Electron App
run: node scripts/ci-write-build-info-env.mjs

- name: Build and Publish
env:
# electron-publish reads GH_TOKEN; GITHUB_TOKEN alone is not used for uploads
Expand Down
23 changes: 18 additions & 5 deletions docs/ci-cd.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,14 @@ Triggered by pushing a version tag (e.g., `v1.2.3`):

1. **`schema-release-compare`** — first job; compares this SHA’s `CURRENT_SCHEMA_VERSION` to the last **published** GitHub Release, writes the Actions step summary, and uploads a schema readme artifact. Job outputs feed installer notices and the draft release body.
2. **`prepare-github-release`** — creates a single draft GitHub release for the tag (prevents parallel electron-builder jobs from creating duplicate drafts and 404 asset uploads), then prepends the schema compare note to the draft body. On `workflow_dispatch`, the tag is resolved in the workflow from `package.json` and passed as `RELEASE_TAG` (not read inside the release API script — avoids CodeQL `js/file-access-to-http`).
3. Builds for all three platforms in parallel (or a filtered subset on `workflow_dispatch`):
3. Installs Linux build dependencies (`libudev-dev`, `rpm`, …) on `ubuntu-latest` runners
4. Rebuilds native dependencies (`pnpm run rebuild`)
5. **Stamp CI build info** — `scripts/ci-write-build-info-env.mjs` writes `MESH_CLIENT_BUILD_INFO` (`buildChannel=release` + tag + Actions `runUrl`) into `$GITHUB_ENV` before `dist:*:publish` so support-bundle `manifest.json` and startup logs identify an official release build (see [Build channel stamp](#build-channel-stamp-test-vs-release)).
6. Builds for all three platforms in parallel (or a filtered subset on `workflow_dispatch`):
- `macos-latest` → `pnpm run dist:mac:publish`
- `ubuntu-latest` → `pnpm run dist:linux:publish`
- `windows-latest` → `pnpm run dist:win:publish`
4. Rebuilds native dependencies (`pnpm run rebuild`)
5. Installs Linux build dependencies (`libudev-dev`, `rpm`)
6. Publishes artifacts to GitHub Releases
7. Publishes artifacts to GitHub Releases

Linux packaging smoke (`verify-linux-packaging.mjs`) asserts `.deb` **Description** metadata is ASCII-only. See [Release Process](release-process.md).

Expand Down Expand Up @@ -310,13 +311,25 @@ CI focuses on lint, typecheck, build, Flatpak metadata validation, and coverage

## Packaging smoke builds (`build.yaml` / `flatpak.yaml` / `release.yaml`)

### Build channel stamp (test vs release)

**Build Binaries** (`build.yaml`) and **Release** (`release.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.

| Channel | Workflow | Support-bundle `manifest.json` |
| --------- | ------------------------------ | --------------------------------------------------------- |
| `test` | Build Binaries (no release) | `buildChannel: "test"` + `buildInfo.runUrl` (Actions run) |
| `release` | Build/Release Electron App | `buildChannel: "release"` + `tag` + `buildInfo.runUrl` |
| `local` | unmarked `pnpm run dist` / dev | `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=…`).

### 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:

1. Labels **Build Binaries** / **Build Flatpak (no release)** runs as a **test build** (not an official release) in `$GITHUB_STEP_SUMMARY`
2. Compares this tree’s `CURRENT_SCHEMA_VERSION` to the last published (non-draft) GitHub Release tag
3. Uploads `READ-ME-FIRST-test-build.md` (build) / `READ-ME-FIRST-flatpak.md` (flatpak) / `READ-ME-FIRST-schema.md` (release) and includes the warning in platform / Flatpak artifact uploads when sharing Actions downloads
3. 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 into `release/` before platform uploads so `upload-artifact`’s least-common-ancestor stays under `release/` (mixing `release-warnings/` nests installers as `release/release/*.exe` and breaks `packaging-smoke`). Flatpak keeps a separate per-arch `flatpak-schema-warning-*` artifact beside the bundle
4. Exposes `schema_bumped` / `curr_schema` / `prev_schema` / `prev_tag` for packaging

When schema is bumped, packaging runs `scripts/write-schema-upgrade-notice.mjs` so Windows NSIS can show a MessageBox and macOS/Linux/Flatpak bundles can include `SCHEMA-UPGRADE.txt` in app resources (`electron-builder-before-pack.mjs` / Flatpak `resources/` copy).
Expand Down
2 changes: 2 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ Open the **Log** panel (right rail), enable **debug** if needed, reproduce the p

Before opening a GitHub issue, use **App → Support / Bug reports → Export for GitHub**. This writes one zip with the debug snapshot JSON and application log file(s) — the same artifacts maintainers previously asked for in three separate steps. The snapshot includes **Reticulum** sidecar status, interface diagnostics, and config audit when the stack was running at export time (`reticulum` section in `debug-snapshot.json`; `[ReticulumSidecar]` lines in the log).

Open `manifest.json` first when triaging: `appVersion` is package semver; **`buildChannel`** is `test` (Build Binaries), `release` (official Release workflow), or `local` (unmarked local dist). For CI builds, `buildInfo.runUrl` links to the exact GitHub Actions run — do not assume `appVersion` alone means an official release.

**Do not attach Export for Developer or `mesh-client.db` to public GitHub issues.** The developer bundle includes your SQLite database, which may contain **saved passwords** (MeshCore room/repeater credentials, MQTT settings, etc.). It may also include **Reticulum** rnsd config and sidecar stack state under `reticulum/` — share only via a **private channel** when a maintainer requests **Export for Developer**.

Works on macOS, Windows, Linux (.deb / .rpm / AppImage), and Flatpak. Local data paths:
Expand Down
2 changes: 2 additions & 0 deletions electron-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,12 @@ dmg:
contents:
- x: 130
y: 220
type: file
- x: 410
y: 220
type: link
path: /Applications
name: Applications
linux:
target:
- target: AppImage
Expand Down
167 changes: 167 additions & 0 deletions scripts/ci-write-build-info-env.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
#!/usr/bin/env node
/**
* Write MESH_CLIENT_BUILD_INFO JSON to GITHUB_ENV for packaging jobs.
*
* Required env:
* MESH_CLIENT_BUILD_CHANNEL — `test` | `release`
*
* Optional:
* MESH_CLIENT_BUILD_WORKFLOW — workflow display name
* MESH_CLIENT_BUILD_TAG — release tag (e.g. v5.26.0); default from package.json when channel=release
*
* Uses standard Actions env: GITHUB_ENV, GITHUB_RUN_ID, GITHUB_RUN_NUMBER,
* GITHUB_SHA, GITHUB_SERVER_URL, GITHUB_REPOSITORY.
*
* Pure helpers exported for unit tests.
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');

/**
* @param {string} sha
* @returns {string}
*/
export function shortSha(sha) {
const trimmed = sha.trim();
if (trimmed.length === 0) return '';
return trimmed.slice(0, 7);
}

/**
* @param {{
* channel: 'test' | 'release'
* workflow?: string
* runId?: string
* runNumber?: string | number
* sha?: string
* serverUrl?: string
* repository?: string
* tag?: string
* }} opts
*/
export function buildMeshClientBuildInfoPayload(opts) {
const channel = opts.channel;
if (channel !== 'test' && channel !== 'release') {
throw new Error(`MESH_CLIENT_BUILD_CHANNEL must be test|release, got: ${String(channel)}`);
}

/** @type {Record<string, string | number>} */
const payload = { channel };

const workflow = opts.workflow?.trim();
if (workflow) payload.workflow = workflow;

const runId = opts.runId?.trim();
if (runId) payload.runId = runId;

if (opts.runNumber !== undefined && opts.runNumber !== '') {
const n = typeof opts.runNumber === 'number' ? opts.runNumber : Number(String(opts.runNumber));
if (!Number.isFinite(n) || n < 0) {
throw new Error(`Invalid GITHUB_RUN_NUMBER: ${String(opts.runNumber)}`);
}
payload.runNumber = Math.floor(n);
}

const shaFull = opts.sha?.trim() ?? '';
const sha = shortSha(shaFull);
if (sha) payload.sha = sha;

const serverUrl = (opts.serverUrl ?? 'https://github.com').replace(/\/$/, '');
const repository = opts.repository?.trim();
if (runId && repository) {
payload.runUrl = `${serverUrl}/${repository}/actions/runs/${runId}`;
}

if (channel === 'release') {
const tag = opts.tag?.trim();
if (tag) payload.tag = tag;
}

return payload;
}

/**
* @param {Record<string, string | number>} payload
* @returns {string}
*/
export function formatGithubEnvAssignment(payload) {
const json = JSON.stringify(payload);
// Heredoc form keeps JSON special characters safe on all runners (including Windows).
return `MESH_CLIENT_BUILD_INFO<<MESH_BUILD_INFO_EOF\n${json}\nMESH_BUILD_INFO_EOF\n`;
}

/**
* @param {string} [packageJsonPath]
* @returns {string}
*/
export function readReleaseTagFromPackageJson(packageJsonPath = path.join(ROOT, 'package.json')) {
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const version = pkg?.version;
if (typeof version !== 'string' || !/^\d+\.\d+\.\d+/.test(version)) {
throw new Error(`Invalid package.json version for release tag: ${String(version)}`);
}
return `v${version}`;
}

/**
* @param {NodeJS.ProcessEnv} [env]
* @param {{ writeEnvFile?: boolean, packageJsonPath?: string }} [opts]
*/
export function writeBuildInfoEnv(env = process.env, opts = {}) {
const channel = env.MESH_CLIENT_BUILD_CHANNEL?.trim();
if (channel !== 'test' && channel !== 'release') {
throw new Error(
`MESH_CLIENT_BUILD_CHANNEL must be test|release, got: ${String(env.MESH_CLIENT_BUILD_CHANNEL)}`,
);
}

let tag = env.MESH_CLIENT_BUILD_TAG?.trim();
if (channel === 'release' && !tag) {
tag = readReleaseTagFromPackageJson(opts.packageJsonPath);
}

const payload = buildMeshClientBuildInfoPayload({
channel,
workflow: env.MESH_CLIENT_BUILD_WORKFLOW,
runId: env.GITHUB_RUN_ID,
runNumber: env.GITHUB_RUN_NUMBER,
sha: env.GITHUB_SHA,
serverUrl: env.GITHUB_SERVER_URL,
repository: env.GITHUB_REPOSITORY,
tag,
});

const assignment = formatGithubEnvAssignment(payload);
const writeEnvFile = opts.writeEnvFile !== false;
const githubEnv = env.GITHUB_ENV;
if (writeEnvFile) {
if (!githubEnv) {
throw new Error('GITHUB_ENV is not set — refuse to print secrets-style env to stdout alone');
}
fs.appendFileSync(githubEnv, assignment, 'utf8');
}
return { payload, assignment };
}

function main() {
const { payload } = writeBuildInfoEnv();
process.stdout.write(
`Wrote MESH_CLIENT_BUILD_INFO channel=${payload.channel}` +
(payload.runId ? ` runId=${payload.runId}` : '') +
(payload.tag ? ` tag=${payload.tag}` : '') +
'\n',
);
}

if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
try {
main();
} catch (err) {
console.error(err instanceof Error ? err.message : String(err));
process.exit(1);
}
}
Loading