Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
name: Publish to npm

# Manual only. Publishing is done deliberately with a short-lived npm token, so
# this does NOT auto-fire on every GitHub release (releases are also cut for the
# desktop DMG, which must not trigger a CLI publish). To publish: bump the
# version, then run this workflow from the Actions tab. Requires an npm
# automation token with publish access to `codbash-app` in the NPM_TOKEN secret.
on:
release:
types: [published]
workflow_dispatch:

jobs:
publish:
Expand All @@ -20,6 +24,14 @@ jobs:
run: |
node bin/cli.js version
node -c bin/cli.js
- name: Fail early if NPM_TOKEN is missing
run: |
if [ -z "${NODE_AUTH_TOKEN}" ]; then
echo "::error::NPM_TOKEN secret is not set — add an npm automation token with publish access to codbash-app."
exit 1
fi
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish
run: npm publish --provenance --access public
env:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ npm i -g codbash-app
codbash run
```

Or grab the **macOS desktop app** from [Releases](https://github.com/vakovalskii/codbash/releases/latest) (Apple Silicon).
Or grab the **macOS desktop app** from [Releases](https://github.com/vakovalskii/codbash/releases/latest) — signed & notarized, Apple Silicon (`-arm64.dmg`) and Intel (`.dmg`). It opens with no Gatekeeper warning.

## Supported Agents

Expand Down
140 changes: 91 additions & 49 deletions desktop/RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# Releasing the codbash macOS app (sign · notarize · publish)

The build is **signed** by electron-builder, **notarized** by the `afterSign`
hook (`scripts/notarize.js`), and **published** to GitHub Releases so the
in-app auto-updater (`electron-updater`) can pick it up. Every step degrades
gracefully: with no credentials you still get a working (unsigned) DMG for
local testing.
The build is **signed** with a Developer ID Application cert, **notarized** by
the `afterSign` hook (`scripts/notarize.js`) + a follow-up pass for the DMG
containers, and **published** to GitHub Releases where the in-app notify-updater
picks it up. Releases since **v7.14.4** are signed + notarized (arm64 + x64) and
open with no Gatekeeper warning. With no credentials the build still succeeds and
produces an unsigned DMG for local smoke-testing (the notarize hook no-ops).

## 0. One-time setup

Expand All @@ -21,83 +22,124 @@ local testing.
- Go to https://appleid.apple.com → **Sign-In & Security → App-Specific Passwords**.
- Generate one (e.g. label it `codbash-notarize`). Copy the `xxxx-xxxx-xxxx-xxxx` value.

## 1. Environment variables
## 1. Notarization credentials

**Preferred — a keychain profile** (no secret in env/shell history/CI logs):

```bash
# Signing (only if the Developer ID cert is NOT already in your login keychain):
export CSC_LINK="/absolute/path/DeveloperIDApplication.p12"
export CSC_KEY_PASSWORD="<p12 export password>"
xcrun notarytool store-credentials "codbash-notary" \
--apple-id "you@example.com" \
--team-id "A933C2TJXU"
# prompts (hidden) for the app-specific password; stored in the login keychain.
```

The build reads it via `APPLE_KEYCHAIN_PROFILE` (see `scripts/notarize.js`).

# Notarization:
**Alternative — env vars** (e.g. CI):

```bash
export APPLE_ID="you@example.com"
export APPLE_APP_SPECIFIC_PASSWORD="xxxx-xxxx-xxxx-xxxx"
export APPLE_TEAM_ID="ABCDE12345"

# Publishing to GitHub Releases (needs repo write):
export GH_TOKEN="<github personal access token, repo scope>"
export APPLE_TEAM_ID="A933C2TJXU"
# and, only if the Developer ID cert is NOT already in the login keychain:
export CSC_LINK="/absolute/path/DeveloperIDApplication.p12"
export CSC_KEY_PASSWORD="<p12 export password>"
```

## 2. Build
## 2. Build (signed + notarized, both arches)

```bash
cd desktop
npm install # once, or after dependency changes

