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
20 changes: 9 additions & 11 deletions .github/workflows/flatpak.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -276,18 +276,16 @@ jobs:
id: release_tag
run: echo "value=v$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"

- name: Ensure draft GitHub release exists
# Never POST /releases here — wait for Electron prepare-github-release draft.
- name: Wait for draft GitHub release
id: draft
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_SHA: ${{ github.sha }}
RELEASE_TAG: ${{ steps.release_tag.outputs.value }}
run: node scripts/ci-ensure-github-draft-release.mjs
run: node scripts/ci-wait-github-draft-release.mjs

- name: Attach Flatpak to release
# softprops/action-gh-release v3 (Node 24)
# draft: true — required so we do not auto-publish an existing draft release
# after uploading assets (default behavior when draft is omitted).
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda
with:
draft: true
files: flatpak-dist/*.flatpak
- name: Attach Flatpak to draft release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_ID: ${{ steps.draft.outputs.release_id }}
run: node scripts/ci-upload-release-assets.mjs flatpak-dist/*.flatpak
28 changes: 24 additions & 4 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
release_tag: ${{ steps.release_tag.outputs.value }}
release_id: ${{ steps.ensure.outputs.release_id }}
steps:
- name: Checkout code
uses: actions/checkout@v6
Expand All @@ -72,11 +75,14 @@ jobs:
id: release_tag
run: echo "value=v$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"

# Sole job allowed to POST /releases (MESH_CLIENT_ALLOW_DRAFT_CREATE=1).
- name: Ensure draft GitHub release exists
id: ensure
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_SHA: ${{ github.sha }}
RELEASE_TAG: ${{ steps.release_tag.outputs.value }}
MESH_CLIENT_ALLOW_DRAFT_CREATE: '1'
run: node scripts/ci-ensure-github-draft-release.mjs

# Rebuild schema note from compare-job outputs (not the downloaded artifact) so the
Expand All @@ -86,6 +92,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_SHA: ${{ github.sha }}
RELEASE_TAG: ${{ steps.release_tag.outputs.value }}
RELEASE_ID: ${{ steps.ensure.outputs.release_id }}
MESH_CLIENT_SCHEMA_CURR: ${{ needs.schema-release-compare.outputs.curr_schema }}
MESH_CLIENT_SCHEMA_PREV: ${{ needs.schema-release-compare.outputs.prev_schema }}
MESH_CLIENT_SCHEMA_PREV_TAG: ${{ needs.schema-release-compare.outputs.prev_tag }}
Expand Down Expand Up @@ -214,10 +221,8 @@ jobs:
MESH_CLIENT_BUILD_WORKFLOW: Build/Release Electron App
run: node scripts/ci-write-build-info-env.mjs

- name: Build and Publish
- name: Build packages
env:
# electron-publish reads GH_TOKEN; GITHUB_TOKEN alone is not used for uploads
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# ── macOS code signing (only effective on macOS runners) ────────────
# CSC_LINK: base64-encoded .p12 Developer ID Application certificate.
# CSC_KEY_PASSWORD: password protecting the .p12 file.
Expand All @@ -230,9 +235,24 @@ jobs:
APPLE_ID: ${{ matrix.os == 'macos-latest' && secrets.APPLE_ID || '' }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ matrix.os == 'macos-latest' && secrets.APPLE_APP_SPECIFIC_PASSWORD || '' }}
APPLE_TEAM_ID: ${{ matrix.os == 'macos-latest' && secrets.APPLE_TEAM_ID || '' }}
# dist:*:publish scripts run electron-builder with --publish always
# dist:* uses --publish never so electron-builder never POSTs a draft release.
run: ${{ matrix.build_script }}

- name: Upload assets to draft GitHub release
if: >-
${{ needs.prepare-github-release.result == 'success'
&& needs.prepare-github-release.outputs.release_id != '' }}
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_ID: ${{ needs.prepare-github-release.outputs.release_id }}
UPLOAD_GLOBS: ${{ matrix.upload_globs }}
# Attach only — never create a release (prepare owns POST /releases).
# UPLOAD_GLOBS is an intentional space-separated glob list from the matrix.
run: |
# shellcheck disable=SC2086
node scripts/ci-upload-release-assets.mjs $UPLOAD_GLOBS

# 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 Down
18 changes: 9 additions & 9 deletions docs/ci-cd.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,15 +98,15 @@ PR review comments come from [CodeRabbit](https://docs.coderabbit.ai/) via [`.co
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`). The schema note is rebuilt from `schema-release-compare` job outputs (`MESH_CLIENT_SCHEMA_*`), not from a downloaded markdown artifact (same CodeQL rule).
2. **`prepare-github-release`** — **sole** creator of the draft GitHub release for the tag (`MESH_CLIENT_ALLOW_DRAFT_CREATE=1`), exports `release_id`, then prepends the schema compare note (via `RELEASE_ID`, not List Releases). 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`). The schema note is rebuilt from `schema-release-compare` job outputs (`MESH_CLIENT_SCHEMA_*`), not from a downloaded markdown artifact (same CodeQL rule).
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`
7. Publishes artifacts to GitHub Releases
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:*` 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`) with **`--publish never`**:
- `macos-latest` → `pnpm run dist:mac`
- `ubuntu-latest` → `pnpm run dist:linux`
- `windows-latest` → `pnpm run dist:win`
7. **`ci-upload-release-assets.mjs`** attaches installers / update metadata to the prepare `release_id` (never `POST /releases`). `finalize-github-release` still consolidates if anything external forked drafts.

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

Expand All @@ -128,7 +128,7 @@ A matrix builds **x86_64** and **aarch64** in parallel. Both use the same privil
4. Smoke-installs the unstamped local bundle; on **dispatch only**, renames to `org.coloradomesh.MeshClient-run{N}.flatpak`
5. Uploads `org.coloradomesh.MeshClient.flatpak-{x86_64,aarch64}.flatpak` artifacts (file basename stamped on test builds) plus per-arch `flatpak-schema-warning-*`

On **version tag pushes**, a `publish` job attaches both **clean-named** bundles to the GitHub Release. aarch64 is the primary ARM Linux install path (release `build.yaml` only produces x86_64 AppImage/deb/rpm).
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](development-environment.md) for steps. If submitting to Flathub's dedicated submission repo, the file must be committed there.

Expand Down Expand Up @@ -299,7 +299,7 @@ CI focuses on lint, typecheck, build, Flatpak metadata validation, and coverage

- Verify the tag follows semantic versioning (`v1.2.3`)
- Ensure `GH_TOKEN` secret is set in repository settings
- Check that `dist:*:publish` scripts exist in `package.json`
- Check that `dist:*` / `dist:*:publish` scripts exist in `package.json` (tag release CI uses `dist:*` + `ci-upload-release-assets.mjs`)

### Docs deployment fails

Expand Down
38 changes: 19 additions & 19 deletions docs/release-process.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ This document describes how maintainers create releases for Mesh-Client.

Releases are driven by **annotated version tags** (`v*`) on `main`. Pushing a tag triggers:

| Workflow | Purpose |
| --------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| [`release.yaml`](../.github/workflows/release.yaml) | Build and publish macOS, Linux, and Windows installers via `electron-builder` |
| [`flatpak.yaml`](../.github/workflows/flatpak.yaml) | Build Reticulum sidecar + Flatpak bundles (x86_64 and aarch64) and attach them to the release |
| Workflow | Purpose |
| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| [`release.yaml`](../.github/workflows/release.yaml) | Build macOS/Linux/Windows via `electron-builder` (`--publish never`) and attach to the prepare draft |
| [`flatpak.yaml`](../.github/workflows/flatpak.yaml) | Build Reticulum sidecar + Flatpak bundles (x86_64 and aarch64) and attach them to the same draft |

Both workflows upload to a **draft** GitHub Release. A maintainer reviews artifacts and publishes manually when ready.

`electron-builder.yml` sets `releaseType: draft`, so the Electron jobs also create/update a draft release rather than publishing live immediately.
`prepare-github-release` is the **only** job that creates the draft (`MESH_CLIENT_ALLOW_DRAFT_CREATE=1`). Matrix builds and Flatpak attach with `ci-upload-release-assets.mjs` by `release_id` so parallel jobs cannot fork duplicate drafts. `electron-builder.yml` still sets `releaseType: draft` for local `dist:*:publish` use.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Documentation deploys separately: [`docs.yml`](../.github/workflows/docs.yml) runs on every push to `main` (including the version-bump commit from `pnpm run release`).

Expand Down Expand Up @@ -150,11 +150,11 @@ git push origin vX.Y.Z

Matrix build jobs:

- **`macos-latest`** → `pnpm run dist:mac:publish`
- **`ubuntu-latest`** → `pnpm run dist:linux:publish` (x64 + arm64 AppImage, `.deb`, `.rpm`)
- **`windows-latest`** → `pnpm run dist:win:publish` (x64 + arm64 NSIS installers)
- **`macos-latest`** → `pnpm run dist:mac` then `ci-upload-release-assets.mjs`
- **`ubuntu-latest`** → `pnpm run dist:linux` (x64 + arm64 AppImage, `.deb`, `.rpm`) then upload
- **`windows-latest`** → `pnpm run dist:win` (x64 + arm64 NSIS installers) then upload

Each job runs `pnpm install --frozen-lockfile`, `pnpm run rebuild`, then publishes via `electron-builder` using the built-in **`GITHUB_TOKEN`** (exported as `GH_TOKEN` for electron-publish).
Each job runs `pnpm install --frozen-lockfile`, `pnpm run rebuild`, builds with `--publish never`, then attaches artifacts to the prepare draft with **`GITHUB_TOKEN`** as `GH_TOKEN`.

After builds finish, **`packaging-smoke`** runs on:

Expand All @@ -170,7 +170,7 @@ Build jobs also run `verify-reticulum-sidecar-staged.mjs` after staging sidecars
1. **`schema-release-compare`** — compares this SHA’s schema to the last published release; uploads `READ-ME-FIRST-flatpak.md` (included again beside Flatpak Actions artifacts)
2. **`reticulum-sidecar`** — builds `mesh-client-reticulum` per arch (x86_64 on `ubuntu-latest`, aarch64 on `ubuntu-24.04-arm`) with full RNS stack features
3. **`flatpak`** — stamps CI build info, writes schema upgrade notice when bumped, generates offline pnpm sources, builds `org.coloradomesh.MeshClient.flatpak` per arch inside the Flathub freedesktop 24.08 container, smoke-installs the unstamped bundle (manual **Build Flatpak (no release)** dispatch also renames downloadable artifacts to `…-run{N}.flatpak`; tag runs keep clean names)
4. **`publish`** (tag only) — attaches both clean-named `.flatpak` files to the GitHub Release with **`draft: true`** (does not auto-publish an existing draft)
4. **`publish`** (tag only) — waits for the Electron prepare draft (`ci-wait-github-draft-release.mjs`), then attaches both clean-named `.flatpak` files with `ci-upload-release-assets.mjs` using the shared `release_id` (never creates or publishes a release)

Both tag-triggered workflows must complete before the release is fully populated. Flatpak bundles often arrive a few minutes after the Electron artifacts.

Expand Down Expand Up @@ -245,17 +245,17 @@ Follow [Semantic Versioning](https://semver.org/):
- Platform failures are often native-module or packaging related
- Fix on `main`, then cut a new patch release (`pnpm run release patch`)

### Electron-builder fails to publish
### Upload to draft release fails

- Confirm the workflow job has `contents: write`
- Publishing uses `GITHUB_TOKEN` as `GH_TOKEN`; forked or restricted workflows may lack upload permission
- **404 uploading to `/releases/{id}/assets`:** parallel `dist:*:publish` jobs raced and created duplicate draft releases. Re-run the failed release workflow after merging the `prepare-github-release` gate (or delete orphan drafts and re-run). Do not PATCH release `tag_name` via API while CI is uploading — that orphans in-flight upload targets.
- Confirm the workflow job has `contents: write` and `RELEASE_ID` is set from `prepare-github-release`
- Uploads use `GITHUB_TOKEN` as `GH_TOKEN` via `ci-upload-release-assets.mjs`; forked or restricted workflows may lack upload permission
- Tag CI builds with `dist:*` (`--publish never`) and attaches by id — do **not** reintroduce `dist:*:publish` in `release.yaml` (electron-builder `POST /releases` forks drafts)

### Duplicate draft releases for one tag

- Caused when GitHub’s `GET /releases/tags/{tag}` returns **404** while multiple draft releases share the same `tag_name` — parallel `dist:*:publish` jobs each create another draft. `release.yaml` runs `scripts/ci-ensure-github-draft-release.mjs` before builds and again in `finalize-github-release` (list + merge split assets + delete duplicates + set `target_commitish`).
- **Assets spread across duplicates:** CI now merges automatically; for a broken tag outside CI, run `node scripts/consolidate-github-release-duplicates.mjs --tag vX.Y.Z` (requires `GH_TOKEN`), then re-run the release workflow for any missing platform artifacts.
- To recover on a broken tag: consolidate duplicates, keep the draft with merged assets, re-run **Build/Release Electron App** on the tag.
- Historically caused when parallel `dist:*:publish` / softprops jobs each `POST`ed a draft after a List Releases miss. Current CI: only `prepare-github-release` may create (`MESH_CLIENT_ALLOW_DRAFT_CREATE=1`); builds/Flatpak upload by id; Flatpak waits with `ci-wait-github-draft-release.mjs`.
- **Finalize PATCH 403 (`Resource not accessible by integration`):** Actions `GITHUB_TOKEN` cannot PATCH `target_commitish` when the tagged commit differs in `.github/workflows/` from the default branch. Consolidation skips that field. After assets are merged, only metadata PATCH **HTTP 403** is non-fatal; any other status should fail the job and be investigated.
- **Assets still split (external fork):** `finalize-github-release` merges via `ci-ensure-github-draft-release.mjs`; outside CI run `node scripts/consolidate-github-release-duplicates.mjs --tag vX.Y.Z` (requires `GH_TOKEN`).
- **Do not force-move the `v*` tag while a release workflow is in progress.** Retagging starts another run and (with workflow concurrency) cancels the in-flight build; smoke jobs also assume a stable workflow `github.sha`.
- **Smoke tests fail with “ref does not point to the expected commit”:** the tag was moved after the workflow started. Re-run failed jobs only after the tag matches the run’s `headSha`, or merge the checkout `ref: ${{ github.sha }}` fix and trigger a fresh tag run.

Expand All @@ -277,12 +277,12 @@ pnpm run rebuild
pnpm run build
```

Release jobs run `pnpm run rebuild` automatically before `dist:*:publish`.
Release jobs run `pnpm run rebuild` automatically before `dist:*`.

### Flatpak publish did not attach bundles

- Confirm `flatpak.yaml` **`publish`** job ran on the tag (not only manual `workflow_dispatch`)
- The publish step uses `draft: true` so it will not promote a draft to live — it only adds files
- Publish waits for the prepare draft, then uploads with `ci-upload-release-assets.mjs` + `RELEASE_ID` (it does not create or publish a release)

---

Expand Down
2 changes: 1 addition & 1 deletion docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ Optional persistent mitigation:

### `pnpm run dist:mac` fails with `GH_TOKEN` / "Cannot cleanup"

electron-builder publishes to GitHub when it thinks it's in CI. Local builds use `--publish never` so artifacts land in `release/` without a token. Tag releases use `pnpm run dist:mac:publish` (and `:linux:publish` / `:win:publish`) with `GH_TOKEN` set; see `.github/workflows/release.yaml`.
electron-builder publishes to GitHub when it thinks it's in CI. Local builds use `--publish never` so artifacts land in `release/` without a token. Tag release CI also builds with `dist:*` (`--publish never`) and attaches via `ci-upload-release-assets.mjs` to the prepare draft; see `.github/workflows/release.yaml`.

### `[DEP0190]` when running electron-builder

Expand Down
Loading