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: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
name: CI

permissions:
contents: read

on:
push:
branches: [main]
Expand Down Expand Up @@ -35,8 +38,21 @@ jobs:

- run: npm ci
- run: npm test
- run: npm run test:release
- run: npx tsc --noEmit
- run: cargo fmt --check
- run: cargo clippy --workspace --all-targets -- -D warnings
- run: cargo check
- run: cargo test

fork-release:
needs: check
if: >-
github.repository == 'EricRasputin/monocode-eric' &&
github.ref == 'refs/heads/main' &&
(github.event_name == 'push' || github.event_name == 'workflow_dispatch')
permissions:
contents: write
uses: ./.github/workflows/fork-release.yml
secrets:
updater-private-key: ${{ secrets.FORK_UPDATER_PRIVATE_KEY }}
84 changes: 84 additions & 0 deletions .github/workflows/fork-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
name: Fork release

# CI calls this only after all three platform checks pass on main.
on:
workflow_call:
secrets:
updater-private-key:
required: true

concurrency:
group: fork-release
cancel-in-progress: false

permissions:
contents: read

env:
FORK_VERSION: 0.2.${{ github.run_number }}

jobs:
build:
strategy:
fail-fast: false
matrix:
target: [aarch64-apple-darwin, x86_64-apple-darwin]
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
with:
key: fork-${{ matrix.target }}
- run: npm ci
- name: Build and sign fork update
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.updater-private-key }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ""
run: |
test -n "$TAURI_SIGNING_PRIVATE_KEY" || { echo 'FORK_UPDATER_PRIVATE_KEY is missing'; exit 1; }
npm run build:fork -- --ci --target '${{ matrix.target }}' --config "{\"version\":\"$FORK_VERSION\"}"
- name: Verify app and stage update
env:
BUILD_TARGET: ${{ matrix.target }}
run: |
APP="target/$BUILD_TARGET/release/bundle/macos/MonoCode Fork.app"
test "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$APP/Contents/Info.plist")" = 'com.monocode.fork.worktrees'
test "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$APP/Contents/Info.plist")" = "$FORK_VERSION"
EXPECTED_ARCH=x86_64
if [[ "$BUILD_TARGET" == aarch64-* ]]; then EXPECTED_ARCH=arm64; fi
test "$(lipo -archs "$APP/Contents/MacOS/monocode")" = "$EXPECTED_ARCH"
codesign --verify --deep --strict "$APP"
node scripts/fork-release.mjs stage "$FORK_VERSION" "$BUILD_TARGET"
- uses: actions/upload-artifact@v4
with:
name: fork-${{ matrix.target }}
path: release-artifacts/
if-no-files-found: error
retention-days: 7

publish:
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- uses: actions/download-artifact@v4
with:
pattern: fork-*
path: release-artifacts
merge-multiple: true
- name: Publish complete update
env:
GH_TOKEN: ${{ github.token }}
run: node scripts/fork-release.mjs publish "$FORK_VERSION"
3 changes: 3 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ permissions:

jobs:
release:
if: github.repository == 'hardbeat920/monocode'
needs: [linux, windows]
runs-on: macos-latest
steps:
Expand Down Expand Up @@ -305,6 +306,7 @@ jobs:
"$DEB" "$APPIMAGE" "$NSIS" "$NSIS_SIG"

linux:
if: github.repository == 'hardbeat920/monocode'
name: Linux packages
runs-on: ubuntu-22.04
steps:
Expand Down Expand Up @@ -346,6 +348,7 @@ jobs:
if-no-files-found: error

windows:
if: github.repository == 'hardbeat920/monocode'
name: Windows packages
runs-on: windows-latest
steps:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ dist/
dist-ssr/
build/
target/
release-artifacts/
gen/schemas/
coverage/
.vite/
Expand Down
27 changes: 22 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ Works with your subscriptions on Claude Code, Codex, Cursor, Grok Build, OpenCod

## Install