# Local unsigned DMG (no credentials needed) — for smoke-testing the package:
npm run dist:mac # → dist/codbash-<version>.dmg
# ⚠️ Build over a VPN with proxy env vars UNSET (see the timestamp note below).
env -u HTTPS_PROXY -u HTTP_PROXY -u ALL_PROXY -u https_proxy -u http_proxy -u all_proxy \
APPLE_KEYCHAIN_PROFILE=codbash-notary CSC_IDENTITY_AUTO_DISCOVERY=true \
./node_modules/.bin/electron-builder --mac --arm64 --x64
```

- Pass `--arm64 --x64` explicitly. Passing a target on the CLI (`… dmg`)
**overrides** the `arch` array in `package.json`, so `npm run dist:mac`
builds only the host arch.
- The signing identity is pinned in `package.json` → `build.mac.identity` as
`"Valeriy Kovalsky (A933C2TJXU)"` — **without** the `Developer ID Application:`
prefix (electron-builder rejects the prefix).
- The `afterSign` hook (`scripts/notarize.js`) notarizes and staples **each
`.app`**. The DMG *containers* are notarized + stapled separately (step 2b),
since electron-builder builds the DMG after the hook runs.

# Signed + notarized + published to GitHub Releases:
npm run release:mac # electron-builder --mac dmg --publish always
**2b. Notarize + staple the DMG containers, then regenerate the update feed**
(the staple mutates the DMG, so blockmaps + `latest-mac.yml` must be recomputed):

```bash
for dmg in dist/codbash-<ver>-arm64.dmg dist/codbash-<ver>.dmg; do
xcrun notarytool submit "$dmg" --keychain-profile codbash-notary --wait
xcrun stapler staple "$dmg"
./node_modules/app-builder-bin/mac/app-builder_arm64 blockmap \
--input "$dmg" --output "$dmg.blockmap" # prints the {size,sha512} for latest-mac.yml
done
# then rewrite dist/latest-mac.yml with the new size+sha512 for both DMGs.
```

`release:mac` will:
1. compile the app and **sign** it with your Developer ID cert,
2. run the `afterSign` hook → **notarize** with `notarytool` (a few minutes),
3. **staple** the ticket to the app/DMG,
4. **upload** the DMG + `latest-mac.yml` to a GitHub Release for the current tag.
Publish:

> Keep `version` in `desktop/package.json` in step with the codbash release
> (and create a matching git tag, e.g. `v7.14.0`) so the release/auto-update
> line up.
```bash
gh release create v<ver> --title "codbash <ver> (macOS desktop)" --target main \
dist/codbash-<ver>-arm64.dmg dist/codbash-<ver>-arm64.dmg.blockmap \
dist/codbash-<ver>.dmg dist/codbash-<ver>.dmg.blockmap \
dist/latest-mac.yml
```

> Keep `version` in both `package.json` and `desktop/package.json` in step, and
> tag `v<ver>` so the release lines up.

## 3. Verify

```bash
# Notarization ticket is stapled:
# App: notarized + stapled + Gatekeeper-accepted
xcrun stapler validate "dist/mac-arm64/codbash.app"
# Gatekeeper accepts it:
spctl -a -vvv -t install "dist/mac-arm64/codbash.app"
spctl -a -vvv -t exec "dist/mac-arm64/codbash.app" # → accepted, source=Notarized Developer ID
# DMG: notarized ticket stapled (spctl on the raw DMG is a false negative — the
# container isn't code-signed; Gatekeeper checks the notarization ticket)
xcrun stapler validate "dist/codbash-<ver>-arm64.dmg"
# Real-world check: mount a QUARANTINED copy and assess the app inside
cp dist/codbash-<ver>-arm64.dmg /tmp/q.dmg && xattr -w com.apple.quarantine "0083;0;Safari;" /tmp/q.dmg
hdiutil attach /tmp/q.dmg -nobrowse; spctl -a -vvv -t exec "/Volumes/codbash <ver>/codbash.app"
```

## 4. Updates

Two models, depending on whether the build is signed:

- **Now (unsigned): notify-only.** On launch and every 6h the app queries the
- **Current model — notify-only.** On launch and every 6h the app queries the
GitHub `releases/latest` API and, if a newer version exists, offers to open
the download page (`main.js` → `checkForUpdates`). This works without signing.
Just publish each release (`gh release create v<x> dist/*.dmg …` or
`--publish always`) and users get notified.
- **Later (signed): silent auto-update.** Once a Developer ID cert is in place,
swap `checkForUpdates` for `electron-updater`: `npm i electron-updater`, call
`autoUpdater.checkForUpdatesAndNotify()`, and publish with `--publish always`
so `latest-mac.yml` + the signed DMG land in the Release. macOS silent
in-place update **requires** the signature, which is why it's gated on the cert.

The first desktop release (v7.13.0, unsigned, arm64) is published at
`https://github.com/vakovalskii/codbash/releases/tag/v7.13.0`.
the download page (`main.js` → `checkForUpdates`). Robust and dependency-free;
just publish each release.
- **Silent auto-update (now possible — builds are signed).** Swap
`checkForUpdates` for `electron-updater`: `npm i electron-updater`, call
`autoUpdater.checkForUpdatesAndNotify()`, and ensure `latest-mac.yml` +
signed/stapled DMGs are in the Release (they are — steps 2b/publish above).
macOS silent in-place update requires the signature, which is now in place.

## CI note

`.github/` is git-ignored in this repo, so there is no tracked GitHub Actions
workflow. To automate, add a macOS runner job that sets the env vars above from
repository secrets and runs `npm --prefix desktop ci && npm --prefix desktop run release:mac`
on tag push. Notarization requires a macOS runner.
Tracked workflows live in `.github/workflows/`: `ci.yml` (tests on push/PR) and
`publish.yml` (npm publish — **manual** `workflow_dispatch`, not on release, so a
desktop DMG release never triggers a CLI publish). There is **no** CI job for the
DMG: notarization needs a macOS runner plus the signing cert + notary creds, and
the signed build must run over a VPN with the proxy unset (see step 2 / the
timestamp note), so the DMG is built and published locally with the commands above.

## Troubleshooting

- **`The timestamp service is not available`** (repeated `signing … retrying`,
build aborts) → `codesign --timestamp` hits `timestamp.apple.com` via the
system network stack and **ignores** the `HTTPS_PROXY` env var. If the network
needs a proxy for outbound HTTPS, every nested file fails. **Fix: turn on a VPN
and build with the proxy env vars unset** (`env -u HTTPS_PROXY …`, as in step 2).
Apple's secure timestamp is mandatory for notarization — you can't swap the TSA.
Pre-flight: sign a throwaway file 5× and expect 5/5 OK before the full build.
- **"app is damaged / unidentified developer"** → not notarized or not stapled;
confirm the three `APPLE_*` vars were set during the build.
confirm the notary credentials (keychain profile or `APPLE_*` vars) were
available during the build, and that step 2b stapled the DMG.
- **node-pty / terminal fails in the packaged app** → ensure
`../node_modules/@lydell` is present at build time (it is copied via
`extraResources`); run `npm install` at the repo root first.
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@

<span id="sidebarStatus" role="status" aria-live="polite" class="sr-only"></span>
<div class="sidebar-author">
Made by <a href="https://t.me/neuraldeep" target="_blank" rel="noopener noreferrer" aria-label="Valerii Kovalskii on Telegram (opens in new tab)">Valerii Kovalskii</a>
Made by <a href="https://t.me/neuraldeep" target="_blank" rel="noopener noreferrer" aria-label="Neuraldeep community on Telegram (opens in new tab)">Neuraldeep community</a>
</div>
</div>

Expand Down
Loading