diff --git a/.github/workflows/nodejs.yml b/.github/workflows/ci-desktop.yml similarity index 80% rename from .github/workflows/nodejs.yml rename to .github/workflows/ci-desktop.yml index 5f1ee6e98..918e0af0a 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/ci-desktop.yml @@ -1,4 +1,4 @@ -name: Desktop App +name: Desktop App CI on: push: @@ -25,10 +25,6 @@ jobs: run: npm ci working-directory: ui/desktop - - name: Package application - run: npm run package - working-directory: ui/desktop - - name: Run E2E tests run: npm run test-e2e working-directory: ui/desktop diff --git a/.github/workflows/desktop-app-release.yaml b/.github/workflows/desktop-app-release.yaml new file mode 100644 index 000000000..3560a9d51 --- /dev/null +++ b/.github/workflows/desktop-app-release.yaml @@ -0,0 +1,111 @@ +name: Desktop App Release + +on: + push: + branches: + - v1.0 + +jobs: + build-and-bundle: + runs-on: macos-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Set up Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + override: true + + - name: Cache Cargo registry + uses: actions/cache@v3 + with: + path: ~/.cargo/registry + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-registry- + + - name: Cache Cargo index + uses: actions/cache@v3 + with: + path: ~/.cargo/index + key: ${{ runner.os }}-cargo-index + restore-keys: | + ${{ runner.os }}-cargo-index + + - name: Cache Cargo build + uses: actions/cache@v3 + with: + path: target + key: ${{ runner.os }}-cargo-build-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-build- + + # Install UV and download tokenizer files + - name: Install UV + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + + - name: Run download_tokenizer_files.py + run: uv run download_tokenizer_files.py + + # Build Rust Binary + - name: Build Release Binary + run: cargo build --release + + - name: copy binary + run: cp target/release/goosed ui/desktop/src/bin/goosed + + + + + # Desktop App Steps + - name: Add MacOS certs for signing and notarization + run: ./add-macos-cert.sh + working-directory: ui/desktop + env: + CERTIFICATE_OSX_APPLICATION: ${{ secrets.CERTIFICATE_OSX_APPLICATION }} + CERTIFICATE_PASSWORD: ${{ secrets.CERTIFICATE_PASSWORD }} + + - name: Set up Node.js + uses: actions/setup-node@v2 + with: + node-version: 'lts/*' + + - name: Install dependencies + run: npm ci + working-directory: ui/desktop + + - name: Make default Goose App + run: npm run bundle:default + working-directory: ui/desktop + env: + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + + - name: Upload default + uses: actions/upload-artifact@v3 + with: + name: Goose.zip + path: ui/desktop/out/Goose-darwin-arm64/Goose.zip + + - name: Make preconfigured Goose App + run: npm run bundle:preconfigured + working-directory: ui/desktop + env: + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + GOOSE_BUNDLE_HOST: ${{ secrets.GOOSE_BUNDLE_HOST }} + GOOSE_BUNDLE_MODEL: ${{ secrets.GOOSE_BUNDLE_MODEL }} + GOOSE_BUNDLE_TYPE: ${{ secrets.GOOSE_BUNDLE_TYPE }} + + - name: Upload preconfigured + uses: actions/upload-artifact@v3 + with: + name: Goose-preconfigured.app + path: ui/desktop/out/Goose-darwin-arm64/Goose.zip \ No newline at end of file diff --git a/ui/desktop/README.md b/ui/desktop/README.md index cd4314b40..d0f15ae89 100644 --- a/ui/desktop/README.md +++ b/ui/desktop/README.md @@ -1,23 +1,36 @@ # Goose App -Mac app for Goose. +Mac (and maybe windows?) app for Goose. ``` git clone git@github.com:block/goose.git cd goose/ui/desktop npm install -export GOOSE_PROVIDER__API_KEY=... # OpenAI API Key npm start ``` -This will run `goosed` from src/bin (currently just copied into place from goose core) listening automatically. +# Building notes -Testing the rust server from source: +This is an electron forge app, using vite and react.js. `gooosed` runs as multi process binaries on each window/tab similar to chrome. -See `test.sh` for curl on how to use goose daemon - which is from rust version: +see `package.json`: -* rust streaming server version of goose at time of writing: https://github.com/block/goose/pull/237 +`npm run bundle:default` will give you a Goose.app/zip which is signed/notarized but only if you setup the env vars as per `forge.config.ts` (you can empty out the section on osxSign if you don't want to sign it) - this will have all defaults. -`cargo run -p goose-server` +`npm run bundle:preconfigured` will make a Goose.app/zip signed and notarized, but use the following: -`./test.sh` (in another shell) \ No newline at end of file +```python + f" process.env.GOOSE_PROVIDER__TYPE = '{os.getenv("GOOSE_BUNDLE_TYPE")}';", + f" process.env.GOOSE_PROVIDER__HOST = '{os.getenv("GOOSE_BUNDLE_HOST")}';", + f" process.env.GOOSE_PROVIDER__MODEL = '{os.getenv("GOOSE_BUNDLE_MODEL")}';" +``` + +This allows you to set for example GOOSE_PROVIDER__TYPE to be "databricks" by default if you want (so when people start Goose.app - they will get that out of the box). There is no way to set an api key in that bundling as that would be a terrible idea, so only use providers that can do oauth (like databricks can), otherwise stick to default goose. + + +# Runninng with goosed server from source + +Set `VITE_START_EMBEDDED_SERVER=yes` to no in `.env. +Run `cargo run -p goose-server` from parent dir. +`npm run start` will then run against this. +You can try server directly with `./test.sh` diff --git a/ui/desktop/add-macos-cert.sh b/ui/desktop/add-macos-cert.sh new file mode 100755 index 000000000..6da80041b --- /dev/null +++ b/ui/desktop/add-macos-cert.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env sh + +KEY_CHAIN=build.keychain +CERTIFICATE_P12=certificate.p12 + +# Recreate the certificate from the secure environment variable +echo $CERTIFICATE_OSX_APPLICATION | base64 --decode > $CERTIFICATE_P12 + +#create a keychain +security create-keychain -p actions $KEY_CHAIN + +# Make the keychain the default so identities are found +security default-keychain -s $KEY_CHAIN + +# Unlock the keychain +security unlock-keychain -p actions $KEY_CHAIN + +security import $CERTIFICATE_P12 -k $KEY_CHAIN -P $CERTIFICATE_PASSWORD -T /usr/bin/codesign; + +security set-key-partition-list -S apple-tool:,apple: -s -k actions $KEY_CHAIN + +# remove certs +rm -fr *.p12 \ No newline at end of file diff --git a/ui/desktop/bundle.py b/ui/desktop/bundle.py index 6fe4a65f2..61d9f5942 100755 --- a/ui/desktop/bundle.py +++ b/ui/desktop/bundle.py @@ -2,8 +2,11 @@ import re from pathlib import Path from typing import Dict, Union +import os -def replace_env_macro(provider_type: str, host: str, model: str) -> bool: +def replace_env_macro() -> bool: + + """ Replace content between environment macro markers with formatted environment variables. @@ -24,9 +27,9 @@ def replace_env_macro(provider_type: str, host: str, model: str) -> bool: # Format the environment variables formatted_vars = [ - f" process.env.GOOSE_PROVIDER__TYPE = '{provider_type}';", - f" process.env.GOOSE_PROVIDER__HOST = '{host}';", - f" process.env.GOOSE_PROVIDER__MODEL = '{model}';" + f" process.env.GOOSE_PROVIDER__TYPE = '{os.getenv("GOOSE_BUNDLE_TYPE")}';", + f" process.env.GOOSE_PROVIDER__HOST = '{os.getenv("GOOSE_BUNDLE_HOST")}';", + f" process.env.GOOSE_PROVIDER__MODEL = '{os.getenv("GOOSE_BUNDLE_MODEL")}';" ] replacement_content = "\n".join(formatted_vars) @@ -59,20 +62,7 @@ def replace_env_macro(provider_type: str, host: str, model: str) -> bool: # Example usage if __name__ == '__main__': - import argparse - - parser = argparse.ArgumentParser(description='Update environment variables in main.ts') - parser.add_argument('--type', required=True, help='Provider type (e.g., databricks)') - parser.add_argument('--host', required=True, help='Host URL') - parser.add_argument('--model', required=True, help='Model name') - - args = parser.parse_args() - - success = replace_env_macro( - provider_type=args.type, - host=args.host, - model=args.model - ) + success = replace_env_macro() if not success: print("Failed to update environment variables") diff --git a/ui/desktop/forge.config.ts b/ui/desktop/forge.config.ts index fe2a4991d..34f66b1c7 100644 --- a/ui/desktop/forge.config.ts +++ b/ui/desktop/forge.config.ts @@ -10,6 +10,13 @@ module.exports = { entitlements: 'entitlements.plist', 'entitlements-inherit': 'entitlements.plist', 'gatekeeper-assess': false, + hardenedRuntime: true, + identity: 'Developer ID Application: Michael Neale (W2L75AE9HQ)', + }, + osxNotarize: { + appleId: process.env['APPLE_ID'], + appleIdPassword: process.env['APPLE_ID_PASSWORD'], + teamId: process.env['APPLE_TEAM_ID'] }, }, rebuildConfig: {}, diff --git a/ui/desktop/package.json b/ui/desktop/package.json index d6a239f8c..833759e7a 100644 --- a/ui/desktop/package.json +++ b/ui/desktop/package.json @@ -8,11 +8,10 @@ "start": "electron-forge start", "package": "electron-forge package", "make": "electron-forge make", - "publish": "electron-forge publish", + "bundle:preconfigured": "./bundle.py && npm run make && echo 'resetting main' && git checkout src/main.ts && cd out/Goose-darwin-arm64 && ditto -c -k --sequesterRsrc --keepParent Goose.app Goose.zip", + "bundle:default": "npm run make && cd out/Goose-darwin-arm64 && ditto -c -k --sequesterRsrc --keepParent Goose.app Goose.zip", "debug": "echo 'run --remote-debugging-port=8315' && lldb out/Goose-darwin-arm64/Goose.app", - "test-e2e": "electron-forge start > /tmp/out.txt & ELECTRON_PID=$! && sleep 12 && if grep -q 'renderer: ChatWindow loaded' /tmp/out.txt; then echo 'process is running'; pkill -f electron; else echo 'not starting correctly'; cat /tmp/out.txt; pkill -f electron; exit 1; fi", - "sign-macos": "cd ./out/Goose-darwin-arm64 && codesign --deep --force --verify --sign \"Developer ID Application: Michael Neale (W2L75AE9HQ)\" Goose.app && ditto -c -k --sequesterRsrc --keepParent Goose.app Goose.zip", - "sign-verify": " cd ./out/Goose-darwin-arm64 && codesign --verify --deep --strict Goose.app && codesign -d --entitlements :- Goose.app" + "test-e2e": "electron-forge start > /tmp/out.txt & ELECTRON_PID=$! && sleep 12 && if grep -q 'renderer: ChatWindow loaded' /tmp/out.txt; then echo 'process is running'; pkill -f electron; else echo 'not starting correctly'; cat /tmp/out.txt; pkill -f electron; exit 1; fi" }, "devDependencies": { "@electron-forge/cli": "^7.5.0",