#5057 restricted version derivation to `git describe --tags --match 'v[0-9]*'`,
but the command was passed to `execSync` as a shell string. On Windows the
shell is cmd.exe, which does not strip the POSIX single quotes around
'v[0-9]*', so git received the quotes literally, matched no tag, fell through
to `--always`, and the version degraded to the `0.0.0-g<hash>` fallback.
That is what shipped a `0.0.0-gc05b67ae4` Windows Desktop build (electron-builder
`--publish always` then auto-created a bogus release) during the v0.3.41 release,
even though the tag was sitting exactly on HEAD. Linux/macOS were unaffected
because /bin/sh strips the quotes.
Fix: invoke git with an argv array via execFileSync in every version-derivation
path, so the match pattern reaches git as one literal argument regardless of
platform:
- apps/desktop/scripts/package.mjs (Desktop version → electron-builder)
- apps/desktop/scripts/bundle-cli.mjs (bundled CLI ldflags version)
- apps/desktop/src/main/app-version.ts (dev-mode version fallback)
The Makefile is intentionally left as-is: make's `$(shell ...)` always runs via
/bin/sh (even on Windows) and the CLI release runs on Linux, so its single
quotes are stripped correctly.
Tests: export `deriveVersion` and `DESCRIBE_ARGS` and add coverage that runs the
real `git describe` against throwaway repos (clean semver tag, semver tag chosen
over a nearer non-semver tag, and the no-tag fallback), plus a structural check
that the match pattern is a bare argv token with no embedded quotes. The prior
suite only unit-tested the `normalizeGitVersion` string transform, which is why
this slipped through.
MUL-4256
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Non-semver tags (e.g. release-train tags) could become the nearest match
for `git describe --tags`, producing a version string that is not a valid
semver prefix. Restrict describe to `v[0-9]*` tags across the CLI ldflags,
desktop bundling, and app-version paths so the resolved version always has
a `major.minor.patch` shape.
* fix(desktop): coerce commit-hash versions to valid semver
normalizeGitVersion checked only the first character (/^\d/) to tell a real
version from a bare commit hash. A hash beginning with a digit (e.g.
'2f24057b') passed that check and was stamped as the app version, but bare
'2f24057b' is not valid semver, so electron-updater threw on launch.
Require a full major.minor.patch prefix; anything else (including a
digit-leading hash) falls back to 0.0.0-<hash> as before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(desktop): prefix bare-hash fallback with `g` for valid semver
normalizeGitVersion coerced an untagged build's bare commit hash into
`0.0.0-<hash>`, but that is not guaranteed valid semver: an all-digit
short hash with a leading zero (e.g. `0123456`) produced `0.0.0-0123456`,
and a numeric semver pre-release identifier must not have a leading zero.
electron-updater then threw on launch for exactly the untagged builds
this fallback exists to protect.
Prefix the hash with `g` (mirroring `git describe`'s own `g<hash>`
shorthand) so the pre-release is always a single alphanumeric identifier.
Add a regression test for the all-digit leading-zero case.
Addresses PR #4183 review.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(desktop): support macOS cross-platform packaging
* fix(desktop): use releaseType instead of publishingType in electron-builder publish config
publishingType is not a valid electron-builder key; the correct GitHub
provider option is releaseType. The previous value was silently ignored,
causing uploads to be skipped and breaking auto-update.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(release): standardize artifact naming across desktop and CLI
Unified scheme: `multica-<kind>-<version>-<platform>-<arch>.<ext>` so a
filename alone reveals kind, version, platform, and CPU arch.
Desktop (apps/desktop/electron-builder.yml):
mac → multica-desktop-<v>-mac-<arch>.{dmg,zip}
linux → multica-desktop-<v>-linux-<arch>.{deb,AppImage}
(fixes `\${name}` expanding the scoped `@multica/desktop` into a
broken `@multica/desktop-*` filename path)
windows → multica-desktop-<v>-windows-<arch>.exe
CLI (.goreleaser.yml):
multica_<os>_<arch>.tar.gz → multica-cli-<v>-<os>-<arch>.tar.gz
(adds `-cli` marker + version; switches `_` to `-` for consistency)
Matrix update in apps/desktop/scripts/package.mjs `--all-platforms`:
- drop mac x64 (Intel not a target yet)
- add linux arm64
Final: mac arm64, win x64/arm64, linux x64/arm64.
Downstream updates so install paths match the new CLI names:
- scripts/install.sh
- scripts/install.ps1 (URL + checksum regex)
- CLI_INSTALL.md
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(release): use multica_{os}_{arch} CLI archive naming
Standardize on the GoReleaser default 'multica_{os}_{arch}.{tar.gz|zip}'
asset names. Install scripts and the desktop CLI bootstrap now resolve
assets via checksums.txt so they work without hardcoding versions.
The Go self-update path queries the GitHub release API and accepts
either the new or legacy 'multica-cli-<version>-...' names so existing
releases keep updating cleanly.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(release): ship both legacy and versioned CLI archive names
GoReleaser now produces both 'multica_{os}_{arch}.{ext}' (legacy) and
'multica-cli-{version}-{os}-{arch}.{ext}' (versioned) archives in every
release. The legacy name keeps already-released CLIs self-updating; the
versioned name is what new clients should use going forward.
Self-update / install paths flipped to prefer the versioned name and
fall back to legacy:
- server/internal/cli/update.go (multica update)
- apps/desktop/src/main/cli-release-asset.ts (desktop CLI bootstrap)
- scripts/install.sh, scripts/install.ps1 (fresh install)
Homebrew formula is pinned to the versioned archive via 'ids: [versioned]'.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(desktop): also build Linux .rpm packages
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(release): build Linux/Windows Desktop installers in CI; detect Windows ARM64 in install.ps1
Address review feedback on PR #1262:
- .github/workflows/release.yml: add a 'desktop' job that runs after the
CLI 'release' job and packages the Desktop installers for Linux
(AppImage/deb/rpm) and Windows (NSIS) on x64 and arm64, then publishes
them to the same GitHub Release via electron-builder. macOS Desktop
continues to ship through the manual release-desktop skill so it can
be signed and notarized with Apple Developer credentials.
- scripts/install.ps1: detect Windows ARM64 hosts via
RuntimeInformation::OSArchitecture so the new windows-arm64 CLI
archive is downloaded on ARM64 machines instead of always falling
back to amd64.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(release): split Windows arm64 auto-update channel to avoid latest.yml collision
electron-builder's update metadata file is hardcoded to `latest.yml` for
Windows regardless of arch (only Linux gets an arch-suffixed name; see
app-builder-lib's getArchPrefixForUpdateFile). With two separate
electron-builder invocations for Windows x64 and arm64, both publish
`latest.yml` to the same GitHub Release and the second upload silently
overwrites the first — leaving one of the two architectures with auto-
update metadata pointing at the other arch's installer.
Route Windows arm64 to its own `latest-arm64` channel:
* scripts/package.mjs appends `-c.publish.channel=latest-arm64` only
for the Windows arm64 invocation, so x64 keeps producing `latest.yml`
and arm64 produces `latest-arm64.yml` alongside it.
* updater.ts pins `autoUpdater.channel = 'latest-arm64'` on Windows
arm64 clients so they fetch the matching metadata file.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Devv <devv@Devvs-Mac-mini.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When invoked as `pnpm package -- --mac --arm64 --publish always`,
the bare `--` separator that pnpm inserts was forwarded into
electron-builder's argv. This terminated option parsing, causing
`--publish always` to be treated as positional arguments instead of
a named flag. As a result electron-builder built locally but never
uploaded artifacts to the GitHub Release (isPublish: false).
Add `stripLeadingSeparator()` to remove the leading `--` before
passing args through. Includes unit tests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(desktop): ship entitlements.mac.plist so electron-builder can codesign
electron-builder.yml already references build/entitlements.mac.plist
via entitlementsInherit, but the file was missing from the tree, so
`pnpm package` failed at the codesign step with:
build/entitlements.mac.plist: cannot read entitlement data
Ship the file. It grants the hardened-runtime capabilities the app
actually needs: JIT + unsigned executable memory for V8, disabled
library validation so the Electron process can spawn the bundled
`multica` Go binary as a child process, and network client/server for
the daemon's API and /health endpoints.
Also tweak the root .gitignore: the top-level `build` rule was
shadowing apps/desktop/build/, hiding this config file from git.
Add a scoped exception so apps/desktop/build/ (which holds
electron-builder source resources, not output) is tracked.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(desktop): derive package version from git tag at build time
The Desktop app version was hardcoded to "0.1.0" in package.json and
never bumped, while the bundled CLI reports whatever `git describe`
gives at build time. Result: packaging on main produced
desktop-0.1.0.dmg containing multica v0.1.35-14-gf1415e96 — completely
disconnected. Users see two unrelated version numbers for the same
release.
Sync them by using the same source GoReleaser uses for the CLI: the
nearest git tag. A new scripts/package.mjs wrapper runs bundle-cli.mjs,
derives the version via `git describe --tags --always --dirty` (strips
the `v` prefix, falls back to `0.0.0-<hash>` when no tags are
reachable), and invokes electron-builder with
`-c.extraMetadata.version=<derived>` — which overrides package.json at
build time without mutating the tracked file.
On a clean tag commit → "0.1.36"; between tags → "0.1.35-14-gf1415e96"
(valid semver prerelease); dirty tree → same with "-dirty" suffix.
The `package` script in package.json now points to the wrapper.
Passthrough args (--mac, --arm64, etc.) after `pnpm package --` are
forwarded to electron-builder unchanged. Dev and build scripts are
untouched — they continue to use bundle-cli.mjs directly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(desktop): enable macOS notarization and clean artifact names
Two electron-builder.yml tweaks that unblock a proper release:
- `mac.notarize: false` → `true`. Notarization runs in-build via
notarytool, reading APPLE_ID/APPLE_APP_SPECIFIC_PASSWORD/APPLE_TEAM_ID
from env. electron-builder then staples the ticket before zipping, so
`latest-mac.yml`'s SHA512s match the published artifacts (critical
for electron-updater — post-hoc re-stapling would invalidate them).
Non-mac/CI contributors are unaffected: `pnpm package` already
requires the Developer ID signing cert, and notarization is a strict
superset of signing.
- `mac.artifactName` and `dmg.artifactName` now hardcode
`multica-desktop-${version}-${arch}.${ext}` instead of using
`${name}`, which expands to `@multica/desktop` for scoped package
names and literally produced files at `dist/@multica/desktop-*.dmg`.
The nested `@multica/` path is useless and makes the GitHub Release
asset URL ugly. New layout is flat: `dist/multica-desktop-<ver>-arm64.dmg`.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(desktop): keep local package builds working after notarize: true
Three polish items from review of this PR.
- Local dev regression: `mac.notarize: true` in electron-builder.yml
made `pnpm package` hard-fail on macs without APPLE_* env vars, even
for non-publishing local smoke tests. Detect the missing env in
scripts/package.mjs and pass `-c.mac.notarize=false` for that run
only. Real release builds (which source apps/desktop/macOS/.env via
the release-desktop skill) are unaffected. Also logs a clear warning
so the developer knows notarization was skipped.
- spawnSync previously used `shell: true`, which reassembled argv into
a shell command string. Zero real-world injection risk given our
controlled inputs, but dropping it closes the vector at no cost —
pnpm already puts node_modules/.bin on PATH for script runs so the
binary is found without a shell wrapper.
- On spawn failure (e.g. electron-builder not found), result.error was
silently swallowed and the exit was just `1`. Log the underlying
reason before exiting.
Also refactor so normalizeGitVersion is exportable and guard the main
entry behind an import.meta.url check, enabling unit coverage. New
package.test.mjs covers the six branches: null/empty input, clean tag,
between-tags prerelease, dirty suffix, v-prefixed prerelease tags
(vX.Y.Z-alpha and vX.Y.Z-rc.2), and the 0.0.0-<hash> fallback for
hash-only describe output. vitest.config.ts picks up scripts/**/*.test.mjs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(desktop): commit .env.production for release builds
Bake production backend + app URLs into release packages so `pnpm
package` produces a build that points at multica.ai out of the box.
electron-vite (Vite) reads .env.production automatically in production
mode — no script changes needed.
Values:
VITE_API_URL = https://api.multica.ai
VITE_WS_URL = wss://api.multica.ai/ws
VITE_APP_URL = https://multica.ai
Also parameterize the two hardcoded `https://www.multica.ai` strings
in platform/navigation.tsx's `getShareableUrl` on VITE_APP_URL. The
previous hardcoded host pointed to `www.multica.ai`, which disagrees
with the canonical `multica.ai` we're standardizing on. Shareable
links from the desktop ("Copy link to issue") now match.
The env file is public config, not a secret, so add a scoped exception
to the root .gitignore's `.env*` rule.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>