How to cut, sign, notarize, and publish the Electron desktop app, plus the packaging trap catalog. Desktop-scoped packaging invariants (asar layout, preload, the extraResources runtime contract, electron-builder.yml structure) live in apps/desktop/AGENTS.md; local dev/test/E2E in docs/DEVELOPMENT.md. Desktop has the automated tag-driven delivery pipeline. Mobile has a manually dispatched production build/submit workflow, but is not yet a release-please component; webview and daemon have no publish workflow.
- Seven GitHub Actions workflows, three script modules, and two composite actions own the release path:
.github/workflows/ci.yml("CI") — runs on every PR..github/workflows/release-please.yml("Release Please") — maintains release PRs after pushes tomaster; it never tags or publishes..github/workflows/finalize-releases.yml("Finalize Releases") — after a successfulmasterCI, turns a merged release PR into a draft Release and pushes its validated tag..github/workflows/build-desktop.yml("Build Desktop") — reusable packaging workflow; not PR-triggered..github/workflows/release-desktop.yml("Release Desktop") — tag-triggered publish..github/workflows/build-mobile.yml("Build Mobile") — manual Android/iOS production builds on GitHub runners, with optional EAS Submit..github/workflows/release-brand-matrix.yml("Release Brand Matrix") — strict brand × Desktop/iOS/Android orchestration, isolated artifacts, compliance, provenance, and optional signing/upload..github/scripts/release-automation.cjs— tested Octokit policy for candidate resolution, recovery, and Release preflight checks..github/scripts/brand-matrix.cjs/release-inputs.cjs— fail-closed matrix and release-input validation..github/actions/build-sidecar— composite action that builds the PTY sidecar per arch..github/actions/render-release-config— renders only through the exact publisher/source commits pinned by each release manifest.
- All jobs run on Blacksmith runners, not stock GitHub:
blacksmith-2vcpu-ubuntu-2404(CI + the publish job), and thebuild-desktopmatrix usesblacksmith-6vcpu-macos-26(arm64/M4; Xcode 26 soactool >= 26compilesmac.iconintoAssets.car),blacksmith-4vcpu-windows-2025(VS Build Tools, enough for NSIS), andblacksmith-4vcpu-ubuntu-2204(older glibc for broader AppImage compatibility).
ci.ymltriggers on push tomaster/release/*,pull_request(paths-ignore**.md), andworkflow_dispatch. Jobs: typescript (pnpm format:check,pnpm lint,pnpm typecheck, buildlinkcode-pty, required-sidecar Vitest, then compiled-daemon process acceptance), desktop (unpackaged Electron entry, window-state persistence, plus unsigned packaged devshell), webview (production bundle plus Chromium browser smoke), mobile (Android and iOS production Expo exports), rust (cargo fmt --check,cargo clippy --all-targets --locked -- -D warnings,cargo test --locked), and All Green (needs: [typescript, desktop, webview, mobile, rust],if: always(), fails unless all five required jobs pass).NODE_OPTIONS=--max-old-space-size=4096; installs usepnpm install --frozen-lockfile, so a driftedpnpm-lock.yamlfails CI at the install step. Markdown-only PRs skip CI (paths-ignoreapplies to thepull_requesttrigger only; pushes tomaster/release/*always run). Reproduce CI locally with--frozen-lockfile+ the 4 GB heap.- No status check gates merges (GitHub-side settings, read via
gh apiin 2026-07 — re-check there; none of this lives in the tree). There is no FOSSA/Snyk/Codecov/Sonar/Renovate/Dependabot config in the tree; the default-branch ruleset gates on code-owner review + review-thread resolution + Copilot review, notrequired_status_checks. Code-review apps (greptile, pullfrog) are advisory and block nothing. "All Green" is the aggregate check intended for branch protection if one is ever required — require it rather than the individual jobs (brittle). build-desktop.ymldoes not validate PRs — itspull_request:block is commented out (cost). Desktop packaging runs only viaworkflow_call(fromrelease-desktop.yml) orgh workflow run build-desktop.yml --ref <branch>. Packaging regressions surface at release/dispatch, never on a PR.
- Never hand-edit a release version or push a release tag.
release-please-config.jsonand.release-please-manifest.jsonbootstrap Desktop at the last published version; conventionalfix/feat/ breaking commits update a dedicated release PR, includingapps/desktop/package.jsonandapps/desktop/CHANGELOG.md. Desktop is a root product component so daemon, sidecar, and shared-package changes count, while pureapps/mobileandapps/webviewcommits are excluded. Merge that PR when the accumulated notes and proposed version are ready to ship. - The release PR merge runs normal
masterCI. EverymasterSHA has its own CI concurrency group so later pushes cannot cancel or replace that exact validation. Only after All Green succeeds doesfinalize-releases.ymlbind the pending release PR to its tested merge SHA, verifyv${version}againstapps/desktop/package.json, and ask release-please to create the lightweight tag plus a draft GitHub Release. Ordinary commits may advancemasterwhile that CI runs, but the release config, manifest, and automation script must remain byte-identical to the tested merge.force-tag-creationmakes release-please push the tag even though the Release stays draft. The App token is load-bearing: a tag created withGITHUB_TOKENwould not start another workflow. release-desktop.ymlfires on the tag push (orworkflow_dispatchwithdry_run, defaulttrue). Its build job callsbuild-desktop.ymlwithsign: true+secrets: inherit; its release job (environment: release) syncsdesktop-*artifacts to R2, reuses the release-please draft, uploads all assets, and then publishes it. A dispatch withdry_run: truebuilds + signs but publishes nothing.- Package-manager bumps close the release job, on stable
v*.*.*tags only (no-prerelease suffix), each with a short-lived token minted from the org GitHub App (BOT_APP_ID/BOT_APP_PRIVATE_KEY) — absent secrets make them self-skip. Homebrew: token scoped toarcboxlabs/homebrew-tap, then that repo'sbump-caskaction forlinkcodewith the two DMG sha256s. WinGet: token scoped to the org forkarcboxlabs/winget-pkgs, thenvedantmgoyal9/winget-releaser(komac update --submitunderneath) opens a version PR onmicrosoft/winget-pkgsforArcBox.LinkCodefrom the release's.exeassets. Both WinGet steps arecontinue-on-errorso packaging never fails a release — check the step status in the release job, not the release itself. Two constraints: the package must already exist upstream (the action only updates; a from-scratch submission iskomac new, no manifests are kept in this repo), and an App installation token only covers installed repos, so if the upstream PR comes backresource not accessible by integrationthe fallback is a classic PAT withpublic_repofor a bot user that can push to the fork. - electron-builder derives artifact names and the updater feed from package.json, not the tag, so
build-desktop.ymlindependently fails unlessv${version}equalsGITHUB_REF_NAME. The GitHub Release is for human downloads only — the updater reads the R2 feed. Tags containing-(e.g.v1.2.3-beta.1) publish as prerelease.release-desktop.ymlconcurrency iscancel-in-progress: false— never cancel a release mid-flight.
build-mobile.yml runs eas build --local once per platform: Android on
${{ vars.CI_RUNNER_LINUX || 'ubuntu-latest' }} and iOS on
${{ vars.CI_RUNNER_MACOS || 'macos-26' }}. Compilation and the resulting .aab / .ipa stay on
GitHub infrastructure; EAS is still contacted to resolve the project, managed signing credentials,
remote build numbers, the production Update channel, and optional store submission. Each signed
artifact is retained in the workflow run for seven days.
Dispatches default submit to false. Setting it to true uploads the exact artifacts just built
with eas submit --path --wait after both platform builds succeed: iOS goes to App Store
Connect/TestFlight, while Android goes to the Google Play internal track. This does not submit an
iOS build to App Review or promote Android beyond internal testing.
The jobs run in GitHub's release environment. Before adding mobile secrets, require a reviewer and
restrict deployment branches/tags there; an unprotected environment does not isolate a robot token
from a manually selected ref. Required GitHub inputs are documented in
ENVIRONMENT.md. Build certificates, provisioning profiles, the Android keystore,
the App Store Connect API key, and the Google Play service-account key remain EAS-managed; do not
duplicate them in GitHub. Before enabling submit, bootstrap those managed credentials
interactively, confirm the App Store Connect/Play app records, and add the App Store Connect
ascAppId to submit.production.ios once Apple assigns it. A submit: true dispatch fails
preflight before either native build until that ID is present. The iOS main app, share extension,
Live Activity target, and App Group must all have valid profiles.
Production uses EAS remote build versions with autoIncrement, serialized by workflow concurrency
so two releases cannot consume versions concurrently. The first build initializes from the local
ios.buildNumber / android.versionCode; if either store already contains a build, sync its highest
value with eas build:version:set before dispatching.
Desktop deliberately remains the only active package in release-please-config.json until the
manual mobile workflow has produced and submitted a validated store build. Activating mobile is then
a separate atomic change: add apps/mobile to the manifest at the current expo.version, configure
release-please's expo strategy with component tags (mobile-v*.*.*), and teach
finalize-releases.yml to start only the matching Mobile workflow. Keep v*.*.* unprefixed tags
reserved for Desktop so existing updater and GitHub download links remain stable.
The Expo strategy updates both apps/mobile/package.json and apps/mobile/app.json: align their starting versions when activating it. Marketing SemVer belongs in expo.version; iOS buildNumber and Android versionCode are monotonically increasing store build identifiers and should be remote EAS-owned rather than release-please-owned. Native store builds and expo-updates production-channel OTA publishes remain separate delivery actions.
Desktop signing and R2 secrets live in the repo's GitHub release Environment (scoped to that environment, not org-wide). Protect it with required reviewers and deployment branch/tag rules before storing credentials. build-desktop.yml sets environment: ${{ inputs.sign && 'release' || '' }}, so unsigned PR/dispatch builds see no secrets and skip signing instead of failing; release-desktop.yml's publish job also runs environment: release or its R2 creds resolve empty. Mobile production jobs always enter the same environment because every store build is signed; GitHub holds the Expo robot token and build-time telemetry values, while EAS holds the native signing and submit credentials.
- macOS — team Developer ID cert (
MACOS_CSC_LINK/MACOS_CSC_KEY_PASSWORD); notarization vianotarytoolwith an App Store Connect API key. electron-builder passesAPPLE_API_KEYas a.p8file path, not content, so a "Materialize App Store Connect API key (.p8)" step decodesAPPLE_API_KEY_BASE64to$RUNNER_TEMP/apple_api_key.p8. Other env:APPLE_API_KEY_ID,APPLE_API_ISSUER,APPLE_TEAM_ID.electron-builder.ymlsetsmac.notarize: true,hardenedRuntime: true,entitlements: build-resources/entitlements.mac.plist. - Windows — Azure Trusted Signing, with no client secret and no declaration in
electron-builder.yml: injected at package time via-c.win.azureSignOptions.*on signed Windows builds only. Auth is OIDC viaazure/login@v2(federated subjectrepo:arcboxlabs/linkcode:environment:release,allow-no-subscriptions: true); the caller needspermissions: id-token: write, and there is deliberately noAZURE_*credential env soDefaultAzureCredentialfalls through to the Azure CLI. Identifiers (releasesecrets):AZURE_PUBLISHER_NAME(must equal the cert subject CN),AZURE_SIGN_ENDPOINT,AZURE_CODE_SIGNING_ACCOUNT,AZURE_CERTIFICATE_PROFILE. Debug it as OIDC +-cinjection, not as a cert file.
- The auto-update feed is an electron-updater
genericprovider athttps://releases.linkcode.ai/desktop(Cloudflare R2 bucketlinkcode-releases), hardcoded inelectron-builder.ymlpublish:and baked into every shipped app — never change it (changing it strands installed clients). The app carries the URL only in packaging config, never in code (apps/desktop/src/main/updater.tsreads it from the baked publish block); auto-update is off in the dev shell. The/desktopprefix namespaces the bucket for future artifact families. - Release builds check at startup and every four hours. electron-updater downloads automatically; once ready, the lower-left sidebar offers an immediate restart or the matching GitHub Changelog. Ignoring the prompt keeps its default install-on-quit behavior.
artifactName: ${productName}-${version}-${arch}.${ext}— the${arch}suffix is load-bearing: electron-updater picks the feed entry whose filename containsprocess.archand silently falls back to the first entry otherwise, handing some clients the wrong arch. All targets build per-arch[x64, arm64], never universal. mac shipsdmg+zip(thezipis required for auto-update — the updater pulls the zip, not the dmg); win shipsnsis(oneClick: false,perMachine: false,buildUniversalInstaller: false); linux shipsAppImage(the only self-updating Linux format) +deb,executableName: linkcode. rpm/snap deliberately off.- R2 publish (
release-desktop.yml):aws s3 sync artifacts/ s3://linkcode-releases/desktop/ --endpoint-url https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.comwith no--delete(prior versions stay for delta updates). Mandatory env:AWS_REGION=auto(R2 ignores it but the CLI requires one),AWS_REQUEST_CHECKSUM_CALCULATION=WHEN_REQUIRED+AWS_RESPONSE_CHECKSUM_VALIDATION=WHEN_REQUIRED(R2 doesn't implement the CRC32 upload checksums recent aws-cli sends). Creds:R2_ACCESS_KEY_ID,R2_SECRET_ACCESS_KEY,R2_ACCOUNT_ID.
Signed desktop builds and every mobile store build embed an immutable config bundle (bootstrap endpoints, public keyrings, bundled defaults) rendered at build time by the config publisher — the client never re-implements rendering. The emergency channel is disabled when its endpoint is null and its public keyring is empty; explicit emergency brands must provide both together. The official render-config jobs download one atomic descriptor from https://config.linkcode.ai/release/v1/linkcode/stable.json. The descriptor carries the exact revision, public keyring, and Desktop/iOS/Android release-manifest bytes with individual SHA-256 digests. The workflow rejects redirects, oversized responses, malformed or noncanonical Base64, unknown fields, digest drift, missing targets, and manifests that do not share one source/publisher/revision/keyring identity.
After that validation, .github/actions/render-release-config checks out publisher code from protected CONFIG_PUBLISHER_REPO at publisherGitSha and structural data from protected CONFIG_SOURCE_REPO at the independent sourceGitSha. It renders through pnpm -F @linkcode/<app> config:render and re-verifies the manifest's digest bindings (revision bytes, public keyring bytes, target identity, telemetry endpoint, expected snapshot SHA-256). Nothing falls back to a mutable Git ref, an unvalidated or cross-organization repository, a global install, or stale generated output.
Each checkout uses its own short-lived installation token minted from the organization secrets
BOT_APP_ID and BOT_APP_PRIVATE_KEY. Trusted workflow steps mint these tokens before checking out
the selected client ref; client-controlled actions receive only repository-scoped read tokens,
never the App private key. Each token requests only Contents read and is explicitly limited to
the selected publisher or source repository. The App must be installed on both private
repositories. Missing secrets or installation access fail before rendering; no long-lived
config-read token is used.
Production rendering reads the root of CONFIG_SOURCE_REPO and remains bound to the downloaded
manifest's exact publisher/source commits, input bytes, target, and expected snapshot digest. It
depends on the normal document origin being available, but does not use a Cloudflare API token,
deployment observer, or copied JSON variables. Workflow code may select only that root or the
reviewed examples/acme-zenith root used by the nonproduction pilot; configuration data cannot
supply a path.
Inputs live in the GitHub release environment and a missing value fails the build with an actionable error:
CONFIG_PUBLISHER_REPO(var) — exact canonicalowner/repositoryidentity for the publisher. The current org-wide App contract requires ownerarcboxlabs; the repository name is not hardcoded. Trusted workflow steps validate and split this value before minting a token restricted to that one repository.CONFIG_SOURCE_REPO(var) — exact canonicalowner/repositoryidentity for structural manifest, layers, and assets. It has the same owner restriction and receives a separate token. The two repository values must differ. Current values arearcboxlabs/linkcodehqandarcboxlabs/linkcode-config, respectively.
No CONFIG_RELEASE_REVISION, CONFIG_RELEASE_KEYRINGS, or
CONFIG_RELEASE_MANIFEST_* Environment variable is used by the official LinkCode
Desktop or Mobile flow. The composite action retains inline JSON inputs only for
the committed nonproduction matrix fixture; they cannot be combined with a
release-input URL.
Enforcement: LINKCODE_REQUIRE_CONFIG_BUNDLE=1 (set for signed desktop builds) makes the Vite main build fail without apps/desktop/generated/config-build-bundle.json and makes verify-artifacts.mts require the staged asar copy, which is always byte-compared against the generated render. Mobile gates twice: pnpm -F @linkcode/mobile config:verify-release before eas build, and the eas-build-pre-install hook inside the EAS project archive rejects the committed { bundle: null } sentinel on production profiles (the root .easignore — which replaces .gitignore for EAS archiving — deliberately lets the generated modules into the archive).
release-brand-matrix.yml is manually dispatched against the exact lowercase 40-hex commit that
loaded the workflow (inputs.ref == github.sha) and rejects commits not already reachable from
master, so protected workflow code, environment ref policy, local actions, and client source have
one trust root. Plan-only requests may supply matrix_json; every build instead requires a
matrix_file directly under .github/release/brand-matrices/ in that same reviewed commit. Add or
update the complete matrix through a PR before dispatching a release; Actions variables are not a
release-plan authority. build, sign, and upload are independent, monotonic gates: signing
requires a build, and upload requires signing. The default (false for all three) only validates
the selected matrix and needs no credential. build: true, sign: false renders one immutable target
set per brand, creates unsigned Desktop packages, and validates production-Hermes exports plus
iOS/Android prebuilds. Nothing is signed or submitted in that path.
The committed code-561-pilot.json is deterministic nonproduction evidence only. It pins
publisher 986d9f21403df53bc932f511eb1b5f0bb634d48d, source
a1ed4d666721c3aed0d563aaea42fce8b5f945b5, and the source root
examples/acme-zenith. The render action byte-compares that root's generated schema mirror with
the canonical schema in the pinned publisher checkout before parsing. Acme and Zenith, their
.invalid endpoints, and this example root are not production brand data.
The JSON root contains brandBuildMatrixVersion: 1 and a non-empty brands array. Every brand has
exactly brandId, channel, sourceRoot, releaseManifests, compliance, and distribution.
sourceRoot is either . for reviewed production data or examples/acme-zenith for the pinned
nonproduction fixture; no other path is accepted. Production roots and every matrix run with
build=true require release-manifest v2, but publicationEvidence is optional and no provider
deployment state is checked.
releaseManifests.desktop|ios|androidare complete release-render manifest v2 objects for a production source root; the reviewed nonproduction example may retain v1. The three targets must share publisher/source commits, config revision, revision digest, and public-keyring digest; target brand/platform/channel mismatches are rejected. Optional publication evidence is validated within the manifest that carries it, but is not required or compared across targets.compliance.desktop|ios|androidhas a lexicographically sorteddisclosedFeaturesarray and a checklist with all five keys set totrue:configurableFeaturesDisclosed,dataPracticesReviewed,noExecutableCode,permissionsReviewed, andstoreMetadataReviewed.distribution.desktopmay benullonly for plan validation. Every build requires an object containingcredentialEnvironment,r2Bucket,r2Prefix, andupdateUrl.credentialEnvironmentmust be exactlyrelease; no per-brand Environment is part of this contract. Both URL and prefix must end in the same brand/channel path. An entry may additionally contain the exact-keylegacyDestinationobject{ r2Bucket, r2Prefix, updateUrl }. Its prefix need not contain the brand/channel segments, allowing an immutable pre-matrix updater feed to remain reachable; when present, Desktop packaging and upload use that destination instead of the standard one. Uploading to a legacy destination additionally requires the workflow ref to be the exactv<package-version>tag, preventing a manualmasterdispatch from replacing an installed app's feed. The field is generic and is accepted only from the reviewed matrix entry—brand IDs do not imply it. Standard and legacy R2 bucket/prefix pairs and update URL paths must not overlap within or across brands.distribution.mobilemay benullonly for plan validation. Every build requireseasProjectId, its exacthttps://u.expo.dev/<id>URL, iOSappleTeamId/ascAppId, and Androidtrack: "internal". EAS project IDs and App Store Connect app IDs must be unique across brands.
After publisher rendering, the gate extracts the actual bundled defaults and requires the
feature/module keys to match disclosedFeatures exactly. Review-like keys outside that disclosure
surface, executable-code key segments (script, code, wasm, plugin, command, and binary
variants), executable URL/file suffixes, and script-like strings fail before any signing starts.
This configuration layer is data-only: it cannot fetch/execute a module or silently enable a
store-review mode. A mobile distribution overlay can set only EAS project/update routing, Apple
team/App Store Connect IDs, and the internal Android track; all other fields are rejected.
Every uploaded build has a canonical release-provenance.<platform>.json. Each listed artifact is
bound by its own SHA-256 and size to the exact brands.manifest.yaml SHA-256, config revision ID,
canonical bundled-defaults SHA-256, config snapshot SHA-256, source/publisher commits, and release
manifest SHA-256, while the sidecar also records the exact client commit and committed matrix-file
SHA-256. Publish jobs re-hash the artifacts and all immutable inputs before upload. The sidecar is
written with create-only semantics after all checks pass.
Brand render jobs, artifact names, runner workspaces, validation roots, credential pairs, and R2
prefixes are separate. Render jobs preserve successful sibling evidence when another row fails,
while aggregate build and publish-preflight jobs require every brand's five provenance sidecars and
upload inputs before any store submission or R2 upload can begin.
Render vars, signing, upload, store, and observability inputs are read from the single protected
release Environment. The reviewed matrix must name that exact Environment for every row. The bot
credentials are organization secrets. Trusted workflow steps report
missing bot credentials before checking out selected client code, and the input scripts report
missing render, signing, or upload values without receiving those bot credentials:
- Official LinkCode
releasevars:CONFIG_PUBLISHER_REPOandCONFIG_SOURCE_REPO. Both must be canonical, differentarcboxlabs/repositoryidentities; malformed, absent, cross-organization, and equal values fail before token minting or checkout. - A nonproduction brand-matrix build additionally uses
CONFIG_RELEASE_REVISIONandCONFIG_RELEASE_KEYRINGSfor its committed Acme/Zenith fixture. Official Desktop and Mobile releases never read those compatibility inputs. - Config checkouts: organization secrets
BOT_APP_IDandBOT_APP_PRIVATE_KEYmint separate, short-lived installation tokens with Contents: read only on the validated publisher and source repositories. The workflow requests no write or organization permission and never passes the App private key to selected code. Each exact commit must be reachable from that repository's reviewedmasterbranch before its role-specific contract is accepted. - macOS Desktop:
MACOS_CSC_LINK,MACOS_CSC_KEY_PASSWORD,APPLE_API_KEY_BASE64,APPLE_API_KEY_ID,APPLE_API_ISSUER, andAPPLE_TEAM_ID. The App Store Connect API key needs only Developer ID notarization access; it must not have app-management or finance roles. - Windows Desktop:
AZURE_TENANT_ID,AZURE_CLIENT_ID,AZURE_PUBLISHER_NAME,AZURE_SIGN_ENDPOINT,AZURE_CODE_SIGNING_ACCOUNT, andAZURE_CERTIFICATE_PROFILE. The Azure app has only the Trusted Signing certificate-profile signer role and an OIDC subject restricted to this repository'sreleaseEnvironment; no client secret exists. - Desktop observability in
release:SENTRY_DSN_DESKTOPandPOSTHOG_PROJECT_TOKENplus thePOSTHOG_HOSTvar. These are required publishable identifiers, not signing credentials. - Mobile:
EXPO_TOKEN,SENTRY_AUTH_TOKEN,SENTRY_DSN_MOBILE, andPOSTHOG_PROJECT_TOKEN. IssueEXPO_TOKENto a robot account with access only to the matrix's EAS projects; scope the Sentry token to source-map upload for the one mobile project. The DSN and PostHog values are publishable identifiers but remain protected release inputs. Native certificates, provisioning profiles, Android keystores, App Store Connect keys, and Google Play service accounts stay EAS-managed and project-scoped. Submissions stop at TestFlight and the Play internal track; this workflow never submits to App Review or promotes a Play release. - Desktop upload:
R2_ACCOUNT_ID,R2_ACCESS_KEY_ID, andR2_SECRET_ACCESS_KEYinrelease. Scope the key pair to the exactr2Bucket/r2Prefixdestinations in the reviewed matrix with object read/write/list only; it must not permit bucket/account administration.R2_ACCOUNT_IDis exactly the lowercase 32-hex Cloudflare account ID; URL-like or otherwise malformed values fail before AWS CLI runs.
Do not store private signing material, access tokens, or service-account JSON in the committed
matrix, repository files, artifacts, or Actions vars. Protect release with required reviewers and
only exact master plus v*.*.* custom deployment policies before enabling build, sign, or
upload. No additional release Environment is required by the brand-matrix workflow.
The environment preflight reads protection metadata with the built-in GITHUB_TOKEN and explicit
actions: read; this metadata-only token cannot approve or bypass an environment review.
- Per-arch single-importer staging (CODE-107). electron-builder never packs
apps/desktopin place;apps/desktop/scripts/package-app.mtsrunspnpm --prod deploy --legacy --cpu=<arch>into one self-contained dir per target architecture outside the workspace, then invokes electron-builder once per dir. This is load-bearing twice: selecting one CPU keeps napi-rs optional bindings target-pure, whileappDir === projectDir === workspaceRootmakes@electron/rebuildfind better-sqlite3 on Windows and keeps the module collector on one importer. Separate macOS/Windows invocations target the same updater manifest, so the script merges theirfilesarrays afterward while retaining x64 as the legacypath/sha512; Linux already names updater manifests per architecture. CI runsnode scripts/package-app.mts <platform> --publish never …in place of a bareelectron-builder. - Cross-arch staging. Each platform build stages both arches' sidecar trees before packaging so
extraResources: sidecar/${arch}resolves: "Build PTY sidecar (both arches)" (.github/actions/build-sidecar). Agent CLI binaries do not ship (CODE-114):filesglobs inelectron-builder.ymlexclude the SDK platform packages from the asar, and the daemon spawns a detected user install or a managed download from its own asset store (@linkcode/assets, CODE-111). The runtimeLINKCODE_PTY_SIDECAR_PATHcontract is desktop-owned — seeapps/desktop/AGENTS.md. - Electron version.
electron-builder.ymlpinselectronVersion: 43.2.0because electron-builder can't read pnpm'scatalog:protocol and downloads per-platform Electron by exact version. Keep it in sync with theelectron: ^43.2.0catalog entry inpnpm-workspace.yaml, or a drift silently packages the wrong Electron. On an Electron bump, also refreshNODE_TARGET/CHROME_TARGETinapps/desktop/vite.shared.tsfrom the new binary (ELECTRON_RUN_AS_NODE=1 electron -p "process.versions").
node apps/desktop/scripts/verify-artifacts.mts <mac|win|linux> runs after every packaging (in CI, and locally from apps/desktop) and fails on packaging regressions. It asserts: the per-arch artifact set is complete and every artifact stays under the 200 MB ceiling (a reintroduced agent binary adds ~66 MB compressed and trips it — CODE-114); every name carries its arch marker; every feed manifest (latest-mac.yml / latest.yml / latest-linux.yml / latest-linux-arm64.yml) points at an on-disk file whose sha512 matches; each unpacked app.asar carries the bundled host runtime (out/daemon/index.mjs + out/drizzle/meta/_journal.json) plus the PTY sidecar (linkcode-pty[.exe]) in Resources; the smartUnpacked better-sqlite3 binding is present and its Mach-O/PE/ELF header targets the app's arch (the tripwire for the CODE-107 Windows rebuild miss — a right-arch/wrong-ABI binding is not header-detectable and is covered by the boot E2E instead); and no agent CLI binaries ship — no Resources/agent-bin, no SDK platform packages in the asar or app.asar.unpacked. It normalizes inner asar paths with inner.replaceAll('/', sep) because @electron/asar splits internal paths by path.sep, so a forward-slash path never matches on Windows.
⨯ … not a filefrom electron-builder on mac → a GitHub Actions${{ … || '' }}yields a set-but-emptyCSC_LINK, which electron-builder still treats as a cert path and resolves againstprojectDir→ carry the cert underMACOS_CSC_LINKandexport CSC_LINKonly inside a run step guarded by[ -n "$MACOS_CSC_LINK" ]; forceCSC_IDENTITY_AUTO_DISCOVERY=falseon unsigned mac builds. (This kept PR builds continuously red.)EMFILEduring packaging (historical) → electron-builder's node-module-collector used to exhaust file descriptors walking this whole monorepo'snode_modules, and pnpm's cross-importer dedup could silently drop a uniquely-placed transitive dep out of the asar (js-yaml → boot crash). Both were multi-importer symptoms; the single-importer staging flow above eliminated them, so the macOSulimitbump and the formerpatches/app-builder-lib@26.15.3.patch(with itspatchedDependenciesentry) are gone. The.pnpmfile.cjsdrizzle-orm↔expo-sqlite peer sever stays — it keeps the expo tree out of the desktoppnpm deployclosure.- Hunting for a
mac.binariesoption to sign the sidecar → it doesn't exist in electron-builder v26;osx-signdiscovers Mach-O binaries inextraResourcesby file header and deep-signs them automatically (the PTY sidecar gets LinkCode's Developer ID + notarization on signed builds). (Team ID observed viacodesign -dvon artifacts, not stored in the tree: LinkCode422ACSY6Y5.) - A new
MAIN_VITE_*value doesn't reach the bundle / turbo serves a stale build → turbo only threads through env declared inapps/desktop/turbo.jsonbuild.env→ add it there.MAIN_VITE_SENTRY_DSNis inlined into the main bundle only on signed builds (inputs.sign && secrets.SENTRY_DSN_DESKTOP || ''; a DSN is a publishable id, not a secret). The packaged daemon supervisor forwards that same value asLINKCODE_SENTRY_DSNand preloadsout/daemon/instrument.mjs. - Webview / mobile Sentry DSNs — repo secrets
SENTRY_DSN_WEBVIEW/SENTRY_DSN_MOBILEmap toVITE_SENTRY_DSN(webview turbobuild.env) andEXPO_PUBLIC_SENTRY_DSN(Expo inlinesEXPO_PUBLIC_*at bundle time).build-mobile.ymlinjects the mobile value directly because EAS variables with Secret visibility are unavailable to local builds. Cloud EAS builds may instead read a plain-text or sensitive value from the project environment selected byeas.json. - Packaged build exits 0 silently, or throws
ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPINGat launch (CODE-101; reproducible only when packaged) → the productName/userData lock-theft + workspace-TS-left-external pair — mechanism and fixes inapps/desktop/AGENTS.md;verify-artifactsplus actually launching the packaged app are the guards. - A yanked version keeps auto-updating / the R2 bucket grows unbounded → the R2 sync runs without
--delete, so prior artifacts persist (delta updates need them) → prune stale objects fromlinkcode-releasesby hand; never add--delete(it breaks in-flight delta updates).