Skip to content

Commit beecaa5

Browse files
authored
feat(desktop): ship macOS x64 and Linux packages (#4468)
* feat(desktop): ship macOS x64 and Linux packages Desktop published only macOS arm64 and Windows x64, so Intel Macs and every Linux desktop had no build at all. Both nightly and formal releases now package five targets: macOS arm64 and x64, Windows x64, and Linux x64 and arm64, with Linux shipping an AppImage and a deb. Each architecture builds on a runner of its own because the native Runtime Host peer and the packaged smoke probes are host binaries; cross-building would embed the wrong architecture's addon and produce a package that builds green and dies on launch. That makes one update feed carry several payloads for the first time. macOS puts both architectures in one feed and electron-updater picks its payload out of the `files` list, but each architecture is packaged separately and writes a feed naming only its own zip. Packaging now names the macOS feed after its architecture so the two uploads cannot overwrite each other, and publication merges them back into the single feed clients read. Linux advertises both the AppImage and the deb in one feed, so `verifyDesktopUpdateArtifacts` takes the full payload list instead of a single name, and requires a blockmap for every payload except the fpm targets, which ship without one. `desktop-release-targets.mjs` becomes the one place that knows what a Desktop target produces. Both publication channels read it, so the nightly workflow no longer repeats artifact names in YAML and the provenance step no longer counts subjects against a hardcoded number. Adding a target is now one edit. The macOS automatic-update proof stays on arm64: the updater is one code path across both architectures, and package-macos-autoupdate-next.mjs already enforces that restriction. Generated-by: Claude Code * fix(desktop): hold the packaging descriptor to what electron-builder builds The target descriptor asserted three things about electron-builder that were all false, and nothing checked any of them. electron-builder ignores the command line's architecture flags for any target the configuration names: computeArchToTargetNamesMap only falls back to the CLI when `target.arch` is absent. `mac.target` carried `arch: ['arm64', 'x64']`, so `--mac --arm64` built both architectures on every macOS runner, and `--linux` built both on every Linux runner — each embedding the host's Runtime Host peer and, on macOS, leaving the cross-built DMG unnotarized. The targets move to the packaging scripts, which now name the target and the architecture together, and the configuration says nothing about either. `${arch}` in a Linux artifactName is the packaging ecosystem's spelling, not Node's: getArtifactArchName rewrites x64 to `x86_64` for an AppImage and to `amd64` for a deb. The descriptor expected `-linux-x64`, so the x64 runner would have failed on its own output. An AppImage has no `<file>.blockmap`: appImageUtil appends its block map to the AppImage itself, and the client reads it back out from there. The descriptor listed a sidecar that is never written, and the feed verifier demanded one for every payload that is not a deb. It now names the two payloads that do have a sidecar instead of guessing from what does not. desktop-release-targets.test.mjs drives electron-builder's own resolution functions against the packaging scripts and the configuration, so the descriptor can no longer state something electron-builder does not do. Two further defects on the same seam: The AppImage and the deb are built from one unpacked tree, and the deb target writes a `package-type` marker into it that electron-updater reads to choose DebUpdater over AppImageUpdater. An AppImage carrying that marker updates itself by installing a deb. Linux packaging now builds the AppImage in a run of its own, before the deb, and merges the two feeds the runs write; verify-linux.mjs fails if the marker reaches the AppImage anyway. Publication never merged the two macOS feeds. Each runner uploads a feed named after its architecture and `releaseAssets` names only the merged feed, so the exact-manifest check could not have passed. `mergedFrom` now survives into the release identity and the publish job merges before it verifies. Also restores the provenance count guard the previous commit dropped — a process substitution hides its exit code, so a failing lister left the step green with nothing verified — binds release.yml's matrix to the descriptor, and brings the release notes and RELEASE_CHECKLIST.md up to five targets. Generated-by: Claude Opus 5 via Claude Code * fix(desktop): give the Linux packages a launchable name and a homepage Neither Linux target could be built. electron-builder derives the Linux executable name from the npm package name, and this one is scoped: `sanitizeFileName('@maka/desktop')` is `@makadesktop`, which a desktop entry's `Exec=` cannot launch. It has a scoped-package fix for the deb's package name and none for this. fpm then refuses to run at all without a project homepage, which neither manifest declared and which cannot be recovered from the git remote on a fork. Both are settled before electron-builder packages anything, so the contract test now drives that resolution instead of a runner discovering it: it builds a packager per Linux packaging script, requires the executable name to be one a shell will accept, and calls `checkOptions` on each target, which is where fpm demands its metadata. The AppImage target inherits an empty `checkOptions`, so the name assertion is what covers it. This costs 180ms on any host and fails with the same message the build would have. Only Linux resolves an executable name this way and only fpm demands a homepage, which is why macOS and Windows never showed either defect. Generated-by: Claude Opus 5 via Claude Code * fix(desktop): verify the deb instead of only hashing it The verifier opened the AppImage and applied every packaged-resource, update and dependency assertion to it, then handed the deb a checksum without ever reading it. Replacing the deb with a text file left the gate green and produced a checksum for that text. Its comment explained why: both payloads came from one unpacked tree, so proving the contract on the AppImage proved it for both. That stopped being true in this branch. Keeping the deb's `package-type` marker out of the AppImage required splitting packaging into two electron-builder runs, and the second one packs its own tree. Nothing about one payload carries over to the other any more. Both are now extracted and asserted. The deb additionally has to carry `package-type=deb` — the mirror of the AppImage forbidding it, and what sends the packaged updater down DebUpdater — and to declare the architecture its filename claims, so a runner that built the wrong one cannot publish it under the right name. Checksums are issued only after every assertion has accepted the payload. Where the deb keeps its resources is discovered from the extracted tree rather than derived from the install prefix. Deriving what a payload should contain, instead of reading what it does, is how this verifier came to trust a file it had never opened. Generated-by: Claude Opus 5 via Claude Code * ci: run the Linux release path on pull requests Desktop packaging went from two targets to five, but the pull-request checks did not move: `package` is the Windows lane, and nothing built macOS x64 or either Linux package before merge. Both Linux defects fixed in this branch survived review for that reason — no check could see them, because nothing outside a real Linux build ever ran that path. `release-windows-check.yml` already carries this lesson, in its own words: the Windows release path first ran on release day, on main, with a tag reserved. This is the same lane for Linux, and its `paths` list overlaps because the packaging configuration and the manifests decide the identity both platforms need — those files changed here, the Windows lane ran, and it passed, because neither defect was observable on Windows. x64 only. arm64 is built from the same scripts, configuration and descriptor, so this lane would re-prove everything it can reach and nothing that differs — a 16K-page host and its own native peer. The Nightly matrix builds and verifies arm64 every night. This covers what the contract test structurally cannot: fpm, the AppImage runtime, the native peer, and verification passing over real payloads. Generated-by: Claude Opus 5 via Claude Code * ci: name the Linux check job so it is distinguishable Both lanes named their job `package`, so a pull request showed two checks called `package` with no way to tell which platform failed. Generated-by: Claude Opus 5 via Claude Code * ci: drop the Linux check's cron The Windows lane pairs its path filter with a nightly cron because the filter is a pre-filter, not an import closure. This lane needs no such pairing: the Nightly matrix already runs `package:linux` and `verify:linux` for both architectures on every npm Nightly publication, which is more often than a daily cron. The Windows cron earns its runner by also exercising the installer lifecycle, automatic update and rollback; this one would only buy a second copy of the same run. Generated-by: Claude Opus 5 via Claude Code * ci: hold the Linux check to the repository's lane policy `ci-workflow-policy.test.mjs` rejected the lane on three counts, and it was right on all three. Restores the cron I had removed one commit earlier. I argued the Nightly made it redundant because that matrix runs the same two commands. The policy is enumerated over the whole workflow directory precisely so that reasoning cannot be applied lane by lane: a path filter is a pre-filter, not an import closure, so the lane itself must be reachable without consulting it. The Nightly is a different workflow, gated on a successful npm publication and a repository variable, so it is not this lane's escape from its own filter. Aligns the timeout to 45, the single tier every pull-request lane carries, rather than the 60 I picked with nothing behind it. The first real run packaged and verified in 6m22s. Registers the lane in the pull-request allowlist, which exists because a workflow that nobody remembered to name once kept an unbounded trigger. Generated-by: Claude Opus 5 via Claude Code * fix(desktop): let the desktop environment recognise the running window electron-builder warned about this once per Linux target in the packaging log, and named the fix; nobody read the log, because the lane was green. Electron derives its app_id — the window's WM_CLASS — from `desktopName` in the manifest. Absent that field, the desktop entry's `StartupWMClass` falls back to the product name, so the entry advertises `Maka` while the window reports `maka`. They never match, and a desktop environment that cannot link the two shows a generic icon and silently ignores an attempt to pin the app. Every Linux user meets this on first launch. Setting `desktopName` in the manifest and `linux.syncDesktopName` in the configuration derives the entry's filename and the app_id from one string. Generated-by: Claude Opus 5 via Claude Code * test(desktop): verify Linux as deeply as macOS and Windows Every Linux assertion read files. `verify-macos-dmg.mjs` asserts the executable's architecture with `lipo`, runs a pty probe, smokes the filesystem worker and launches the renderer over CDP; the Windows lane runs its sandbox probe. Linux never ran anything it had built. Each defect this branch collected — the unlaunchable executable name, the unread deb, the unset desktop name — was some check the other two platforms already had and Linux did not. Three assertions close that gap: The Runtime Host peer's ELF `e_machine` is read in both payloads. The packaging scripts refuse to cross-build precisely because a mismatched peer produces a package that installs and dies at launch, and Linux was the only platform where nothing could detect it. `assertElfArchitecture` is exported and covered by `verify-linux-harness.test.mjs`, which builds ELF headers byte by byte and so runs on any host. The deb's `Package:` field must be an installable Debian name. electron-builder derives it from the product name, and `dpkg` rejects a capital letter, so a package nobody could install would otherwise reach the Nightly — which publishes without a human. I could not settle this by reading fpm's sources, and reading sources instead of running them is the mistake this branch keeps repeating, so it is now a question CI answers. The extracted AppImage is launched under `xvfb-run`. Only this can fail on a package that is structurally perfect and still will not start. It is applied to the AppImage alone: extracting it yields the tree its runtime mounts at launch, whereas `dpkg-deb -x` output is not an installation — `dpkg` would still have to set the sandbox helper's setuid bit — so launching that would prove something about a tree no user has. Also drops the lane comment claiming it covered "the AppImage runtime and the native peer", which described none of what the lane did; requires exactly one `resources/app.asar` in the deb rather than taking whichever `find` returned first; and extends the path filter to what the packaging configuration and the verifier actually import, plus the icon catalogue `assertPackagedResources` enumerates to build its own required list. Generated-by: Claude Opus 5 via Claude Code * refactor(desktop): let the descriptor name the macOS DMG This branch exists to make `desktop-release-targets.mjs` the one place that knows what a target produces, and it freed the Nightly's artifact staging from spelling names in YAML. Then it added three new spellings in the same diff: macOS went from one architecture to two, so the hardcoded `-mac-arm64.dmg` became `-mac-${{ matrix.arch }}.dmg` in the signing step, the verification step, and again in the Nightly. A second authority on the artifact name, and unlike the descriptor nothing checked this one. `verify:macos` now takes an architecture and resolves the path from the descriptor, the way `verify:linux` already did — which also means the verification fails if the descriptor and the build ever disagree. The signing and notarization steps discover the DMG instead: a runner builds one architecture, so exactly one exists, and signing never needed its name. Finding none or several now fails the step rather than passing a wrong path to `codesign`. Two spellings remain, both older than this branch and neither on the matrix: the arm64-only automatic-update ZIP and the Windows installer. Generated-by: Claude Opus 5 via Claude Code * refactor(scripts): drop an identity remap and two unused exports `resolveProductReleaseIdentity` rebuilt each published feed as `{name, advertised, mergedFrom}` — the exact shape `desktopPublishedFeeds` already returns, field for field. The comment above it described what the function itself does. `mergeDesktopUpdateFeedDocuments` and `desktopNightlyPublishedFeeds` were exported with no consumer outside their own modules, tests included. Two fewer public contracts to keep. Generated-by: Claude Opus 5 via Claude Code * fix(test): copy the builder configuration through JSON The contract test isolates a configuration copy per packager because electron-builder rewrites the object it is handed. structuredClone did that locally on Node 26 and failed on the Node 24 the CI lane runs: "Unable to deserialize cloned data due to invalid or unsupported version". Everything electron-builder reads out of this configuration has to survive serialization anyway, and JSON round-tripping behaves the same on every version. `beforePack` is the one function in it and is carried across by reference. Generated-by: Claude Opus 5 via Claude Code * fix(desktop): verify the update the updater chose, not a platform table The attestation verifier held a hand-written platform/arch to filename table that named only the macOS arm64 ZIP and the Windows installer, so every target this branch adds ended in a download error with a valid feed, payload and attestation. The table was also a second authority for what is built, and could not be completed: electron-updater serves the arm64 ZIP to an x64 build under Rosetta, and one Linux tuple installs either the AppImage or the deb. Identify the download by the name electron-updater gave it, require that name among the feed entries the updater was offered and under the released version, and keep the attestation subject as the trust anchor. * fix(release): hand Finalize the published set the publish job verified Merging the per-architecture macOS feeds split the set each runner uploads from the set that is published, but the merged result lived only in the publish job's working directory. Finalize rebuilt its input from the raw runner artifacts and failed the exact-set check deterministically (missing latest-mac.yml; unexpected latest-mac-arm64.yml, latest-mac-x64.yml), and would have attested the per-architecture feeds rather than the one users download. The checklist's `list` command printed the same stale set. Upload the verified release-assets directory as a run artifact, have Finalize consume it instead of reconstructing it, and make `list` report the published set. * test(desktop): verify the merged Linux update feed before checksums Linux is the one platform whose feed this repository assembles itself, and the merged bytes were first read at publication. Verify them in the Linux lane against the payloads it just accepted. * fix(ci): run the Linux harness after dependencies are installed Reading a feed goes through the yaml package, so the suite can no longer run in the planner step that has only node builtins. * test: drop a vacuous checksum guard and a hand-copied payload list The harness asserted that a function with no write path wrote nothing, and the attestation suite restated the descriptor's payload names that desktop-release-targets.test.mjs already pins to electron-builder. * refactor(scripts): let every packaging and verify script read the descriptor The target descriptor was declared the one authority on what each desktop target builds, advertises and checksums, but only the Linux scripts read it: macOS and Windows packaging still spelled their own payload and feed names, and their verifiers hard-coded which files get a checksum while the descriptor's `checksums` mirrored them by hand. Resolve every name from the descriptor, and let the verifiers take an architecture the way `verify:linux` does. * refactor(release): drop the restated Windows installer name and the matrix target key The identity's `exe` output and ten inline spellings of the installer name were the Windows twin of the macOS `dmg` output this branch already removed; the workflows now discover the installer the way they discover the DMG, and verify it through the descriptor. The matrix `target` key was always `${platform}-${arch}`, which both policy tests asserted. * refactor(scripts): resolve a release target in one place Six scripts repeated the same lookup — manifest version, nightly override, descriptor entry, release directory — and the macOS autoupdate step still spelled its ZIP in YAML. One resolver owns it now, and the runner-label assertions check that each matrix row's runner suits its platform and architecture instead of restating the list. * docs(scripts): state the rule behind the checksum set A checksum is issued only for a payload the verify script opened; the macOS script opens the DMG alone. * fix(release): run the Linux verification under xvfb on every lane The verifier launches the extracted AppImage's renderer over CDP, and only the pull-request lane ran it under xvfb-run; the release and Nightly matrices called it bare, which fails on a runner without a display. Wrap all three, install xvfb the way ci.yml does, and hold every workflow to it. Alongside, the checks the review exposed as never executed now run over real functions: the Linux harness merges two single-document feeds with mergeDesktopUpdateFeeds before verifying, and a release test drives the publish job's merge and verify over a directory built from every staged group. The macOS verifier asserts the architecture it was asked for, and the three verifiers take the channel from the resolved target instead of re-reading the Nightly environment. * fix(desktop): resolve the local setup package without naming a peer target A packaged build never reads the development peer target — it ships its own setup package — but both local callers evaluated it eagerly, and the whitelist has no darwin-x64 because no npm prebuild exists for it. An Intel Mac, which this branch now ships with its own native peer, would throw at first use of local remote access. Read the target only where a development build needs it; remote callers keep choosing their own. * fix(desktop): require the downloaded update to be this platform's package The feed-derived identity accepted any same-version artifact the release attested, including the CLI archive and other platforms' packages. Match the platform and format only — never the architecture, so a Rosetta process still accepts the arm64 ZIP — and read a legacy feed without a files list as offering nothing. * docs: describe the five-target Desktop in the README and release notes
1 parent 43aa35e commit beecaa5

51 files changed

Lines changed: 2735 additions & 450 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/DESKTOP_NIGHTLY.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
Desktop Nightly is an ephemeral developer snapshot, not an Apache release. It builds the current `main` commit every day so contributors can try recent Desktop changes and report problems without waiting for an ASF source-release vote.
2323

24-
The npm publication workflow gives each snapshot an immutable version such as `0.2.0-dev.42.20260829`. The run number is the sole ordering authority. After that exact npm version is public, it triggers Desktop Nightly with a version-only artifact; the authenticated workflow event supplies the exact source commit and upstream run. Each fresh Desktop Nightly creates a `v<version>` tag protected by the checked-in `Immutable release tags` ruleset and one GitHub draft prerelease containing the macOS and Windows packages, blockmaps, `dev-mac.yml`, `dev.yml`, and one offline Sigstore bundle. The workflow verifies every remote asset before it publishes the prerelease as non-Latest. Packaged Nightlies use the GitHub `dev` channel and verify that downloaded bytes were attested by `.github/workflows/desktop-nightly.yml` on `main`. A formal Desktop build uses the separate stable GitHub Release channel and formal product-release attestation identity.
24+
The npm publication workflow gives each snapshot an immutable version such as `0.2.0-dev.42.20260829`. The run number is the sole ordering authority. After that exact npm version is public, it triggers Desktop Nightly with a version-only artifact; the authenticated workflow event supplies the exact source commit and upstream run. Each fresh Desktop Nightly creates a `v<version>` tag protected by the checked-in `Immutable release tags` ruleset and one GitHub draft prerelease containing the macOS, Windows, and Linux packages, blockmaps, `dev-mac.yml`, `dev.yml`, `dev-linux.yml`, `dev-linux-arm64.yml`, and one offline Sigstore bundle. macOS builds one architecture per runner and both write the same feed name, so each upload carries `dev-mac-<arch>.yml` and publication merges them into the single `dev-mac.yml` clients read. The workflow verifies every remote asset before it publishes the prerelease as non-Latest. Packaged Nightlies use the GitHub `dev` channel and verify that downloaded bytes were attested by `.github/workflows/desktop-nightly.yml` on `main`. A formal Desktop build uses the separate stable GitHub Release channel and formal product-release attestation identity.
2525

2626
Nightly currently uses the same application identity as the formal Desktop. Installing it replaces the existing Maka installation rather than creating a second side-by-side app. Its user data remains in the same location. Testers who need the formal build should reinstall that build before returning to the formal channel. Builds previously downloaded from `nightlies.apache.org` do not migrate automatically; testers must install the newest GitHub prerelease once, after which GitHub Nightlies update automatically.
2727

@@ -31,8 +31,8 @@ Nightly currently uses the same application identity as the formal Desktop. Inst
3131
2. Configure npm Trusted Publishing for `apache/maka` and `.github/workflows/npm-publication.yml`, restricted to the `npm-publication` Environment and with both `npm publish` and `npm stage publish` allowed. Do not create or store a long-lived npm token.
3232
3. After npm Trusted Publishing is ready, set `NPM_NIGHTLY_ENABLED` to `true`, run `npm publication` from `main` with `channel=nightly`, and verify the exact npm version and `nightly` dist-tag.
3333
4. Set `DESKTOP_NIGHTLY_ENABLED` to `true` and manually dispatch a fresh npm Nightly. Confirm that its successful run triggers `Desktop Nightly`. Do not rerun a failed attempt in place.
34-
5. Verify that `v<version>` points to the exact source SHA and that its GitHub Release is published with Draft off, Prerelease on, Latest off, and exactly the nine expected assets. Install that prerelease on both platforms.
35-
6. Publish one later fresh Nightly and confirm a GitHub-to-GitHub automatic and differential update on both platforms before sharing the channel with testers.
34+
5. Verify that `v<version>` points to the exact source SHA and that its GitHub Release is published with Draft off, Prerelease on, Latest off, and exactly the expected assets, which `desktopNightlyReleaseAssetNames` defines. Install that prerelease on every packaged target.
35+
6. Publish one later fresh Nightly and confirm a GitHub-to-GitHub automatic and differential update on every packaged target before sharing the channel with testers. Linux AppImage installs update in place; Linux deb installs ask for the privilege `dpkg` needs.
3636

3737
The npm schedule starts at 18:17 UTC. Before changing the npm tag, the workflow requires its run number to exceed the current `nightly` version. Desktop assembles and verifies a draft before one publish mutation; a packaging, attestation, tag, upload, or digest failure leaves no partially published GitHub Release. Never rerun a failed workflow attempt in place; dispatch a fresh npm Nightly with a newer version.
3838

.github/RELEASE_CHECKLIST.md

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@ gate. The workflow creates no Draft until every required artifact job succeeds.
2727

2828
Phase 1 requires:
2929

30-
- signed and notarized Apple Silicon macOS Desktop artifacts;
30+
- signed and notarized Apple Silicon and Intel macOS Desktop artifacts;
3131
- the unsigned Windows x64 Desktop installer and ZIP;
32+
- the unsigned x64 and arm64 Linux AppImage and deb;
3233
- the signed, notarized, relocatable Apple Silicon CLI/TUI ZIP;
3334
- checksums generated after each artifact reaches its final form.
3435

@@ -84,12 +85,12 @@ bytes and expected filename are not covered by that protected workflow identity.
8485
version, and the CLI manifest exposes only the `maka` command.
8586
4. Dispatch `Release` from the exact approved candidate tag and supply the same tag as
8687
`source_reference_tag`. A rerun must use that same tag; never select current `main` instead.
87-
5. Confirm `release-identity`, both Desktop matrix entries, `cli-macos-arm64`, and
88+
5. Confirm `release-identity`, every Desktop matrix entry, `cli-macos-arm64`, and
8889
`publish` pass. A skipped or failed required job must prevent Draft creation.
8990
6. Confirm one Draft named `v<version>` targets the approved source SHA, identifies the ASF source
9091
reference in its notes, is not marked as a GitHub prerelease or Latest while it remains a Draft,
91-
and contains exactly the manifest
92-
reported by `node scripts/product-release-artifacts.mjs list`. The manifest covers both Desktop
92+
and contains exactly the release asset names
93+
reported by `node scripts/product-release-artifacts.mjs list`. That list covers all three Desktop
9394
platforms and update metadata, the standalone CLI/TUI, and their required checksums.
9495
7. Inspect the CLI ZIP. It must contain `bin/maka`, `RELEASE.json`, `DISCLAIMER-WIP`, `LICENSE`, `NOTICE`,
9596
`THIRD_PARTY_NOTICES.txt`, the pinned Node license, and no `bin/maka-agent`.
@@ -100,7 +101,7 @@ bytes and expected filename are not covered by that protected workflow identity.
100101
entitlements must retain the required hardened-runtime capabilities and omit
101102
`com.apple.security.get-task-allow`, as required by Apple's
102103
[notarization guidance](https://developer.apple.com/documentation/security/resolving-common-notarization-issues).
103-
9. Inspect both Desktop resource trees and confirm `git/`, `bundled-git.json`, `licenses/git/`, and
104+
9. Inspect every Desktop resource tree and confirm `git/`, `bundled-git.json`, `licenses/git/`, and
104105
`licenses/dugite/` are absent.
105106

106107
If the publish job created the product tag or Draft but failed before every asset was uploaded,
@@ -131,7 +132,11 @@ stable release Latest in the same GitHub operation. Do not publish or
131132
change the Latest designation manually. A failed or rejected npm candidate requires a new product
132133
version; never publish the Draft to work around npm state.
133134

134-
## Acceptance on another Apple Silicon Mac
135+
## Acceptance on another Mac
136+
137+
Run this section twice: once on an Apple Silicon Mac with the `mac-arm64` DMG, and once on an Intel
138+
Mac with the `mac-x64` DMG. The CLI/TUI ships for Apple Silicon only, so steps 4 to 7 belong to the
139+
Apple Silicon pass.
135140

136141
Download the DMG, CLI ZIP, and their checksum files through a browser from the Draft. Do not move
137142
artifacts directly from the workflow runner; the browser path supplies the real quarantine
@@ -168,9 +173,26 @@ Download the installer, Windows Desktop ZIP, and both checksum files through a b
168173
7. Add a clean remote Runtime Host from the packaged Desktop app. Confirm setup installs the exact
169174
public `maka-agent@<version>` package and the remote session completes one model turn.
170175

176+
## Acceptance on a Linux machine
177+
178+
Run this section twice: once on x64 with the `x86_64` AppImage and the `amd64` deb, and once on
179+
arm64 with the `arm64` pair. Download both distributables and their checksum files through a browser
180+
from the same Draft.
181+
182+
1. Run `sha256sum -c` for the AppImage and the deb.
183+
2. `chmod +x` the AppImage and launch it once to confirm the portable artifact starts.
184+
3. Install the deb with `sudo apt install ./Maka-<version>-linux-<arch>.deb` and launch Maka from the
185+
desktop launcher entry.
186+
4. Configure a model connection, send one prompt, and run one representative file-tool task.
187+
5. Run one terminal task and confirm packaged `node-pty` behavior.
188+
6. Confirm the documented Computer Use limitation remains accurate: Computer Use is not offered on
189+
Linux.
190+
7. Add a clean remote Runtime Host from the packaged Desktop app. Confirm setup installs the exact
191+
public `maka-agent@<version>` package and the remote session completes one model turn.
192+
171193
Immediately before approving the `product-release` Environment, reverify that the approved ASF
172194
candidate tag and convenience `v<version>` tag still resolve to the same recorded commit. Approve
173-
only after npm verification and both independent-machine acceptance passes. If any required artifact, npm step, or
195+
only after npm verification and every independent-machine acceptance pass. If any required artifact, npm step, or
174196
acceptance step fails, keep the Draft unpublished, fix the issue, increment the root product
175197
version, and run the full workflow again. Never replace an existing release identity.
176198

.github/workflows/desktop-nightly.yml

Lines changed: 54 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,20 @@ jobs:
8787
matrix:
8888
include:
8989
- platform: macos
90+
arch: arm64
9091
runner: macos-15
92+
- platform: macos
93+
arch: x64
94+
runner: macos-15-intel
9195
- platform: windows
96+
arch: x64
9297
runner: windows-2025
98+
- platform: linux
99+
arch: x64
100+
runner: ubuntu-24.04
101+
- platform: linux
102+
arch: arm64
103+
runner: ubuntu-24.04-arm
93104
runs-on: ${{ matrix.runner }}
94105
environment: nightly
95106
timeout-minutes: 75
@@ -145,60 +156,69 @@ jobs:
145156
APPLE_API_KEY: ${{ runner.temp }}/AuthKey_Maka.p8
146157
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
147158
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
148-
run: npm run package:macos-arm64
159+
run: npm run package:macos-${{ matrix.arch }}
149160

150161
- name: Notarize and verify the macOS Nightly
151162
if: matrix.platform == 'macos'
152163
env:
153164
APPLE_API_KEY: ${{ runner.temp }}/AuthKey_Maka.p8
154165
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
155166
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
156-
DMG_PATH: apps/desktop/release/Maka-${{ needs.identity.outputs.version }}-mac-arm64.dmg
157167
run: |
168+
# Discovered, not spelled out — see the same step in release.yml.
169+
shopt -s nullglob
170+
dmgs=(apps/desktop/release/*.dmg)
171+
if [[ ${#dmgs[@]} -ne 1 ]]; then
172+
echo "Expected exactly one DMG to notarize, found ${#dmgs[@]}" >&2
173+
exit 1
174+
fi
175+
DMG_PATH="${dmgs[0]}"
176+
158177
xcrun notarytool submit "$DMG_PATH" \
159178
--key "$APPLE_API_KEY" \
160179
--key-id "$APPLE_API_KEY_ID" \
161180
--issuer "$APPLE_API_ISSUER" \
162181
--wait
163182
xcrun stapler staple "$DMG_PATH"
164-
npm run verify:macos-arm64 -- "$DMG_PATH"
183+
npm run verify:macos -- "${{ matrix.arch }}"
165184
166185
- name: Package the Windows Nightly
167186
if: matrix.platform == 'windows'
168187
run: npm run package:windows-x64
169188

170189
- name: Verify the Windows Nightly
171190
if: matrix.platform == 'windows'
172-
run: >-
173-
npm run verify:windows-x64 --
174-
"apps/desktop/release/Maka-${{ needs.identity.outputs.version }}-win-x64.exe"
191+
run: npm run verify:windows-x64 -- "${{ matrix.arch }}"
192+
193+
- name: Package the Linux Nightly
194+
if: matrix.platform == 'linux'
195+
run: npm run package:linux
196+
197+
- name: Ensure xvfb
198+
if: matrix.platform == 'linux'
199+
run: command -v xvfb-run >/dev/null 2>&1 || { sudo apt-get update && sudo apt-get install -y xvfb; }
200+
201+
# `xvfb-run` because the verification launches the extracted AppImage's
202+
# renderer over CDP, the way the macOS and Windows verifications do.
203+
- name: Verify the Linux Nightly
204+
if: matrix.platform == 'linux'
205+
run: xvfb-run -a npm run verify:linux -- "${{ matrix.arch }}"
175206

176207
- name: Stage the exact Nightly artifacts
177208
env:
178209
NIGHTLY_VERSION: ${{ needs.identity.outputs.version }}
179210
STAGE_DIRECTORY: ${{ runner.temp }}/desktop-nightly
180211
run: |
181-
mkdir -p "$STAGE_DIRECTORY"
182-
if [[ "${{ matrix.platform }}" = macos ]]; then
183-
cp -- \
184-
"apps/desktop/release/Maka-$NIGHTLY_VERSION-mac-arm64.dmg" \
185-
"apps/desktop/release/Maka-$NIGHTLY_VERSION-mac-arm64.zip" \
186-
"apps/desktop/release/Maka-$NIGHTLY_VERSION-mac-arm64.zip.blockmap" \
187-
apps/desktop/release/dev-mac.yml \
188-
"$STAGE_DIRECTORY/"
189-
else
190-
cp -- \
191-
"apps/desktop/release/Maka-$NIGHTLY_VERSION-win-x64.exe" \
192-
"apps/desktop/release/Maka-$NIGHTLY_VERSION-win-x64.exe.blockmap" \
193-
"apps/desktop/release/Maka-$NIGHTLY_VERSION-win-x64.zip" \
194-
apps/desktop/release/dev.yml \
195-
"$STAGE_DIRECTORY/"
196-
fi
212+
node scripts/desktop-nightly.mjs stage-target \
213+
"${{ matrix.platform }}-${{ matrix.arch }}" \
214+
apps/desktop/release \
215+
"$STAGE_DIRECTORY" \
216+
"$NIGHTLY_VERSION"
197217
198218
- name: Preserve the verified Nightly artifacts
199219
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
200220
with:
201-
name: desktop-nightly-${{ matrix.platform }}
221+
name: desktop-nightly-${{ matrix.platform }}-${{ matrix.arch }}
202222
path: ${{ runner.temp }}/desktop-nightly
203223
if-no-files-found: error
204224
compression-level: 0
@@ -236,7 +256,7 @@ jobs:
236256
- name: Install publisher dependencies
237257
run: npm ci --ignore-scripts
238258

239-
- name: Download both verified Desktop builds
259+
- name: Download every verified Desktop build
240260
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
241261
with:
242262
pattern: desktop-nightly-*
@@ -263,7 +283,15 @@ jobs:
263283
ATTESTATION_BUNDLE: ${{ steps.attest.outputs.bundle-path }}
264284
CERTIFICATE_IDENTITY: https://github.com/${{ github.repository }}/.github/workflows/desktop-nightly.yml@refs/heads/main
265285
GH_TOKEN: ${{ github.token }}
286+
NIGHTLY_VERSION: ${{ needs.identity.outputs.version }}
266287
run: |
288+
# Verify what is on disk, and prove the count matches what the target
289+
# descriptor says was attested. The list is written to a file first
290+
# because a process substitution hides its exit code, which would let
291+
# a failing lister leave this step green with nothing verified.
292+
node scripts/desktop-nightly.mjs attested-assets "$NIGHTLY_VERSION" \
293+
> "$RUNNER_TEMP/attested-assets.txt"
294+
expected="$(wc -l < "$RUNNER_TEMP/attested-assets.txt")"
267295
verified=0
268296
while IFS= read -r -d '' artifact; do
269297
gh attestation verify "$artifact" \
@@ -273,8 +301,8 @@ jobs:
273301
--cert-oidc-issuer https://token.actions.githubusercontent.com
274302
verified=$((verified + 1))
275303
done < <(find "$GITHUB_WORKSPACE/.nightly-stage/release" -maxdepth 1 -type f -print0)
276-
if (( verified != 8 )); then
277-
echo "Expected 8 verified Desktop Nightly subjects, found $verified" >&2
304+
if (( verified != expected )); then
305+
echo "Expected $expected verified Desktop Nightly subjects, found $verified" >&2
278306
exit 1
279307
fi
280308

.github/workflows/release-cli-finalize.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,9 +235,8 @@ jobs:
235235
- name: Download the exact verified Release run artifacts
236236
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
237237
with:
238-
pattern: release-*-${{ needs.inspect.outputs.release_run_attempt }}
238+
name: product-release-assets-${{ needs.inspect.outputs.release_run_attempt }}
239239
path: ${{ runner.temp }}/product-release
240-
merge-multiple: true
241240
github-token: ${{ github.token }}
242241
repository: ${{ github.repository }}
243242
run-id: ${{ needs.inspect.outputs.release_run_id }}

0 commit comments

Comments
 (0)