**This fork (macOS):** download **MonoCode Fork** for Apple Silicon (`darwin-aarch64`)
or Intel (`darwin-x86_64`) from [fork releases](https://github.com/EricRasputin/monocode-eric/releases/latest)
and install it in `/Applications`. Then use **Check for Updates…** in the app menu
or Settings. The app asks before downloading, installing, and restarting.
Builds from before `0.2.0` need this one-time installation to enable updates.
See [fork updates](docs/fork-updates.md) for release and signing details.

The download links below are for upstream MonoCode, which is a separate app.

> Install and log in to at least one provider first:
>
> - [Claude Code](https://claude.com/product/claude-code) - `claude auth login`
Expand Down Expand Up @@ -48,13 +57,21 @@ Small, focused pull requests are welcome. Anything large is worth an issue first

## Build from source

For the macOS fork, run `npm ci` followed by `npm run build:fork`. The app and
DMG are written under `target/release/bundle/`. This uses the existing
**MonoCode Fork** identity (`com.monocode.fork.worktrees`) and its session data,
with upstream automatic updates disabled.
Fork releases are built and published automatically after changes to `main`
pass CI on macOS, Linux, and Windows. Users update from inside the app; no local
build is needed. Fork versions use `0.2.<CI run number>` independently of upstream.

For a local macOS build, run `npm ci` followed by `npm run build:fork` with
`TAURI_SIGNING_PRIVATE_KEY` set to the fork's signing key path and
`TAURI_SIGNING_PRIVATE_KEY_PASSWORD` set to an empty string. The app, DMG, and
signed update archive are written under `target/release/bundle/`. This preserves
the **MonoCode Fork** identity (`com.monocode.fork.worktrees`) and its session data,
and checks only this fork's update feed. Without the signing key, use
`npm run build:fork -- --config '{"bundle":{"createUpdaterArtifacts":false}}'`
to create a local app and DMG without a publishable update archive.

When building from a session inside MonoCode Fork, leave the running app in
place. Quit it before replacing `~/Applications/MonoCode Fork.app` with the
place. Quit it before replacing `/Applications/MonoCode Fork.app` with the
new bundle, then reopen it. Build in this checkout's own `target` directory so
other running development builds are unaffected.

Expand Down
75 changes: 75 additions & 0 deletions docs/fork-updates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# MonoCode Fork updates

Install the appropriate macOS DMG from
[fork releases](https://github.com/EricRasputin/monocode-eric/releases/latest) in
`/Applications`. The app keeps the existing `com.monocode.fork.worktrees` identity
and saved conversations. Do not install upstream MonoCode as a fork update.

Choose **Check for Updates…** from the app menu or Settings. If a newer release
exists, the app shows its version and release notes and asks whether to install
and restart. Declining leaves the app running. Accepting downloads the package,
verifies its signature, installs it, and restarts. Startup checks also show an
available update in the sidebar. Failed checks or downloads do not count as a
successful update.

The old `0.1.x` fork builds have no update endpoint or public key, so they need
one initial replacement with a `0.2.x` build. Quit the old app first. If both
`~/Applications/MonoCode Fork.app` and `/Applications/MonoCode Fork.app` exist,
launch the new copy from `/Applications` to avoid opening the old build.

## Releases

The `CI` workflow calls `fork-release.yml` after **all** macOS, Linux, and Windows
checks pass for a push to `main`. PR checks never publish. A manual run of `CI`
on `main` can also publish a release. No version edit, tag push, local build,
or agent request is needed after merging a change.

- The app's fork version is `0.2.<CI run number>`. Upstream's version stays in
`package.json`, Cargo, and the base Tauri config; the fork config overrides it.
- Release tags use `fork-v0.2.<CI run number>`, so upstream `v*` tags stay separate.
- Both `darwin-aarch64` and `darwin-x86_64` packages must finish successfully.
- The workflow checks bundle identity, version, architecture, and code signature.
It stages a DMG, signed `.app.tar.gz`, and signature for each architecture.
- A single publisher validates artifact hashes and commit/version consistency,
then uploads all packages, `SHA256SUMS`, and `latest.json` to a draft release.
Only a complete upload becomes public and the latest release.
- The app reads `releases/latest/download/latest.json`; package URLs point to
immutable version tags. The embedded public key verifies update signatures.
- Releases are serialized. Reruns and delayed older builds never replace a
published newer release. A failed draft can be retried with the same CI run;
a fresh manual CI run gets a new version.

The inherited upstream release workflow is restricted to `hardbeat920/monocode`.
It cannot publish an upstream-branded release over this fork's update feed.
Fork distribution currently targets macOS; Linux and Windows remain CI targets.

## Signing

`src-tauri/tauri.fork.conf.json` contains the public updater key. The matching
private key is the GitHub Actions repository secret `FORK_UPDATER_PRIVATE_KEY`,
passed only to the packaging step. The key has no password, so the workflow sets
`TAURI_SIGNING_PRIVATE_KEY_PASSWORD` to an empty string. Never commit the private
key or print it in logs. The initial local backup is stored outside the repo at
`~/.config/monocode-eric/updater/private.key` with owner-only permissions; keep a
durable backup because losing it prevents updates to already-installed apps.

Tauri update signing authenticates downloaded update packages. Apple signing and
notarization are separate: these builds currently use the existing ad-hoc macOS
signature. A downloaded installer may require approval in macOS Privacy &
Security on first installation. Apple Developer ID distribution and notarization
can be configured later without replacing the updater key or application identity.

References: [Tauri updater](https://v2.tauri.app/plugin/updater/) and
[macOS signing](https://v2.tauri.app/distribute/sign/macos/).

## Validation and recovery

`npm run check:web` includes release metadata tests and the updater interaction
tests. CI additionally compiles and tests Rust on all three platforms. To recover
from a bad release, revert the source change and let CI publish a higher fork
version. Do not move an existing release tag or replace a published archive.

Local builds default to `0.2.0`; release builds get their version through the
Tauri config override in CI. Local builds are useful for development but are not
published releases. A private-key-free local build can disable generation of
updater artifacts using the command in the README.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@
"build:fork": "tauri build --bundles app,dmg --config src-tauri/tauri.fork.conf.json",
"preview": "vite preview",
"test": "vitest run",
"test:release": "node --test scripts/fork-release.test.mjs",
"test:watch": "vitest",
"check": "npm run check:web && npm run check:rust",
"check:web": "vitest run && tsc --noEmit",
"check:web": "vitest run && npm run test:release && tsc --noEmit",
"check:rust": "cargo fmt --check && cargo clippy --workspace --all-targets -- -D warnings && cargo test",
"set-version": "node scripts/bump-version.mjs",
"tauri": "tauri",
Expand Down
Loading
Loading