[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/levelcodeai/levelcode/develop/docs/RELEASING.md [Back]  [Original]

# Releasing LevelCode (build  strip  sign  notarize  publish)

How to turn the source into a **de-Microsoft'd, Developer-ID-signed, notarized, stapled** `.dmg` that
launches cleanly on any Mac with no Gatekeeper warning  in one command once the one-time setup is done.

> **Status (2026-07-01):** Apple Developer Program **enrolled**; the *Developer ID Application* cert is
> installed (`AJ27Y4Z2HS`) and the `levelcode-notary` credential is stored. `scripts/notarize.sh` +
> `scripts/levelcode.entitlements` + the `make-dmg.sh`/`build-macos.sh` wiring are all in place.
> Without `CODESIGN_IDENTITY` set, `make-dmg.sh` still produces an **ad-hoc** dmg (local/tester use;
> other Macs need right-clickOpen or `xattr -dr com.apple.quarantine`).

## 0. Where we're going

| | Ad-hoc (default) | Developer ID + notarized (release) |
| --- | --- | --- |
| Signature | `codesign --sign -` | `Developer ID Application:  (TEAMID)` |
| Hardened runtime | no | **yes** (`--options runtime`) |
| Entitlements | none | Electron/V8 JIT (`scripts/levelcode.entitlements`) |
| Notarized + stapled | no | **yes** (`notarytool` + `stapler`) |
| Proprietary MS/Copilot code | **stripped** either way | **stripped** (`scripts/strip-proprietary.mjs`) |
| Other Macs | Gatekeeper blocks | **launches cleanly** |

**Pipeline:** build  de-Microsoft strip  sign inside-out (hardened runtime + entitlements)  build dmg  notarize  staple  verify  publish.

## 1. One-time setup *(manual  only you can do this)*

**a. Developer ID Application certificate.**  Pick the exact type  outside-the-App-Store distribution
needs **"Developer ID Application"** (NOT "Apple Distribution", NOT "Developer ID Installer" which is for
`.pkg`). Easiest via Xcode  **Settings  Accounts  Manage Certificates  +  Developer ID Application**
(creates the private key in your login keychain). Confirm + note your Team ID:
```bash
security find-identity -v -p codesigning
#  "Developer ID Application: SERGII DEMIANCHUK (AJ27Y4Z2HS)"      the () is your TEAM ID
```
Back it up: Keychain Access  My Certificates  right-click  **Export** the `.p12` somewhere safe
(losing the private key means re-issuing).

**b. Notary credential** (stored once; no secret ever enters the repo):
```bash
# App-specific password: appleid.apple.com  Sign-In and Security  App-Specific Passwords  Generate
xcrun notarytool store-credentials "levelcode-notary" \
  --apple-id "you@example.com" --team-id "AJ27Y4Z2HS" --password "xxxx-xxxx-xxxx-xxxx"
```
(For CI, use an App Store Connect API key instead: `--key AuthKey_XXXX.p8 --key-id KEYID --issuer ISSUER-UUID`.)

**c. Export for your shell** (or your release env):
```bash
export CODESIGN_IDENTITY="Developer ID Application: SERGII DEMIANCHUK (AJ27Y4Z2HS)"
export NOTARY_PROFILE="levelcode-notary"
```

## 2. Cut a release

```bash
./scripts/bootstrap.sh        # first time / after an upstream bump (clone + brand + patch + npm ci)
./scripts/build-macos.sh      #  VSCode-darwin-/LevelCode.app; strips proprietary MS/Copilot code, then stamps the release version (3)
./scripts/make-dmg.sh         # de-Microsoft (defensive)  sign (Developer ID)  dmg  notarize  staple  verify
```
With `CODESIGN_IDENTITY` + `NOTARY_PROFILE` set, `make-dmg.sh` runs the whole signed+notarized pipeline;
without them it's ad-hoc (unnotarized). **The de-Microsoft strip runs on the built *app*, never the source
`vscode/` checkout**  so dev mode (`run-dev.sh`  typecheck) still sees the real packages.

## 3. Verify it will pass Gatekeeper on a clean Mac

```bash
xcrun stapler validate LevelCode-arm64.dmg
spctl -a -t open --context context:primary-signature -v LevelCode-arm64.dmg     #  "source=Notarized Developer ID" / "accepted"
```
Best real test: copy the dmg to a **different Mac** (or fresh user account), open it, drag to Applications,
double-click  it should launch with no warning.

Confirm the app carries no proprietary code:
```bash
find VSCode-darwin-arm64/LevelCode.app \( -path "*@github/copilot*" -o -path "*mxc-sdk*" \) \
  \( -name "*.node" -o -name "*.dylib" -o -name "mxc-exec-mac" \) -print   #  prints NOTHING
```

Confirm the app reports **its own** version rather than the Code-OSS base  this **fails silently**, so
check it every release:
```bash
python3 -c "import json;d=json.load(open('VSCode-darwin-arm64/LevelCode.app/Contents/Resources/app/product.json'));print(d.get('levelcodeVersion'),'|',d.get('version'),'|',d.get('commit','')[:7])"
#  0.8.1 | 1.126.0 | 07b7341
#   ^ the release version   ^ MUST stay 1.126.0 (extensions' engines.vscode is checked against it)
```
A **missing `levelcodeVersion`** means `build-macos.sh` could not resolve a tag (`git describe --tags`
failed) and fell back  the update tooltip and About will then report `1.126.0`, the upstream base, which
is exactly the confusion this stamp exists to prevent. The build log distinguishes the two:

| Log line | Meaning |
| --- | --- |
| `[build] Stamping the LevelCode release version (v0.8.1) ` | Good. |
| `[build] WARN: no reachable git tag  skipping the release-version stamp.` | **No stamp**  fix before publishing. |

An off-tag build is stamped `0.8.1-3-g1a2b3c4` on purpose (see `scripts/stamp-levelcode-version.mjs`): a
build that isn't a release must not impersonate one. A release build sits on the tag and gets a clean
`0.8.1`, so a suffix here means you are not building what you think you are.

Build **both architectures** on matching hardware (Apple silicon  `arm64`, Intel  `x64`) to ship both dmgs.

## 4. Publish

```bash
gh release create v0.1.0 --title "LevelCode v0.1.0" --notes "First public build. " \
  LevelCode-arm64.dmg LevelCode-x64.dmg \
  LevelCode-arm64.app.zip LevelCode-x64.app.zip
```
Upload **both kinds**: the `.dmg` is what humans install; the `.app.zip` is what the built-in updater
installs (Squirrel takes a zip, never a dmg). `make-dmg.sh` also writes a `.app.zip.sha256` next to each
zip  that one is **not** a release asset; it stays local. See 5.
Stable URL: `https://github.com/levelcodeai/levelcode/releases/latest/download/LevelCode-arm64.dmg`  link it
from **levelcode.ai/download**.

## 5. The update feed

>  **Auto-update is LIVE  publishing is deploying.** `LEVELCODE_UPDATE_FEED_SIGNED=1` is set in
> production, so a published release is no longer just *announced*: the built-in Squirrel updater
> **downloads and installs it on every existing install** at that install's next check. There is no
> staged rollout and no percentage gate. The rollback lever (below) stops it propagating further but
> **cannot un-update anyone who already took it**. Finish 3 on a real Mac *before* `--draft=false`.
>
> This changed with v0.8.0  the first release to carry signed `.app.zip` assets. Everything published
> before that stayed notify-only regardless of the flag, because the feed refuses to hand Squirrel a
> release that has no installable asset for its arch.

Publishing the GitHub release **is** the announcement  `Levelcode::EditorReleaseFeed` (thin.ly) reads
`releases/latest` and serves `/api/update/{target}/{quality}/{commit}`. There is no feed file to
hand-maintain per release.

Confirm the feed picked the release up (it caches for 5 minutes):
```bash
curl -s -H "User-Agent: LevelCode Updater" \
  https://levelcode.ai/api/update/darwin-arm64/stable/deadbeef | python3 -m json.tool
# url must end in LevelCode-arm64.app.zip (NOT a /releases/tag/ page), and productVersion must be the new one
```

Two assets, **not** interchangeable:

| Asset | Consumer |
| --- | --- |
| `LevelCode-.dmg` | Humans  fresh install, drag to Applications. |
| `LevelCode-.app.zip` | The built-in **Squirrel** updater  it installs from a zip, never a dmg. |

`make-dmg.sh` emits the `.app.zip` **only on the Developer-ID path**, because Squirrel refuses an update
whose signing identity doesn't match the running app  an ad-hoc build must never be served as an update.

**Only those two files get uploaded per arch.** `make-dmg.sh` also writes `LevelCode-.app.zip.sha256`,
but that is a **local verification convenience, not a release asset**: the feed reads `sha256hash` from
GitHub's own API-computed asset `digest` (`"sha256:"`), so nothing ever fetches a sidecar file.
Uploading one is harmless  `EditorReleaseFeed` matches assets by exact filename and ignores anything
else  just unnecessary. Use it to confirm the zip you published is the zip you built:
```bash
Z=LevelCode-arm64.app.zip
[ "$(shasum -a 256 "$Z" | cut -d' ' -f1)" = "$(cat "$Z.sha256")" ] && echo "$Z OK" || echo "$Z MISMATCH"
```

`LEVELCODE_UPDATE_FEED` (env JSON on the server) overrides the GitHub lookup and is the **rollback pin**:
point it at the previous commit to stop a bad release propagating. Note it can't un-update anyone who
already took the release.

Full contract, rollout order, and risks: **`docs/AUTO-UPDATE.md`**.

## 6. Troubleshooting *(the ones that actually bit us are marked )*

| Symptom | Cause / fix |
| --- | --- |
|  `security find-identity`  **0 valid identities**, cert shows **"not trusted"** in Keychain Access | The Apple **intermediate** is missing. Install the Developer ID **G2** CA: `curl -O https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer`  double-click. Do NOT hand-set trust on the leaf  leave "Use System Defaults". |
|  `notarytool store-credentials`  **HTTP 401 Invalid credentials** | You used your Apple ID password, or never generated an app-specific one. appleid.apple.com  App-Specific Passwords  **Generate**  paste that (with the dashes). |
|  Notarization stuck **In Progress** for hours | Apple-side queue/incident (check [system status](https://developer.apple.com/system-status/)). The submission is server-side  `Ctrl-C` the `--wait`, then `xcrun notarytool info  --keychain-profile levelcode-notary` later and `stapler staple` once `Accepted`. Don't resubmit (duplicates just queue behind it). |
| `notarytool`  **Invalid** | `xcrun notarytool log  --keychain-profile levelcode-notary` names the exact file/entitlement. Usually: sign with the Developer ID identity + `--options runtime`. |
| App crashes on launch (SIGKILL / code 137) after signing | Missing JIT entitlements on the main app or a helper  check `scripts/levelcode.entitlements` is applied to the helper `.app`s too (notarize.sh does this). |
| "signature does not include a secure timestamp" | Add `--timestamp` (needs network)  notarize.sh always does. |
| Gatekeeper still warns after notarizing | Forgot to **staple**, or stapled the app but not the dmg. |
| Chat won't open / shortcut dead in a build | `levelcode.ai.focus` is `Ctrl+Cmd+I` (moved off the `Cmd+Alt+I` DevTools collision); the chat also auto-reveals until the first message is sent. |

## 7. CI build  hybrid model (`.github/workflows/release.yml`)

**CI builds both arches; you sign locally.** Your Developer ID cert never touches GitHub. CI exists to
solve the awkward part  building the **Intel (x64)** dmg, which you can't easily do on an Apple-silicon
Mac  by building each arch on its **native** runner (`macos-14` = arm64, `macos-15-intel` = x64). It
produces **unsigned** `.app` bundles; you do the fast, sensitive sign + notarize + staple on your machine.

> **Intel runner note.** `macos-13` (the old x64 runner) was retired 2025-12-04, so we build x64 on
> `macos-15-intel`  GitHub's last native x86_64 image. It's a premium/large runner (bills ~2 minutes)
> and Intel support on Actions ends **Fall 2027**. After that, the x64 job must cross-compile on an
> arm64 runner (set `VSCODE_ARCH=x64`/`npm_config_arch=x64`, rebuild native modules for x64) rather than
> build natively  a `scripts/build-macos.sh` + `scripts/bootstrap.sh` change, not just a runner swap.

The whole release becomes:

```sh
# 1. Kick off CI (builds both arches, ~3060 min/arch; free on public repos, 10 minutes while private)
git tag v0.1.0 && git push --tags
#     workflow builds  creates a DRAFT release with UNSIGNED-LevelCode-.app.zip attached

# 2. Sign + notarize LOCALLY (needs the one-time setup from 1)
gh release download v0.1.0 --pattern 'UNSIGNED-*.app.zip'
for A in arm64 x64; do
  rm -rf "VSCode-darwin-$A" && ditto -x -k "UNSIGNED-LevelCode-$A.app.zip" "VSCode-darwin-$A"
  CODESIGN_IDENTITY="Developer ID Application: SERGII DEMIANCHUK (AJ27Y4Z2HS)" \
    NOTARY_PROFILE=levelcode-notary ./scripts/make-dmg.sh "$A"     #  LevelCode-$A.dmg + LevelCode-$A.app.zip
done

# 3. Verify (3), then attach the dmgs AND the update zips, drop the unsigned zips, and publish.
#    The .app.zip.sha256 files stay local on purpose  the feed uses GitHub's own asset digest (5).
gh release upload v0.1.0 LevelCode-arm64.dmg LevelCode-x64.dmg \
                         LevelCode-arm64.app.zip LevelCode-x64.app.zip
# `gh release delete-asset` takes ONE asset per call  drop each unsigned zip separately (`-y` skips the prompt).
gh release delete-asset v0.1.0 UNSIGNED-LevelCode-arm64.app.zip -y
gh release delete-asset v0.1.0 UNSIGNED-LevelCode-x64.app.zip -y
gh release edit v0.1.0 --draft=false --notes-file RELEASE-NOTES.md
```

Notes:
- **No secrets required**  the workflow is credential-free by design (that's the whole point of hybrid).
- The dmg names (`LevelCode-arm64.dmg` / `LevelCode-x64.dmg`) are exactly what the download funnel at
  `levelcode.ai/download/` expects  don't rename them.
- `releases/latest` only resolves once this is a **published, non-prerelease** release with both dmgs.
- **Fully-automated alternative** (signing in CI) if you ever want zero local steps: base64 the `.p12`
  Developer ID export + store it and an App Store Connect API key as secrets behind a *protected
  Environment*, import into a temp keychain at job start, and run `make-dmg.sh` with `CODESIGN_IDENTITY`
  set. Standard (VSCodium does this) but puts your signing identity in the cloud  the hybrid flow above
  deliberately doesn't.

Web Proxy Viewer  |  New URL  |  Original Page