Merge pull request #7828 from elizaOS/develop #29
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Build & Release — Electrobun desktop app (signed/notarized on macOS, CEF on Windows/Linux) | |
| # Produces installer artifacts plus platform-prefixed .tar.zst/*-update.json | |
| # update channel files. | |
| # | |
| # Path notes for eliza-native port: | |
| # - The Electrobun shell lives at `packages/app-core/platforms/electrobun/` | |
| # in eliza (not `packages/app/electrobun/` — that directory only carries a | |
| # .gitignore stub). All build artifacts, scripts, and signing helpers live | |
| # under that platforms/electrobun/ path. | |
| # - Helper scripts that did not exist in upstream eliza (ensure-electrobun-core.mjs, | |
| # ensure-legacy-electrobun-compat.mjs, patch-release-check-pack-fallback.mjs) | |
| # have been removed; the corresponding caching/preflight steps are dropped | |
| # and surfaced as TODOs so the user can port them if needed. | |
| # | |
| # Required GitHub Secrets (macOS signing + notarization): | |
| # CSC_LINK – base64-encoded Developer ID Application .p12 | |
| # CSC_KEY_PASSWORD – password for the .p12 certificate | |
| # APPLE_ID – Apple ID email for notarization | |
| # APPLE_APP_SPECIFIC_PASSWORD – app-specific password from appleid.apple.com | |
| # APPLE_TEAM_ID – 10-char Apple Developer Team ID | |
| # | |
| # Configure on elizaOS/eliza repo settings: | |
| # WINDOWS_SIGN_CERT_BASE64, WINDOWS_SIGN_CERT_PASSWORD, WINDOWS_SIGN_TIMESTAMP_URL | |
| # ANTHROPIC_API_KEY (used by Windows packaged smoke test as the boot AI provider) | |
| # ELIZAOS_CLOUD_API_KEY / ELIZACLOUD_API_KEY / ELIZAOS_CLOUD_BASE_URL (optional cloud regression) | |
| # | |
| # Optional release-host upload secrets: | |
| # RELEASE_UPLOAD_KEY – SSH private key for releases server | |
| # RELEASE_HOST_FINGERPRINT – pinned SSH host key entry for releases host | |
| name: Build & Release (Electrobun) | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| workflow_call: | |
| inputs: | |
| tag: | |
| description: "Release tag override (e.g. v2.0.0-beta.0)" | |
| required: false | |
| type: string | |
| draft: | |
| description: "Create the GitHub release as a draft" | |
| required: false | |
| type: boolean | |
| default: false | |
| publish_release: | |
| description: "Create the GitHub release and upload updater files" | |
| required: false | |
| type: boolean | |
| default: false | |
| platform: | |
| description: "Desktop platform matrix to build (all, windows, macos, linux)" | |
| required: false | |
| type: string | |
| default: "all" | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: "Release tag (e.g. v2.0.0-beta.0)" | |
| required: false | |
| type: string | |
| draft: | |
| description: "Create as draft release" | |
| required: false | |
| type: boolean | |
| default: true | |
| platform: | |
| description: "Desktop platform matrix to build" | |
| required: false | |
| type: choice | |
| default: "all" | |
| options: | |
| - all | |
| - windows | |
| - macos | |
| - linux | |
| concurrency: | |
| group: release-electrobun-${{ github.ref }} | |
| cancel-in-progress: false | |
| permissions: | |
| contents: write | |
| env: | |
| CI: "true" | |
| BUN_VERSION: "1.3.13" # 1.3.13 is the verified desktop packaging runtime; 1.3.11 trips Electrobun bundle creation in CI. | |
| jobs: | |
| prepare: | |
| name: Prepare Release | |
| if: >- | |
| ${{ | |
| github.event_name != 'push' || | |
| ( | |
| !contains(github.ref_name, '-') | |
| ) | |
| }} | |
| runs-on: ${{ vars.RUNNER_UBUNTU || 'ubuntu-24.04' }} | |
| outputs: | |
| version: ${{ steps.version.outputs.version }} | |
| tag: ${{ steps.version.outputs.tag }} | |
| env: ${{ steps.version.outputs.env }} | |
| source_sha: ${{ steps.version.outputs.source_sha }} | |
| desktop_matrix: ${{ steps.desktop-matrix.outputs.desktop_matrix }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| submodules: false | |
| - name: Determine version + env | |
| id: version | |
| run: | | |
| if [[ -n "${{ inputs.tag }}" ]]; then | |
| TAG="${{ inputs.tag }}" | |
| elif [[ "${{ github.ref_type }}" == "tag" ]]; then | |
| TAG="${{ github.ref_name }}" | |
| else | |
| echo "Manual branch dispatches must provide inputs.tag; refusing to derive a release tag from package.json." >&2 | |
| exit 1 | |
| fi | |
| VERSION="${TAG#v}" | |
| # canary for pre-release tags, stable for everything else | |
| if [[ "$VERSION" == *"-"* ]]; then | |
| BUILD_ENV="canary" | |
| else | |
| BUILD_ENV="stable" | |
| fi | |
| { | |
| echo "tag=$TAG" | |
| echo "version=$VERSION" | |
| echo "env=$BUILD_ENV" | |
| echo "source_sha=$GITHUB_SHA" | |
| } >> "$GITHUB_OUTPUT" | |
| echo "Release: $VERSION (tag: $TAG, env: $BUILD_ENV, sha: $GITHUB_SHA)" | |
| - name: Select desktop platform matrix | |
| id: desktop-matrix | |
| env: | |
| RELEASE_PLATFORM: ${{ inputs.platform || 'all' }} | |
| run: | | |
| case "$RELEASE_PLATFORM" in | |
| ""|all) | |
| printf '%s\n' '{"platform":[{"name":"macOS (Apple Silicon)","os":"macos","runner":"macos-14","artifact-name":"macos-arm64"},{"name":"macOS (Intel)","os":"macos","runner":"macos-15-intel","artifact-name":"macos-x64"},{"name":"Windows","os":"windows","runner":"${{ vars.RUNNER_WINDOWS || 'windows-2025' }}","artifact-name":"windows-x64"},{"name":"Linux","os":"linux","runner":"ubuntu-latest","artifact-name":"linux-x64"}]}' > "$RUNNER_TEMP/desktop-matrix.json" | |
| ;; | |
| windows) | |
| printf '%s\n' '{"platform":[{"name":"Windows","os":"windows","runner":"${{ vars.RUNNER_WINDOWS || 'windows-2025' }}","artifact-name":"windows-x64"}]}' > "$RUNNER_TEMP/desktop-matrix.json" | |
| ;; | |
| macos) | |
| printf '%s\n' '{"platform":[{"name":"macOS (Apple Silicon)","os":"macos","runner":"macos-14","artifact-name":"macos-arm64"},{"name":"macOS (Intel)","os":"macos","runner":"macos-15-intel","artifact-name":"macos-x64"}]}' > "$RUNNER_TEMP/desktop-matrix.json" | |
| ;; | |
| linux) | |
| printf '%s\n' '{"platform":[{"name":"Linux","os":"linux","runner":"ubuntu-latest","artifact-name":"linux-x64"}]}' > "$RUNNER_TEMP/desktop-matrix.json" | |
| ;; | |
| *) | |
| echo "::error::Unsupported desktop release platform: $RELEASE_PLATFORM" | |
| exit 1 | |
| ;; | |
| esac | |
| echo "desktop_matrix=$(tr -d '\n' < "$RUNNER_TEMP/desktop-matrix.json")" >> "$GITHUB_OUTPUT" | |
| cat "$RUNNER_TEMP/desktop-matrix.json" | |
| validate-release: | |
| name: Validate Release Inputs | |
| if: ${{ inputs.platform != 'windows' }} | |
| needs: prepare | |
| runs-on: ${{ vars.RUNNER_UBUNTU || 'ubuntu-24.04' }} | |
| env: | |
| NODE_NO_WARNINGS: "1" | |
| defaults: | |
| run: | |
| shell: bash -euo pipefail {0} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| submodules: false | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: "24" | |
| - name: Setup Bun workspace | |
| uses: ./.github/actions/setup-bun-workspace | |
| with: | |
| bun-version: ${{ env.BUN_VERSION }} | |
| install-command: bun install --ignore-scripts | |
| install-native-deps: "true" | |
| run-postinstall: "true" | |
| init-submodules: "false" | |
| # TODO: verify this script exists in eliza or port equivalent. | |
| # In eliza this was `bun run test:regression-matrix:release`. | |
| - name: Regression matrix contract | |
| run: bun run test:regression-matrix:release | |
| - name: Prepare Whisper model artifact | |
| run: | | |
| bash packages/app-core/platforms/electrobun/scripts/ensure-whisper-gguf.sh base.en | |
| mkdir -p .artifacts/whisper-model | |
| cp "$HOME/.cache/eliza/whisper/ggml-base.en.bin" .artifacts/whisper-model/ggml-base.en.bin | |
| - name: Upload Whisper model artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: whisper-model-base-en | |
| path: .artifacts/whisper-model/ggml-base.en.bin | |
| if-no-files-found: error | |
| - name: Align version with release tag | |
| run: | | |
| node packages/app-core/scripts/align-electrobun-version.mjs | |
| node <<'NODE' | |
| const fs = require("node:fs"); | |
| const version = process.env.RELEASE_VERSION; | |
| const packagePath = "packages/app-core/platforms/electrobun/package.json"; | |
| const configPath = "packages/app-core/platforms/electrobun/electrobun.config.ts"; | |
| const pkg = JSON.parse(fs.readFileSync(packagePath, "utf8")); | |
| pkg.version = version; | |
| fs.writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + "\n"); | |
| let config = fs.readFileSync(configPath, "utf8"); | |
| config = config.replace(/version:\s*"[^"]+"/, `version: "${version}"`); | |
| fs.writeFileSync(configPath, config); | |
| NODE | |
| env: | |
| RELEASE_VERSION: ${{ needs.prepare.outputs.version }} | |
| - name: Ensure avatar assets | |
| run: node packages/app-core/scripts/ensure-avatars.mjs | |
| - name: Build core dist (server bundle) | |
| run: | | |
| node --input-type=module <<'NODE' | |
| import fs from "node:fs"; | |
| import path from "node:path"; | |
| const root = process.cwd(); | |
| const hasRootTsdownEntry = | |
| fs.existsSync(path.join(root, "tsdown.config.ts")) || | |
| fs.existsSync(path.join(root, "tsdown.config.mts")) || | |
| fs.existsSync(path.join(root, "tsdown.config.js")) || | |
| fs.existsSync(path.join(root, "src", "index.ts")); | |
| if (hasRootTsdownEntry) { | |
| const { spawnSync } = await import("node:child_process"); | |
| const result = spawnSync("bunx", ["tsdown", "--fail-on-warn", "false"], { | |
| cwd: root, | |
| stdio: "inherit", | |
| shell: process.platform === "win32", | |
| }); | |
| process.exit(result.status ?? 1); | |
| } | |
| const entryTarget = "../packages/app-core/src/entry.ts"; | |
| const entrySource = [ | |
| "// auto-generated by release-electrobun.yml", | |
| "// Standalone elizaOS checkouts do not have a root tsdown entry.", | |
| `export * from ${JSON.stringify(entryTarget)};`, | |
| "", | |
| ].join("\n"); | |
| fs.mkdirSync(path.join(root, "dist"), { recursive: true }); | |
| fs.writeFileSync(path.join(root, "dist", "entry.js"), entrySource); | |
| fs.writeFileSync(path.join(root, "dist", "index.js"), entrySource); | |
| fs.writeFileSync(path.join(root, "dist", "package.json"), '{"type":"module"}\n'); | |
| NODE | |
| node --import tsx packages/scripts/write-build-info.ts | |
| - name: Run heavy E2E regression suite | |
| # TODO: verify this target exists in eliza root package.json. | |
| run: bun run test:e2e:heavy | |
| - name: Probe optional cloud live regression key | |
| id: probe-cloud-live-key | |
| env: | |
| ELIZAOS_CLOUD_API_KEY: ${{ secrets.ELIZAOS_CLOUD_API_KEY != '' && secrets.ELIZAOS_CLOUD_API_KEY || secrets.ELIZACLOUD_API_KEY }} | |
| run: | | |
| if [ -n "${ELIZAOS_CLOUD_API_KEY:-}" ]; then | |
| echo "available=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "::warning::No Eliza Cloud API key configured for release validation - skipping optional cloud live regression." | |
| echo "available=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Run optional cloud live regression suite | |
| if: steps.probe-cloud-live-key.outputs.available == 'true' | |
| shell: bash | |
| run: | | |
| set -o pipefail | |
| log_file="$(mktemp)" | |
| if bun run test:live:cloud 2>&1 | tee "$log_file"; then | |
| exit 0 | |
| fi | |
| echo "::warning::Optional cloud live regression failed; release validation continues with deterministic build and packaging checks." | |
| exit 0 | |
| env: | |
| ELIZA_LIVE_TEST: "1" | |
| ELIZAOS_CLOUD_API_KEY: ${{ secrets.ELIZAOS_CLOUD_API_KEY != '' && secrets.ELIZAOS_CLOUD_API_KEY || secrets.ELIZACLOUD_API_KEY }} | |
| ELIZAOS_CLOUD_BASE_URL: ${{ secrets.ELIZAOS_CLOUD_BASE_URL }} | |
| - name: Restore build metadata after test rebuilds | |
| run: node --import tsx packages/scripts/write-build-info.ts | |
| - name: Generate release validation manifests | |
| run: | | |
| node packages/app-core/scripts/write-homepage-release-data.mjs | |
| node packages/app-core/scripts/generate-static-asset-manifest.mjs | |
| - name: Release readiness checks | |
| env: | |
| ELIZA_RELEASE_TAG: ${{ needs.prepare.outputs.tag }} | |
| ELIZA_CDN_VALIDATION_REF: ${{ github.sha }} | |
| ELIZA_VALIDATE_CDN: "1" | |
| run: bun run release:check | |
| build-browser-companions: | |
| name: Build Agent Browser Bridge companions | |
| if: ${{ inputs.platform == '' || inputs.platform == 'all' }} | |
| needs: [prepare, validate-release] | |
| runs-on: macos-15 | |
| timeout-minutes: 60 | |
| outputs: | |
| packaged: ${{ steps.package-browser-bridge.outputs.packaged }} | |
| defaults: | |
| run: | |
| shell: bash -euo pipefail {0} | |
| env: | |
| ELIZA_RELEASE_TAG: ${{ needs.prepare.outputs.tag }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| submodules: false | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: "24" | |
| - name: Setup Bun workspace | |
| uses: ./.github/actions/setup-bun-workspace | |
| with: | |
| bun-version: ${{ env.BUN_VERSION }} | |
| install-command: bun install --ignore-scripts | |
| install-native-deps: "false" | |
| run-postinstall: "true" | |
| init-submodules: "false" | |
| - name: Package Agent Browser Bridge release bundles | |
| id: package-browser-bridge | |
| run: | | |
| if bun run browser-bridge:package:release; then | |
| echo "packaged=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "::warning::Agent Browser Bridge packaging failed; desktop release will continue without browser companion bundles." | |
| echo "packaged=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Upload Agent Browser Bridge release artifacts | |
| if: steps.package-browser-bridge.outputs.packaged == 'true' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: browser-bridge-store-bundles | |
| path: | | |
| packages/browser-bridge-extension/dist/artifacts/browser-bridge-chrome-v*.zip | |
| packages/browser-bridge-extension/dist/artifacts/browser-bridge-safari-v*.zip | |
| packages/browser-bridge-extension/dist/artifacts/browser-bridge-safari-project-v*.zip | |
| packages/browser-bridge-extension/dist/artifacts/browser-bridge-release-manifest-v*.json | |
| if-no-files-found: error | |
| retention-days: 30 | |
| build: | |
| name: Build ${{ matrix.platform.name }} | |
| if: ${{ always() && needs.prepare.result == 'success' && (needs.validate-release.result == 'success' || needs.validate-release.result == 'skipped') }} | |
| needs: [prepare, validate-release] | |
| runs-on: ${{ matrix.platform.runner }} | |
| timeout-minutes: 150 | |
| defaults: | |
| run: | |
| shell: bash -euo pipefail {0} | |
| strategy: | |
| fail-fast: false | |
| matrix: ${{ fromJson(needs.prepare.outputs.desktop_matrix) }} | |
| steps: | |
| - name: Enable Git long paths (Windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| function Set-GitLongPaths([string]$Scope) { | |
| & git config $Scope core.longpaths true | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "git config $Scope core.longpaths true failed with exit code $LASTEXITCODE" | |
| } | |
| } | |
| try { | |
| Set-GitLongPaths "--system" | |
| Write-Host "Enabled Git long paths in system config." | |
| } catch { | |
| Write-Warning "System git config failed; falling back to --global. $($_.Exception.Message)" | |
| Set-GitLongPaths "--global" | |
| Write-Host "Enabled Git long paths in global config." | |
| } | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| submodules: false | |
| - name: Validate public desktop signing inputs | |
| env: | |
| BUILD_ENV: ${{ needs.prepare.outputs.env }} | |
| PUBLISH_RELEASE: ${{ inputs.publish_release || github.event_name == 'push' }} | |
| PLATFORM_OS: ${{ matrix.platform.os }} | |
| CSC_LINK: ${{ secrets.CSC_LINK }} | |
| CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| WINDOWS_SIGN_CERT_BASE64: ${{ secrets.WINDOWS_SIGN_CERT_BASE64 }} | |
| WINDOWS_SIGN_CERT_PASSWORD: ${{ secrets.WINDOWS_SIGN_CERT_PASSWORD }} | |
| WINDOWS_SIGN_TIMESTAMP_URL: ${{ secrets.WINDOWS_SIGN_TIMESTAMP_URL }} | |
| run: | | |
| if [[ "$PUBLISH_RELEASE" != "true" && "$BUILD_ENV" != "stable" ]]; then | |
| echo "Non-published canary build; signing inputs are advisory." | |
| exit 0 | |
| fi | |
| missing=() | |
| case "$PLATFORM_OS" in | |
| macos) | |
| [[ -z "$CSC_LINK" ]] && missing+=("CSC_LINK") | |
| [[ -z "$CSC_KEY_PASSWORD" ]] && missing+=("CSC_KEY_PASSWORD") | |
| [[ -z "$APPLE_ID" ]] && missing+=("APPLE_ID") | |
| [[ -z "$APPLE_APP_SPECIFIC_PASSWORD" ]] && missing+=("APPLE_APP_SPECIFIC_PASSWORD") | |
| [[ -z "$APPLE_TEAM_ID" ]] && missing+=("APPLE_TEAM_ID") | |
| ;; | |
| windows) | |
| [[ -z "$WINDOWS_SIGN_CERT_BASE64" ]] && missing+=("WINDOWS_SIGN_CERT_BASE64") | |
| [[ -z "$WINDOWS_SIGN_CERT_PASSWORD" ]] && missing+=("WINDOWS_SIGN_CERT_PASSWORD") | |
| [[ -z "$WINDOWS_SIGN_TIMESTAMP_URL" ]] && missing+=("WINDOWS_SIGN_TIMESTAMP_URL") | |
| ;; | |
| esac | |
| if [[ ${#missing[@]} -gt 0 ]]; then | |
| echo "::error::Missing public desktop signing inputs for $PLATFORM_OS: ${missing[*]}" | |
| exit 1 | |
| fi | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: "24" | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: ${{ env.BUN_VERSION }} | |
| - name: Install Linux packaging tools | |
| if: matrix.platform.os == 'linux' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y --no-install-recommends \ | |
| curl \ | |
| desktop-file-utils \ | |
| file \ | |
| rpm | |
| - name: Install root dependencies | |
| # --ignore-scripts skips native dependency install scripts (e.g. @tensorflow/tfjs-node | |
| # which has no pre-built binary for Node 22 on Windows and fails to compile). | |
| # Workspace prepare scripts are NOT suppressed by --ignore-scripts in bun. | |
| # macOS Intel: keep the x64 invocation explicit in case the runner image | |
| # ships with dual-arch shims. | |
| run: | | |
| if [ "${{ matrix.platform.os }}" = "macos" ] && [ "${{ matrix.platform.artifact-name }}" = "macos-x64" ]; then | |
| install_cmd=(arch -x86_64 bun install --ignore-scripts) | |
| else | |
| install_cmd=(bun install --ignore-scripts) | |
| fi | |
| for attempt in 1 2 3; do | |
| if "${install_cmd[@]}"; then | |
| exit 0 | |
| fi | |
| if [ "$attempt" -eq 3 ]; then | |
| echo "bun install failed after ${attempt} attempts" >&2 | |
| exit 1 | |
| fi | |
| echo "bun install failed on attempt ${attempt}; retrying in 15 seconds" >&2 | |
| sleep 15 | |
| done | |
| - name: Run repository postinstall patches | |
| run: bun run postinstall | |
| - name: Align version with release tag | |
| run: | | |
| node packages/app-core/scripts/align-electrobun-version.mjs | |
| node <<'NODE' | |
| const fs = require("node:fs"); | |
| const version = process.env.RELEASE_VERSION; | |
| const packagePath = "packages/app-core/platforms/electrobun/package.json"; | |
| const configPath = "packages/app-core/platforms/electrobun/electrobun.config.ts"; | |
| const pkg = JSON.parse(fs.readFileSync(packagePath, "utf8")); | |
| pkg.version = version; | |
| fs.writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + "\n"); | |
| let config = fs.readFileSync(configPath, "utf8"); | |
| config = config.replace(/version:\s*"[^"]+"/, `version: "${version}"`); | |
| fs.writeFileSync(configPath, config); | |
| NODE | |
| env: | |
| RELEASE_VERSION: ${{ needs.prepare.outputs.version }} | |
| - name: Generate i18n keyword data | |
| run: | | |
| node packages/shared/scripts/generate-keywords.mjs --target ts | |
| test -f packages/shared/src/i18n/generated/validation-keyword-data.js | |
| - name: Stage desktop bundle inputs | |
| run: | | |
| if [ "${{ matrix.platform.os }}" = "macos" ] && [ "${{ matrix.platform.artifact-name }}" = "macos-x64" ]; then | |
| ELIZA_DESKTOP_COMMAND_PREFIX="arch -x86_64" node packages/app-core/scripts/desktop-build.mjs stage --variant=base | |
| else | |
| node packages/app-core/scripts/desktop-build.mjs stage --variant=base | |
| fi | |
| mkdir -p dist/node_modules/@elizaos/shared/src/i18n/generated | |
| cp packages/shared/src/i18n/generated/validation-keyword-data.ts dist/node_modules/@elizaos/shared/src/i18n/generated/ | |
| cp packages/shared/src/i18n/generated/validation-keyword-data.js dist/node_modules/@elizaos/shared/src/i18n/generated/ | |
| test -f dist/node_modules/@elizaos/shared/src/i18n/generated/validation-keyword-data.js | |
| - name: Validate Electrobun runtime copy contract | |
| env: | |
| ELIZA_ELECTROBUN_REPO_ROOT: ${{ github.workspace }} | |
| run: | | |
| test -f dist/entry.js | |
| test -f package.json | |
| test -f dist/node_modules/@elizaos/agent/package.json | |
| test -f dist/node_modules/@elizaos/shared/src/i18n/generated/validation-keyword-data.js | |
| node --import tsx <<'NODE' | |
| const path = await import("node:path"); | |
| const configModule = await import("./packages/app-core/platforms/electrobun/electrobun.config.ts"); | |
| const config = configModule.default?.default ?? configModule.default; | |
| const electrobunDir = path.resolve("packages/app-core/platforms/electrobun"); | |
| const copyMap = config.build?.copy ?? {}; | |
| const runtimeSource = Object.entries(copyMap).find( | |
| ([, destination]) => destination === "eliza-dist", | |
| )?.[0]; | |
| if (!runtimeSource) { | |
| throw new Error("Electrobun copy map does not include eliza-dist runtime destination"); | |
| } | |
| const resolvedRuntimeSource = path.resolve(electrobunDir, runtimeSource); | |
| const expectedRuntimeSource = path.resolve("dist"); | |
| if (resolvedRuntimeSource !== expectedRuntimeSource) { | |
| throw new Error( | |
| `Electrobun runtime source resolves to ${resolvedRuntimeSource}, expected ${expectedRuntimeSource}`, | |
| ); | |
| } | |
| NODE | |
| # Import the Developer ID certificate into a temporary keychain. | |
| # Requires CSC_LINK (base64-encoded .p12) + CSC_KEY_PASSWORD. | |
| # The signing identity string is extracted and passed to Electrobun as | |
| # ELECTROBUN_DEVELOPER_ID in the build step below. | |
| - name: Set up macOS signing keychain | |
| if: matrix.platform.os == 'macos' | |
| id: macos-keychain | |
| env: | |
| CSC_LINK: ${{ secrets.CSC_LINK }} | |
| CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} | |
| run: | | |
| if [ -z "$CSC_LINK" ]; then | |
| echo "::warning::CSC_LINK not set — building unsigned (no codesign/notarize)" | |
| echo "skip_codesign=1" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| KEYCHAIN_PATH="$RUNNER_TEMP/eliza-signing.keychain-db" | |
| KEYCHAIN_PASSWORD="$(openssl rand -base64 32)" | |
| # Create a short-lived keychain for this build | |
| security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" | |
| security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| # Decode and import the .p12 certificate | |
| CERT_PATH="$RUNNER_TEMP/cert.p12" | |
| echo "$CSC_LINK" | base64 --decode > "$CERT_PATH" | |
| security import "$CERT_PATH" \ | |
| -k "$KEYCHAIN_PATH" \ | |
| -P "$CSC_KEY_PASSWORD" \ | |
| -T /usr/bin/codesign \ | |
| -T /usr/bin/security | |
| rm -f "$CERT_PATH" | |
| # Add to the keychain search list (preserving login keychain) | |
| security list-keychains -d user -s "$KEYCHAIN_PATH" login.keychain | |
| # Allow codesign to access the key without a password prompt | |
| security set-key-partition-list -S apple-tool:,apple:,codesign: -s \ | |
| -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| # Extract identity for Electrobun | |
| IDENTITY=$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" \ | |
| | grep "Developer ID Application" | head -1 | sed 's/.*"\(.*\)"/\1/') | |
| if [ -z "$IDENTITY" ]; then | |
| echo "::error::No 'Developer ID Application' identity found in certificate" | |
| exit 1 | |
| fi | |
| echo "Signing identity: $IDENTITY" | |
| echo "developer_id=$IDENTITY" >> "$GITHUB_OUTPUT" | |
| echo "skip_codesign=" >> "$GITHUB_OUTPUT" | |
| - name: Sign native macOS effects dylib | |
| if: matrix.platform.os == 'macos' && steps.macos-keychain.outputs.skip_codesign != '1' | |
| env: | |
| ELECTROBUN_DEVELOPER_ID: ${{ steps.macos-keychain.outputs.developer_id }} | |
| run: | | |
| DYLIB="packages/app-core/platforms/electrobun/src/libMacWindowEffects.dylib" | |
| if [ ! -f "$DYLIB" ]; then | |
| echo "::error::Expected $DYLIB to exist after build:native-effects" | |
| exit 1 | |
| fi | |
| codesign --force --timestamp --sign "$ELECTROBUN_DEVELOPER_ID" "$DYLIB" | |
| codesign --verify --strict --verbose=2 "$DYLIB" | |
| - name: Install quiet macOS packaging wrappers | |
| if: matrix.platform.os == 'macos' && steps.macos-keychain.outputs.skip_codesign != '1' | |
| run: | | |
| wrapper_dir="$RUNNER_TEMP/eliza-notary-bin" | |
| mkdir -p "$wrapper_dir" | |
| cp packages/app-core/platforms/electrobun/scripts/hdiutil-wrapper.sh "$wrapper_dir/hdiutil" | |
| cp packages/app-core/platforms/electrobun/scripts/xcrun-wrapper.sh "$wrapper_dir/xcrun" | |
| cp packages/app-core/platforms/electrobun/scripts/zip-wrapper.sh "$wrapper_dir/zip" | |
| chmod +x "$wrapper_dir/hdiutil" | |
| chmod +x "$wrapper_dir/xcrun" | |
| chmod +x "$wrapper_dir/zip" | |
| echo "$wrapper_dir" >> "$GITHUB_PATH" | |
| - name: Resolve electrobun package dir | |
| id: resolve-electrobun | |
| shell: bash | |
| run: | | |
| package_dir="$(node <<'NODE' | |
| const { createRequire } = require("node:module"); | |
| const fs = require("node:fs"); | |
| const path = require("node:path"); | |
| const workspacePackageJson = path.resolve("packages/app-core/platforms/electrobun/package.json"); | |
| const req = createRequire(workspacePackageJson); | |
| const entryPath = req.resolve("electrobun"); | |
| let packageDir = path.dirname(entryPath); | |
| while (!fs.existsSync(path.join(packageDir, "package.json"))) { | |
| const parentDir = path.dirname(packageDir); | |
| if (parentDir === packageDir) { | |
| throw new Error( | |
| `Could not find electrobun package.json starting from ${entryPath}`, | |
| ); | |
| } | |
| packageDir = parentDir; | |
| } | |
| const manifestPath = path.join(packageDir, "package.json"); | |
| const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); | |
| if (manifest.name !== "electrobun") { | |
| throw new Error( | |
| `Resolved unexpected package at ${manifestPath}: ${manifest.name}`, | |
| ); | |
| } | |
| process.stdout.write(packageDir); | |
| NODE | |
| )" | |
| if [ -z "$package_dir" ]; then | |
| echo "::error::Failed to resolve electrobun package directory" | |
| exit 1 | |
| fi | |
| case "${{ matrix.platform.artifact-name }}" in | |
| macos-arm64|macos-x64|linux-x64|linux-arm64) | |
| electrobun_core_target="${{ matrix.platform.artifact-name }}" | |
| ;; | |
| windows-x64) | |
| electrobun_core_target="win-x64" | |
| ;; | |
| *) | |
| echo "::error::Unsupported Electrobun release artifact target: ${{ matrix.platform.artifact-name }}" | |
| exit 1 | |
| ;; | |
| esac | |
| echo "Resolved electrobun package dir: $package_dir" | |
| echo "package-dir=$package_dir" >> "$GITHUB_OUTPUT" | |
| echo "cache-dir=$package_dir/.cache" >> "$GITHUB_OUTPUT" | |
| echo "core-target=$electrobun_core_target" >> "$GITHUB_OUTPUT" | |
| echo "core-cache-dir=$package_dir/dist-$electrobun_core_target" >> "$GITHUB_OUTPUT" | |
| - name: Cache Electrobun CLI and core binaries | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| ${{ steps.resolve-electrobun.outputs.cache-dir }} | |
| ${{ steps.resolve-electrobun.outputs.core-cache-dir }} | |
| key: electrobun-core-${{ matrix.platform.artifact-name }}-${{ hashFiles('packages/app-core/platforms/electrobun/package.json') }} | |
| restore-keys: electrobun-core-${{ matrix.platform.artifact-name }}- | |
| # TODO: verify this script exists in eliza. `ensure-electrobun-core.mjs` | |
| # was eliza-only and is not present in upstream eliza/packages/app-core/scripts/. | |
| # Without it the cache step above is decorative — Electrobun will fall | |
| # back to its own download flow on first build. Port the script if you | |
| # want pre-warmed core binaries on the first packaging run. | |
| - name: Prepare Electrobun core binaries | |
| run: | | |
| if [ -f packages/app-core/scripts/ensure-electrobun-core.mjs ]; then | |
| node packages/app-core/scripts/ensure-electrobun-core.mjs \ | |
| --package-dir "${{ steps.resolve-electrobun.outputs.package-dir }}" \ | |
| --target "${{ steps.resolve-electrobun.outputs.core-target }}" | |
| else | |
| echo "::warning::packages/app-core/scripts/ensure-electrobun-core.mjs not present — Electrobun will download core binaries on demand." | |
| fi | |
| - name: Probe Electrobun bun entry build | |
| working-directory: packages/app-core/platforms/electrobun | |
| run: | | |
| out_dir="$RUNNER_TEMP/electrobun-entry-probe" | |
| rm -rf "$out_dir" | |
| bun build src/index.ts --target=bun --packages=external --outdir "$out_dir" | |
| - name: Build patched Electrobun CLI | |
| shell: bash | |
| run: | | |
| node packages/app-core/scripts/build-patched-electrobun-cli.mjs "${{ steps.resolve-electrobun.outputs.package-dir }}" "${{ matrix.platform.artifact-name }}" | |
| - name: Build Electrobun app | |
| id: build-electrobun-app | |
| run: | | |
| set +e | |
| if [ "${{ matrix.platform.os }}" = "macos" ] && [ "${{ matrix.platform.artifact-name }}" = "macos-x64" ]; then | |
| ELIZA_DESKTOP_COMMAND_PREFIX="arch -x86_64" node packages/app-core/scripts/desktop-build.mjs package --env=${{ needs.prepare.outputs.env }} | |
| else | |
| node packages/app-core/scripts/desktop-build.mjs package --env=${{ needs.prepare.outputs.env }} | |
| fi | |
| status=$? | |
| set -e | |
| if [ "$status" -eq 0 ]; then | |
| echo "fallback=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| if [ "${{ inputs.draft }}" != "true" ] || [ "${{ inputs.publish_release }}" = "true" ]; then | |
| exit "$status" | |
| fi | |
| artifact_root="packages/app-core/platforms/electrobun/artifacts" | |
| build_root="packages/app-core/platforms/electrobun/build" | |
| build_dir="$(find "$build_root" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort | tail -1 || true)" | |
| if [ -z "$build_dir" ]; then | |
| echo "::error::Electrobun packaging failed and no built app tree exists" | |
| exit "$status" | |
| fi | |
| VERSION="$(node -p "require('./packages/app-core/platforms/electrobun/package.json').version")" | |
| ENV_NAME="${{ needs.prepare.outputs.env }}" | |
| find -L "$build_dir" -type d -name "Resources" | while read -r res_dir; do | |
| dest="$res_dir/version.json" | |
| printf '{"identifier":"ai.elizaos.Eliza","channel":"%s","name":"eliza","version":"%s"}' \ | |
| "$ENV_NAME" "$VERSION" > "$dest" | |
| echo "Wrote fallback $dest" | |
| done | |
| mkdir -p "$artifact_root" | |
| case "${{ matrix.platform.os }}" in | |
| linux) | |
| tar --zstd -cf "$artifact_root/eliza-${{ needs.prepare.outputs.env }}-${{ matrix.platform.artifact-name }}.tar.zst" -C "$build_root" "$(basename "$build_dir")" | |
| ;; | |
| macos) | |
| tar -czf "$artifact_root/eliza-${{ needs.prepare.outputs.env }}-${{ matrix.platform.artifact-name }}.app.tar.gz" -C "$build_root" "$(basename "$build_dir")" | |
| ;; | |
| windows) | |
| powershell -NoProfile -Command "Compress-Archive -Path '$build_dir' -DestinationPath '$artifact_root/eliza-${{ needs.prepare.outputs.env }}-${{ matrix.platform.artifact-name }}.exe.zip' -Force" | |
| ;; | |
| esac | |
| echo "fallback=true" >> "$GITHUB_OUTPUT" | |
| echo "::warning::Electrobun package command exited with $status; uploaded draft-validation fallback archive from $build_dir" | |
| env: | |
| # Electrobun reads these env vars directly. The workflow derives the | |
| # signing identity from CSC_LINK/CSC_KEY_PASSWORD during keychain setup | |
| # and maps the notarization credentials from the standard repo secrets. | |
| ELECTROBUN_DEVELOPER_ID: ${{ steps.macos-keychain.outputs.developer_id }} | |
| ELECTROBUN_APPLEID: ${{ secrets.APPLE_ID }} | |
| ELECTROBUN_APPLEIDPASS: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| ELECTROBUN_TEAMID: ${{ secrets.APPLE_TEAM_ID }} | |
| ELECTROBUN_SKIP_CODESIGN: ${{ steps.macos-keychain.outputs.skip_codesign }} | |
| ELECTROBUN_REAL_HDIUTIL: /usr/bin/hdiutil | |
| ELIZA_ELECTROBUN_NOTARIZE: 0 | |
| ELECTROBUN_REAL_XCRUN: /usr/bin/xcrun | |
| ELECTROBUN_REAL_ZIP: /usr/bin/zip | |
| ELIZA_ELECTROBUN_REPO_ROOT: ${{ github.workspace }} | |
| # Flat release root for the updater. Env is encoded in the platform prefix | |
| # (canary-macos-arm64-update.json etc.), so the baseUrl has no env subdir. | |
| # Points to GitHub Releases as the CDN; the ota-publish job attaches | |
| # latest-{channel}.json after the release job completes. | |
| ELIZA_RELEASE_URL: ${{ (github.event_name != 'workflow_call' || inputs.publish_release) && !inputs.draft && format('https://github.com/{0}/releases/download/{1}/', github.repository, needs.prepare.outputs.tag) || '' }} | |
| - name: Dump Electrobun build diagnostics | |
| if: failure() | |
| run: | | |
| root="packages/app-core/platforms/electrobun" | |
| echo "=== ${root}/build ===" | |
| find -L "$root/build" -maxdepth 6 -type f -print 2>/dev/null | sort | head -300 || true | |
| echo "=== ${root}/artifacts ===" | |
| find -L "$root/artifacts" -maxdepth 6 -type f -print 2>/dev/null | sort | head -300 || true | |
| echo "=== ${root} wrapper diagnostics ===" | |
| find -L "$root/build" -name wrapper-diagnostics.json -print -exec sed -n '1,220p' {} \; 2>/dev/null || true | |
| - name: Upload Electrobun build diagnostics | |
| if: failure() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: electrobun-${{ matrix.platform.artifact-name }}-build-diagnostics | |
| path: | | |
| packages/app-core/platforms/electrobun/build/** | |
| packages/app-core/platforms/electrobun/build/**/wrapper-diagnostics.json | |
| packages/app-core/platforms/electrobun/artifacts/** | |
| if-no-files-found: warn | |
| retention-days: 14 | |
| # Inject version.json into the packaged Resources directory. | |
| # The bun entry getVersionInfo() reads ../Resources/version.json and | |
| # throws if absent, killing the process before main() runs. | |
| # Electrobun does not generate this file for us, so write it from the | |
| # release metadata after the packaged bundle exists. | |
| - name: Inject version.json into bundle (Windows) | |
| if: matrix.platform.os == 'windows' | |
| shell: pwsh | |
| run: | | |
| $version = (Get-Content packages/app-core/platforms/electrobun/package.json | ConvertFrom-Json).version | |
| $envName = "${{ needs.prepare.outputs.env }}" | |
| $buildRoot = "packages/app-core/platforms/electrobun/build" | |
| Get-ChildItem -Path $buildRoot -Recurse -Directory -Filter "Resources" | ForEach-Object { | |
| $versionJson = @{ | |
| identifier = "ai.elizaos.Eliza" | |
| channel = $envName | |
| name = "eliza" | |
| version = $version | |
| } | ConvertTo-Json -Compress | |
| $dest = Join-Path $_.FullName "version.json" | |
| Write-Host "Writing $dest" | |
| Set-Content -Path $dest -Value $versionJson -Encoding utf8 | |
| } | |
| - name: Inject version.json into bundle (macOS / Linux) | |
| if: matrix.platform.os != 'windows' | |
| run: | | |
| VERSION="$(node -p "require('./packages/app-core/platforms/electrobun/package.json').version")" | |
| ENV_NAME="${{ needs.prepare.outputs.env }}" | |
| build_root="packages/app-core/platforms/electrobun/build" | |
| if [ -d "$build_root" ]; then | |
| find -L "$build_root" -type d -name "Resources" | while read -r res_dir; do | |
| dest="$res_dir/version.json" | |
| printf '{"identifier":"ai.elizaos.Eliza","channel":"%s","name":"eliza","version":"%s"}' \ | |
| "$ENV_NAME" "$VERSION" > "$dest" | |
| echo "Wrote $dest" | |
| done | |
| fi | |
| - name: Package Linux desktop installers | |
| if: matrix.platform.os == 'linux' && steps.build-electrobun-app.outputs.fallback != 'true' | |
| run: | | |
| node packages/app-core/scripts/package-electrobun-linux.mjs \ | |
| --version=${{ needs.prepare.outputs.version }} \ | |
| --channel=${{ needs.prepare.outputs.env }} \ | |
| --arch=x64 | |
| - name: List build output | |
| run: | | |
| echo "=== Electrobun artifacts ===" | |
| find -L packages/app-core/platforms/electrobun/artifacts -type f 2>/dev/null | sort || echo "(no artifacts directory)" | |
| - name: Stage standard macOS release app | |
| if: matrix.platform.os == 'macos' && steps.build-electrobun-app.outputs.fallback != 'true' | |
| env: | |
| ELECTROBUN_DEVELOPER_ID: ${{ steps.macos-keychain.outputs.developer_id }} | |
| ELECTROBUN_APPLEID: ${{ secrets.APPLE_ID }} | |
| ELECTROBUN_APPLEIDPASS: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| ELECTROBUN_TEAMID: ${{ secrets.APPLE_TEAM_ID }} | |
| ELECTROBUN_SKIP_CODESIGN: ${{ steps.macos-keychain.outputs.skip_codesign }} | |
| run: bash packages/app-core/platforms/electrobun/scripts/stage-macos-release-artifacts.sh | |
| - name: Sign Windows executables | |
| if: matrix.platform.os == 'windows' && steps.build-electrobun-app.outputs.fallback != 'true' | |
| env: | |
| # Configure on elizaOS/eliza repo settings. | |
| WINDOWS_SIGN_CERT_BASE64: ${{ secrets.WINDOWS_SIGN_CERT_BASE64 }} | |
| WINDOWS_SIGN_CERT_PASSWORD: ${{ secrets.WINDOWS_SIGN_CERT_PASSWORD }} | |
| WINDOWS_SIGN_TIMESTAMP_URL: ${{ secrets.WINDOWS_SIGN_TIMESTAMP_URL }} | |
| shell: pwsh | |
| run: pwsh -File packages/app-core/platforms/electrobun/scripts/sign-windows.ps1 -ArtifactsDir (Join-Path $PWD "packages/app-core/platforms/electrobun/artifacts") -BuildDir (Join-Path $PWD "packages/app-core/platforms/electrobun/build") | |
| - name: Install Inno Setup 6.7.1 | |
| if: matrix.platform.os == 'windows' && steps.build-electrobun-app.outputs.fallback != 'true' | |
| shell: pwsh | |
| run: | | |
| $url = "https://github.com/jrsoftware/issrc/releases/download/is-6_7_1/innosetup-6.7.1.exe" | |
| $installer = Join-Path $env:RUNNER_TEMP "innosetup-6.7.1.exe" | |
| Write-Host "Downloading Inno Setup 6.7.1..." | |
| Invoke-WebRequest -Uri $url -OutFile $installer -UseBasicParsing | |
| Write-Host "Installing silently..." | |
| Start-Process -FilePath $installer -ArgumentList "/VERYSILENT","/SUPPRESSMSGBOXES","/NORESTART","/SP-" -Wait -NoNewWindow | |
| $iscc = @( | |
| "C:\Program Files (x86)\Inno Setup 6\ISCC.exe", | |
| "C:\Program Files\Inno Setup 6\ISCC.exe" | |
| ) | Where-Object { Test-Path $_ } | Select-Object -First 1 | |
| if (-not $iscc) { | |
| Write-Error "ISCC.exe not found after installing Inno Setup 6.7.1" | |
| exit 1 | |
| } | |
| Add-Content -Path $env:GITHUB_ENV -Value "ELIZA_INNO_SETUP_COMPILER=$iscc" | |
| Write-Host "Using ISCC compiler: $iscc" | |
| - name: Extract Windows app bundle for Inno Setup | |
| if: matrix.platform.os == 'windows' && steps.build-electrobun-app.outputs.fallback != 'true' | |
| shell: pwsh | |
| run: | | |
| $artifactsDir = Join-Path $PWD "packages/app-core/platforms/electrobun/artifacts" | |
| $tarball = Get-ChildItem -Path $artifactsDir -File -Filter "*.tar.zst" -ErrorAction SilentlyContinue | | |
| Sort-Object LastWriteTime -Descending | | |
| Select-Object -First 1 | |
| if (-not $tarball) { | |
| Write-Error "No .tar.zst archive found in artifacts" | |
| exit 1 | |
| } | |
| # Use a short path to avoid Windows MAX_PATH (260 char) limits | |
| # with deeply nested node_modules inside the app bundle | |
| $extractDir = "C:\e" | |
| Remove-Item $extractDir -Recurse -Force -ErrorAction SilentlyContinue | |
| New-Item -ItemType Directory -Force -Path $extractDir | Out-Null | |
| Write-Host "Extracting $($tarball.Name) to $extractDir ..." | |
| tar -xf $tarball.FullName -C $extractDir | |
| Write-Host "Extracted top-level structure:" | |
| Get-ChildItem -Path $extractDir -Recurse -Depth 3 | ForEach-Object { Write-Host " $($_.FullName)" } | |
| # Verify the expected runtime entry point exists without mutating the | |
| # extracted installer source tree. Inno follows recursively, so | |
| # junction/copy aliases here can duplicate the deep runtime tree and | |
| # trip Windows path handling during compilation. | |
| $appRoot = Get-ChildItem -Path $extractDir -Directory | Select-Object -First 1 | |
| if ($appRoot) { | |
| $resApp = Join-Path $appRoot.FullName "Resources\app" | |
| $elizaDist = Join-Path $resApp "eliza-dist" | |
| $entryCheckEliza = Join-Path $elizaDist "entry.js" | |
| Write-Host "Checking for: $entryCheckEliza" | |
| if (Test-Path $entryCheckEliza) { | |
| Write-Host "eliza-dist/entry.js found" | |
| } else { | |
| Write-Host "WARNING: eliza-dist/entry.js NOT found" | |
| Write-Host "Resources/app contents:" | |
| if (Test-Path $resApp) { | |
| Get-ChildItem -Path $resApp -Recurse -Depth 2 | ForEach-Object { Write-Host " $($_.FullName)" } | |
| } else { | |
| Write-Host " Resources/app directory does not exist" | |
| } | |
| } | |
| $keywordDataCheck = Join-Path $elizaDist "node_modules\@elizaos\shared\src\i18n\generated\validation-keyword-data.js" | |
| Write-Host "Checking for: $keywordDataCheck" | |
| if (Test-Path $keywordDataCheck) { | |
| Write-Host "eliza-dist generated keyword data found" | |
| } else { | |
| Write-Error "eliza-dist generated keyword data NOT found: $keywordDataCheck" | |
| exit 1 | |
| } | |
| $agentManifestPath = Join-Path $elizaDist "node_modules\@elizaos\agent\package.json" | |
| Write-Host "Checking for: $agentManifestPath" | |
| if (Test-Path $agentManifestPath) { | |
| Write-Host "eliza-dist @elizaos/agent package manifest found" | |
| } else { | |
| Write-Error "eliza-dist @elizaos/agent package manifest NOT found: $agentManifestPath" | |
| exit 1 | |
| } | |
| } | |
| - name: Build Inno Setup installer | |
| if: matrix.platform.os == 'windows' && steps.build-electrobun-app.outputs.fallback != 'true' | |
| env: | |
| WINDOWS_SIGN_CERT_BASE64: ${{ secrets.WINDOWS_SIGN_CERT_BASE64 }} | |
| WINDOWS_SIGN_CERT_PASSWORD: ${{ secrets.WINDOWS_SIGN_CERT_PASSWORD }} | |
| WINDOWS_SIGN_TIMESTAMP_URL: ${{ secrets.WINDOWS_SIGN_TIMESTAMP_URL }} | |
| shell: pwsh | |
| run: | | |
| pwsh -File packages/app-core/packaging/inno/build-inno.ps1 ` | |
| -BuildDir "C:\e" ` | |
| -OutputDir (Join-Path $PWD "packages/app-core/platforms/electrobun/artifacts") ` | |
| -Version "${{ needs.prepare.outputs.version }}" ` | |
| -Channel "${{ needs.prepare.outputs.env }}" | |
| - name: Verify Windows public installer looks complete | |
| if: matrix.platform.os == 'windows' && steps.build-electrobun-app.outputs.fallback != 'true' | |
| shell: pwsh | |
| run: | | |
| $installer = Get-ChildItem -Path "packages/app-core/platforms/electrobun/artifacts" -File -Filter "ElizaOSApp-Setup-*.exe" -ErrorAction SilentlyContinue | | |
| Sort-Object LastWriteTime -Descending | | |
| Select-Object -First 1 | |
| if (-not $installer) { | |
| Write-Error "No public Windows installer found in packages/app-core/platforms/electrobun/artifacts" | |
| exit 1 | |
| } | |
| $minimumBytes = 50MB | |
| if ($installer.Length -lt $minimumBytes) { | |
| Write-Error "Installer is too small to be a standalone Windows package: $($installer.FullName) ($($installer.Length) bytes)" | |
| exit 1 | |
| } | |
| Write-Host "Verified standalone Windows installer: $($installer.Name)" | |
| Write-Host "Installer size: $([math]::Round($installer.Length / 1MB, 1)) MB" | |
| - name: Cache Windows embedding model | |
| if: matrix.platform.os == 'windows' && steps.build-electrobun-app.outputs.fallback != 'true' | |
| uses: actions/cache@v5 | |
| with: | |
| path: ~/.eliza/models | |
| key: embedding-model-nomic-embed-text-v1.5-Q4_K_S | |
| - name: Seed Windows embedding model cache | |
| if: matrix.platform.os == 'windows' && steps.build-electrobun-app.outputs.fallback != 'true' | |
| shell: pwsh | |
| run: | | |
| $modelsDir = Join-Path $env:USERPROFILE ".eliza\models" | |
| $modelName = "nomic-embed-text-v1.5.Q4_K_S.gguf" | |
| $modelRepo = "nomic-ai/nomic-embed-text-v1.5-GGUF" | |
| $modelPath = Join-Path $modelsDir $modelName | |
| if (-not (Test-Path $modelPath)) { | |
| New-Item -ItemType Directory -Force -Path $modelsDir | Out-Null | |
| $url = "https://huggingface.co/$modelRepo/resolve/main/$modelName" | |
| Write-Host "Downloading Windows embedding model cache seed: $url" | |
| Invoke-WebRequest -Uri $url -OutFile $modelPath | |
| } | |
| Write-Host "Windows embedding model cache ready: $modelPath" | |
| - name: Smoke test packaged Windows app | |
| if: matrix.platform.os == 'windows' && steps.build-electrobun-app.outputs.fallback != 'true' | |
| timeout-minutes: 30 | |
| shell: pwsh | |
| env: | |
| ELIZA_DISABLE_LOCAL_EMBEDDINGS: "1" | |
| # The runtime requires at least one AI provider plugin to fully | |
| # initialize — without it, startApiServer() can fail before | |
| # server.listen() and the health endpoint never becomes reachable. | |
| ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} | |
| ELIZA_WINDOWS_SMOKE_REQUIRE_INSTALLER: "1" | |
| # Use a short path under runner.temp to avoid MAX_PATH issues with | |
| # deeply nested node_modules while staying in a user-writable location. | |
| ELIZA_TEST_WINDOWS_INSTALL_DIR: ${{ runner.temp }}\el | |
| ELIZA_TEST_WINDOWS_LAUNCHER_DIR: ${{ runner.temp }}\eliza-windows-ui-launcher | |
| ELIZA_TEST_WINDOWS_LAUNCHER_PATH_FILE: ${{ runner.temp }}\eliza-windows-ui-launcher.txt | |
| # Inno installer is built into platforms/electrobun/artifacts. | |
| ELIZA_TEST_WINDOWS_ARTIFACTS_DIR: ${{ github.workspace }}\packages\app-core\platforms\electrobun\artifacts | |
| ELIZA_TEST_WINDOWS_BUILD_DIR: ${{ github.workspace }}\packages\app-core\platforms\electrobun\build | |
| run: | | |
| Remove-Item $env:ELIZA_TEST_WINDOWS_INSTALL_DIR -Recurse -Force -ErrorAction SilentlyContinue | |
| Remove-Item $env:ELIZA_TEST_WINDOWS_LAUNCHER_DIR -Recurse -Force -ErrorAction SilentlyContinue | |
| Remove-Item $env:ELIZA_TEST_WINDOWS_LAUNCHER_PATH_FILE -Force -ErrorAction SilentlyContinue | |
| bun run test:desktop:packaged:windows | |
| if ($LASTEXITCODE -ne 0) { | |
| Write-Error "Packaged Windows smoke test exited with code $LASTEXITCODE." | |
| exit $LASTEXITCODE | |
| } | |
| if (-not (Test-Path $env:ELIZA_TEST_WINDOWS_LAUNCHER_PATH_FILE)) { | |
| Write-Error "Smoke test did not emit a reusable Windows launcher path." | |
| exit 1 | |
| } | |
| $launcherPath = (Get-Content $env:ELIZA_TEST_WINDOWS_LAUNCHER_PATH_FILE -Raw).Trim() | |
| if ([string]::IsNullOrWhiteSpace($launcherPath) -or -not (Test-Path $launcherPath)) { | |
| Write-Error "Reusable Windows launcher path is invalid: $launcherPath" | |
| exit 1 | |
| } | |
| Add-Content -Path $env:GITHUB_ENV -Value "ELIZA_TEST_WINDOWS_LAUNCHER_PATH=$launcherPath" | |
| Write-Host "Reusing packaged Windows launcher for Playwright: $launcherPath" | |
| - name: Run Windows clean installer proof | |
| if: matrix.platform.os == 'windows' && steps.build-electrobun-app.outputs.fallback != 'true' | |
| shell: pwsh | |
| env: | |
| ELIZA_DISABLE_LOCAL_EMBEDDINGS: "1" | |
| ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} | |
| ELIZA_TEST_WINDOWS_PROOF_INSTALL_DIR: ${{ runner.temp }}\el-proof | |
| run: | | |
| Remove-Item $env:ELIZA_TEST_WINDOWS_PROOF_INSTALL_DIR -Recurse -Force -ErrorAction SilentlyContinue | |
| pwsh -File packages/app-core/platforms/electrobun/scripts/verify-windows-installer-proof.ps1 ` | |
| -ArtifactsDir (Join-Path $PWD "packages/app-core/platforms/electrobun/artifacts") ` | |
| -BuildDir (Join-Path $PWD "packages/app-core/platforms/electrobun/build") ` | |
| -ProofInstallDir $env:ELIZA_TEST_WINDOWS_PROOF_INSTALL_DIR ` | |
| -OutputDir (Join-Path $PWD "packages/app-core/platforms/electrobun/artifacts/windows-installer-proof") | |
| if ($LASTEXITCODE -ne 0) { | |
| Write-Error "Windows clean installer proof exited with code $LASTEXITCODE." | |
| exit $LASTEXITCODE | |
| } | |
| - name: Upload Windows installer proof artifact | |
| if: always() && matrix.platform.os == 'windows' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: electrobun-windows-installer-proof | |
| path: packages/app-core/platforms/electrobun/artifacts/windows-installer-proof/** | |
| if-no-files-found: warn | |
| retention-days: 14 | |
| - name: Upload Windows installer .exe + Inno log on smoke failure | |
| if: failure() && matrix.platform.os == 'windows' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: electrobun-windows-installer-debug | |
| path: | | |
| packages/app-core/platforms/electrobun/artifacts/*.exe | |
| ${{ runner.temp }}/eliza-inno-setup.log | |
| if-no-files-found: warn | |
| retention-days: 14 | |
| - name: Run Windows packaged renderer bootstrap check | |
| if: matrix.platform.os == 'windows' && steps.build-electrobun-app.outputs.fallback != 'true' | |
| run: bun run test:desktop:playwright | |
| - name: Upload Windows Playwright UI diagnostics | |
| if: failure() && matrix.platform.os == 'windows' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: electrobun-windows-ui-playwright-diagnostics | |
| # TODO: confirm packaged-app Playwright result location in eliza | |
| # (was apps/app/test-results/** in eliza; might be packages/app/test-results/**). | |
| path: packages/app/test-results/** | |
| if-no-files-found: warn | |
| retention-days: 14 | |
| - name: Stage Windows setup executables | |
| if: matrix.platform.os == 'windows' && steps.build-electrobun-app.outputs.fallback != 'true' | |
| shell: pwsh | |
| run: | | |
| $artifactsDir = Join-Path $PWD "packages/app-core/platforms/electrobun/artifacts" | |
| $setupExecutables = Get-ChildItem -Path "packages/app-core/platforms/electrobun/build" -Recurse -File -Filter "*Setup*.exe" -ErrorAction SilentlyContinue | |
| if (-not $setupExecutables) { | |
| Write-Error "No Windows setup executable found under packages/app-core/platforms/electrobun/build" | |
| exit 1 | |
| } | |
| New-Item -ItemType Directory -Force -Path $artifactsDir | Out-Null | |
| $publicInstaller = Get-ChildItem -Path $artifactsDir -File -Filter "ElizaOSApp-Setup-*.exe" -ErrorAction SilentlyContinue | | |
| Sort-Object LastWriteTime -Descending | | |
| Select-Object -First 1 | |
| foreach ($setupExecutable in $setupExecutables) { | |
| $destinationPath = Join-Path $artifactsDir $setupExecutable.Name | |
| if ( | |
| $publicInstaller -and | |
| $publicInstaller.Name -eq $setupExecutable.Name -and | |
| $publicInstaller.Length -ge 50MB -and | |
| $setupExecutable.Length -lt $publicInstaller.Length | |
| ) { | |
| Write-Warning "Skipping build setup stub that would overwrite verified public installer: $($setupExecutable.FullName)" | |
| continue | |
| } | |
| if (Test-Path $destinationPath) { | |
| Write-Host "Skipping setup executable already present in artifacts: $destinationPath" | |
| continue | |
| } | |
| Copy-Item $setupExecutable.FullName -Destination $destinationPath -Force | |
| Write-Host "Staged Windows setup executable: $($setupExecutable.Name)" | |
| } | |
| - name: Sign Windows executables (post-stage) | |
| if: matrix.platform.os == 'windows' && steps.build-electrobun-app.outputs.fallback != 'true' | |
| env: | |
| WINDOWS_SIGN_CERT_BASE64: ${{ secrets.WINDOWS_SIGN_CERT_BASE64 }} | |
| WINDOWS_SIGN_CERT_PASSWORD: ${{ secrets.WINDOWS_SIGN_CERT_PASSWORD }} | |
| WINDOWS_SIGN_TIMESTAMP_URL: ${{ secrets.WINDOWS_SIGN_TIMESTAMP_URL }} | |
| shell: pwsh | |
| run: pwsh -File packages/app-core/platforms/electrobun/scripts/sign-windows.ps1 -ArtifactsDir (Join-Path $PWD "packages/app-core/platforms/electrobun/artifacts") -BuildDir (Join-Path $PWD "packages/app-core/platforms/electrobun/build") | |
| - name: Verify Windows code signature | |
| if: matrix.platform.os == 'windows' && env.WINDOWS_SIGN_CERT_BASE64 != '' | |
| env: | |
| WINDOWS_SIGN_CERT_BASE64: ${{ secrets.WINDOWS_SIGN_CERT_BASE64 }} | |
| shell: pwsh | |
| run: | | |
| $signtool = Get-ChildItem "C:\Program Files (x86)\Windows Kits\10\bin" -Recurse -Filter "signtool.exe" -ErrorAction SilentlyContinue | | |
| Where-Object { $_.FullName -match "x64" } | | |
| Sort-Object { [version]($_.FullName -replace '.*\\(\d+\.\d+\.\d+\.\d+)\\.*', '$1') } -Descending | | |
| Select-Object -First 1 -ExpandProperty FullName | |
| $artifactsDir = Join-Path $PWD "packages/app-core/platforms/electrobun/artifacts" | |
| $signed = 0 | |
| $failed = 0 | |
| Get-ChildItem -Path $artifactsDir -Recurse -Filter "*.exe" -ErrorAction SilentlyContinue | ForEach-Object { | |
| Write-Host "Verifying: $($_.FullName)" | |
| & $signtool verify /pa /v $_.FullName | |
| if ($LASTEXITCODE -eq 0) { | |
| $signed++ | |
| } else { | |
| Write-Host "::error::Signature verification failed: $($_.Name)" | |
| $failed++ | |
| } | |
| } | |
| Write-Host "Verification complete: $signed signed, $failed failed" | |
| if ($failed -gt 0) { | |
| exit 1 | |
| } | |
| - name: Re-verify Windows public installer before upload | |
| if: matrix.platform.os == 'windows' && steps.build-electrobun-app.outputs.fallback != 'true' | |
| shell: pwsh | |
| run: | | |
| $installer = Get-ChildItem -Path "packages/app-core/platforms/electrobun/artifacts" -File -Filter "ElizaOSApp-Setup-*.exe" -ErrorAction SilentlyContinue | | |
| Sort-Object LastWriteTime -Descending | | |
| Select-Object -First 1 | |
| if (-not $installer) { | |
| Write-Error "No public Windows installer found in packages/app-core/platforms/electrobun/artifacts after staging/signing." | |
| exit 1 | |
| } | |
| $minimumBytes = 50MB | |
| if ($installer.Length -lt $minimumBytes) { | |
| Write-Error "Public Windows installer regressed below standalone size threshold: $($installer.FullName) ($($installer.Length) bytes)" | |
| exit 1 | |
| } | |
| Write-Host "Public installer still valid for release upload: $($installer.Name)" | |
| Write-Host "Installer size: $([math]::Round($installer.Length / 1MB, 1)) MB" | |
| - name: Build MSIX package | |
| if: matrix.platform.os == 'windows' && steps.build-electrobun-app.outputs.fallback != 'true' | |
| env: | |
| WINDOWS_SIGN_CERT_BASE64: ${{ secrets.WINDOWS_SIGN_CERT_BASE64 }} | |
| WINDOWS_SIGN_CERT_PASSWORD: ${{ secrets.WINDOWS_SIGN_CERT_PASSWORD }} | |
| WINDOWS_SIGN_TIMESTAMP_URL: ${{ secrets.WINDOWS_SIGN_TIMESTAMP_URL }} | |
| shell: pwsh | |
| run: | | |
| pwsh -File packages/app-core/packaging/msix/build-msix.ps1 ` | |
| -BuildDir (Join-Path $PWD "packages/app-core/platforms/electrobun/build") ` | |
| -OutputDir (Join-Path $PWD "packages/app-core/platforms/electrobun/artifacts") ` | |
| -Version "${{ needs.prepare.outputs.version }}" | |
| # When building unsigned (skip_codesign=1), the CEF framework ships with | |
| # a pre-existing signature. Injecting version.json into Resources breaks | |
| # that signature (resource hash mismatch). Ad-hoc re-sign the .app so | |
| # the bundle is internally consistent even without a Developer ID cert. | |
| - name: Ad-hoc re-sign unsigned macOS bundle | |
| if: matrix.platform.os == 'macos' && steps.macos-keychain.outputs.skip_codesign == '1' && steps.build-electrobun-app.outputs.fallback != 'true' | |
| run: | | |
| for app in packages/app-core/platforms/electrobun/artifacts/*.app; do | |
| [ -d "$app" ] || continue | |
| echo "Ad-hoc signing: $app" | |
| codesign --force --deep --sign - "$app" || echo "::warning::Ad-hoc re-sign failed for $app" | |
| done | |
| - name: Verify macOS signature and notarization | |
| if: matrix.platform.os == 'macos' && steps.build-electrobun-app.outputs.fallback != 'true' | |
| run: | | |
| shopt -s nullglob | |
| retry_stapler_validate() { | |
| local target="$1" | |
| local attempts="${2:-5}" | |
| local delay_seconds="${3:-15}" | |
| local attempt | |
| for ((attempt = 1; attempt <= attempts; attempt += 1)); do | |
| if xcrun stapler validate "$target"; then | |
| return 0 | |
| fi | |
| if [ "$attempt" -lt "$attempts" ]; then | |
| echo "::warning::stapler validate failed for $target (attempt $attempt/$attempts); retrying..." | |
| sleep "$((delay_seconds * attempt))" | |
| fi | |
| done | |
| return 1 | |
| } | |
| apps=() | |
| while IFS= read -r app; do | |
| apps+=("$app") | |
| done < <(find -L packages/app-core/platforms/electrobun/artifacts -maxdepth 1 -type d -name "*.app" | sort) | |
| mounted_volume="" | |
| if [ "${#apps[@]}" -eq 0 ]; then | |
| dmgs=(packages/app-core/platforms/electrobun/artifacts/*.dmg) | |
| if [ "${#dmgs[@]}" -eq 0 ]; then | |
| echo "::error::No .app bundle or .dmg found in packages/app-core/platforms/electrobun/artifacts" | |
| exit 1 | |
| fi | |
| dmg="${dmgs[0]}" | |
| echo "No .app bundle found in artifacts; mounting DMG: $dmg" | |
| mount_line="" | |
| for attempt in 1 2 3 4 5; do | |
| attach_output="$(hdiutil attach -nobrowse -readonly "$dmg" 2>&1)" && \ | |
| mount_line="$(printf '%s\n' "$attach_output" | awk '/\/Volumes\// { print substr($0, index($0, "/Volumes/")); exit }')" && \ | |
| [ -n "$mount_line" ] && [ -d "$mount_line" ] && break | |
| echo "::warning::DMG attach attempt $attempt/5 failed" | |
| if [ -n "${attach_output:-}" ]; then | |
| printf '%s\n' "$attach_output" | |
| fi | |
| mount_line="" | |
| sleep 2 | |
| done | |
| if [ -z "$mount_line" ]; then | |
| echo "::error::Failed to determine mounted DMG volume" | |
| exit 1 | |
| fi | |
| mounted_volume="$mount_line" | |
| trap 'if [ -n "$mounted_volume" ] && [ -d "$mounted_volume" ]; then hdiutil detach "$mounted_volume" >/dev/null 2>&1 || true; fi' EXIT | |
| apps=() | |
| while IFS= read -r app; do | |
| apps+=("$app") | |
| done < <(find "$mounted_volume" -maxdepth 1 -type d -name "*.app" | sort) | |
| if [ "${#apps[@]}" -eq 0 ]; then | |
| echo "::error::No .app bundle found inside mounted DMG" | |
| exit 1 | |
| fi | |
| fi | |
| for app in "${apps[@]}"; do | |
| echo "Verifying: $app" | |
| codesign --verify --deep --strict --verbose=2 "$app" | |
| info="$(codesign -dv --verbose=4 "$app" 2>&1 || true)" | |
| echo "$info" | grep -q "Authority=Developer ID Application" || { | |
| echo "::error::Missing Developer ID Application signature on $app" | |
| exit 1 | |
| } | |
| # Gatekeeper can reject the standalone staging copy on Intel even | |
| # when the notarized DMG is valid; treat as advisory. | |
| if ! spctl -a -vv --type exec "$app"; then | |
| echo "::warning::Gatekeeper rejected staged app bundle $app; continuing because the notarized DMG is the release artifact." | |
| fi | |
| retry_stapler_validate "$app" 3 10 || echo "::warning::Stapler validation failed (may be expected for .app; DMG stapling is the norm)" | |
| done | |
| found_dmg=0 | |
| while IFS= read -r dmg; do | |
| [[ -z "$dmg" ]] && continue | |
| found_dmg=1 | |
| echo "Verifying installer: $dmg" | |
| dmg_info="$(codesign -dv --verbose=4 "$dmg" 2>&1 || true)" | |
| echo "$dmg_info" | |
| echo "$dmg_info" | grep -q "Authority=Developer ID Application" || { | |
| echo "::error::Missing Developer ID Application signature on $dmg" | |
| exit 1 | |
| } | |
| retry_stapler_validate "$dmg" | |
| done < <(find -L packages/app-core/platforms/electrobun/artifacts -maxdepth 1 -type f -name "*.dmg" | sort) | |
| if [ "$found_dmg" -eq 0 ]; then | |
| echo "::error::No DMG found in packages/app-core/platforms/electrobun/artifacts" | |
| exit 1 | |
| fi | |
| - name: Smoke test packaged macOS app | |
| if: matrix.platform.os == 'macos' && steps.build-electrobun-app.outputs.fallback != 'true' | |
| continue-on-error: true | |
| env: | |
| SMOKE_DIAGNOSTICS_DIR: ${{ runner.temp }}/eliza-smoke-diagnostics | |
| run: | | |
| STARTUP_TIMEOUT=420 \ | |
| LIVENESS_TIMEOUT=10 \ | |
| SKIP_BUILD=1 \ | |
| SKIP_SIGNATURE_CHECK=1 \ | |
| bun run test:desktop:packaged | |
| - name: Collect Windows smoke diagnostics | |
| if: failure() && matrix.platform.os == 'windows' | |
| shell: pwsh | |
| run: | | |
| $diagnosticsDir = Join-Path $PWD "packages/app-core/platforms/electrobun/artifacts/windows-smoke-diagnostics" | |
| $appDataRoot = if ($env:ELIZA_TEST_WINDOWS_APPDATA_PATH) { $env:ELIZA_TEST_WINDOWS_APPDATA_PATH } else { $env:APPDATA } | |
| $localAppDataRoot = if ($env:ELIZA_TEST_WINDOWS_LOCALAPPDATA_PATH) { $env:ELIZA_TEST_WINDOWS_LOCALAPPDATA_PATH } else { $env:LOCALAPPDATA } | |
| $wrapperRoots = @(@( | |
| (Join-Path $localAppDataRoot "com.elizaai.eliza"), | |
| (Join-Path $localAppDataRoot "ai.elizaos.Eliza"), | |
| (Join-Path $localAppDataRoot "ai.elizaos.app") | |
| ) | Select-Object -Unique) | |
| Write-Host "Diagnostics roots:" | |
| Write-Host " appDataRoot = $appDataRoot" | |
| Write-Host " localAppDataRoot = $localAppDataRoot" | |
| foreach ($wrapperRoot in $wrapperRoots) { | |
| Write-Host " wrapperRoot = $wrapperRoot" | |
| } | |
| Remove-Item $diagnosticsDir -Recurse -Force -ErrorAction SilentlyContinue | |
| New-Item -ItemType Directory -Force -Path $diagnosticsDir | Out-Null | |
| if (Test-Path $appDataRoot) { | |
| Write-Host "--- APPDATA tree ($appDataRoot) ---" | |
| Get-ChildItem -Path $appDataRoot -Recurse -ErrorAction SilentlyContinue | ForEach-Object { Write-Host " $($_.FullName)" } | |
| Write-Host "--- end APPDATA tree ---" | |
| $logFiles = Get-ChildItem -Path $appDataRoot -Recurse -File -Include "*.log" -ErrorAction SilentlyContinue | |
| foreach ($log in $logFiles) { | |
| $relative = $log.FullName.Substring($appDataRoot.Length).TrimStart('\','/') | |
| $safeName = $relative -replace '[\\/:]', '_' | |
| $dest = Join-Path $diagnosticsDir $safeName | |
| New-Item -ItemType Directory -Force -Path (Split-Path -Parent $dest) | Out-Null | |
| Copy-Item $log.FullName -Destination $dest -Force -ErrorAction SilentlyContinue | |
| Write-Host "--- $($log.FullName) ---" | |
| Get-Content $log.FullName -Tail 400 -ErrorAction SilentlyContinue | ForEach-Object { Write-Host $_ } | |
| Write-Host "--- end $($log.FullName) ---" | |
| } | |
| if (-not $logFiles) { | |
| Write-Warning "No *.log files found anywhere under $appDataRoot" | |
| } | |
| } else { | |
| Write-Warning "APPDATA root not found: $appDataRoot" | |
| } | |
| if (Test-Path $localAppDataRoot) { | |
| $localLogFiles = Get-ChildItem -Path $localAppDataRoot -Recurse -File -Include "*.log" -ErrorAction SilentlyContinue | |
| foreach ($log in $localLogFiles) { | |
| $relative = $log.FullName.Substring($localAppDataRoot.Length).TrimStart('\','/') | |
| $safeName = "local_" + ($relative -replace '[\\/:]', '_') | |
| $dest = Join-Path $diagnosticsDir $safeName | |
| New-Item -ItemType Directory -Force -Path (Split-Path -Parent $dest) | Out-Null | |
| Copy-Item $log.FullName -Destination $dest -Force -ErrorAction SilentlyContinue | |
| Write-Host "--- $($log.FullName) ---" | |
| Get-Content $log.FullName -Tail 400 -ErrorAction SilentlyContinue | ForEach-Object { Write-Host $_ } | |
| Write-Host "--- end $($log.FullName) ---" | |
| } | |
| } | |
| foreach ($pattern in @("eliza-windows-smoke-*.state.json", "eliza-windows-smoke-*.events.jsonl", "eliza-startup-trace-*.state.json")) { | |
| $traceGlob = Join-Path $env:RUNNER_TEMP $pattern | |
| $traceFiles = Get-ChildItem -Path $traceGlob -ErrorAction SilentlyContinue | |
| if ($traceFiles) { | |
| foreach ($f in $traceFiles) { | |
| $dest = Join-Path $diagnosticsDir $f.Name | |
| New-Item -ItemType Directory -Force -Path (Split-Path -Parent $dest) | Out-Null | |
| Copy-Item $f.FullName -Destination $dest -Force -ErrorAction SilentlyContinue | |
| Write-Host "--- startup-trace: $($f.Name) ---" | |
| Get-Content $f.FullName -Tail 400 | ForEach-Object { Write-Host $_ } | |
| Write-Host "--- end $($f.Name) ---" | |
| } | |
| } | |
| } | |
| foreach ($root in @($wrapperRoots + @("C:\\e", "D:\\a\\_temp\\el"))) { | |
| if (Test-Path $root) { | |
| Get-ChildItem -Path $root -Recurse -File -Filter "startup-session.json" -ErrorAction SilentlyContinue | | |
| ForEach-Object { | |
| $dest = Join-Path $diagnosticsDir ("startup-session-" + ($_.FullName -replace '[\\:]', '_') + ".json") | |
| New-Item -ItemType Directory -Force -Path (Split-Path -Parent $dest) | Out-Null | |
| Copy-Item $_.FullName -Destination $dest -Force -ErrorAction SilentlyContinue | |
| Write-Host "--- $($_.FullName) ---" | |
| Get-Content $_.FullName | ForEach-Object { Write-Host $_ } | |
| Write-Host "--- end ---" | |
| } | |
| } | |
| } | |
| foreach ($wrapperRoot in $wrapperRoots) { | |
| if (Test-Path $wrapperRoot) { | |
| Get-ChildItem -Path $wrapperRoot -Recurse -File -Filter "wrapper-diagnostics.json" -ErrorAction SilentlyContinue | | |
| ForEach-Object { | |
| $relativePath = $_.FullName.Substring($wrapperRoot.Length).TrimStart('\\') | |
| $destination = Join-Path $diagnosticsDir $relativePath | |
| $destinationParent = Split-Path -Parent $destination | |
| New-Item -ItemType Directory -Force -Path $destinationParent | Out-Null | |
| Copy-Item $_.FullName -Destination $destination -Force | |
| } | |
| } else { | |
| Write-Warning "Wrapper diagnostics root not found at $wrapperRoot" | |
| } | |
| } | |
| - name: Upload Windows smoke diagnostics | |
| if: failure() && matrix.platform.os == 'windows' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: electrobun-${{ matrix.platform.artifact-name }}-smoke-diagnostics | |
| path: packages/app-core/platforms/electrobun/artifacts/windows-smoke-diagnostics/** | |
| if-no-files-found: warn | |
| retention-days: 14 | |
| - name: Upload macOS smoke diagnostics | |
| if: failure() && matrix.platform.os == 'macos' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: electrobun-${{ matrix.platform.artifact-name }}-smoke-diagnostics | |
| path: | | |
| ${{ runner.temp }}/eliza-smoke-diagnostics/** | |
| packages/app-core/platforms/electrobun/build/**/wrapper-diagnostics.json | |
| if-no-files-found: warn | |
| retention-days: 14 | |
| - name: Compress Windows artifacts before upload | |
| if: matrix.platform.os == 'windows' && steps.build-electrobun-app.outputs.fallback != 'true' | |
| shell: pwsh | |
| run: | | |
| $artifactsDir = Join-Path $PWD "packages/app-core/platforms/electrobun/artifacts" | |
| $canonicalInstallers = Get-ChildItem -Path $artifactsDir -File -Filter "ElizaOSApp-Setup-*.exe" -ErrorAction SilentlyContinue | | |
| Sort-Object LastWriteTime -Descending | |
| if (-not $canonicalInstallers) { | |
| Write-Error "No canonical Windows installer found before compression." | |
| exit 1 | |
| } | |
| if ($canonicalInstallers.Count -gt 1) { | |
| Write-Error "Multiple canonical Windows installers found before compression." | |
| $canonicalInstallers | ForEach-Object { Write-Host " - $($_.FullName)" } | |
| exit 1 | |
| } | |
| $canonicalInstaller = $canonicalInstallers | Select-Object -First 1 | |
| $otherSetupExecutables = Get-ChildItem -Path $artifactsDir -File -Filter "*Setup*.exe" -ErrorAction SilentlyContinue | | |
| Where-Object { $_.FullName -ne $canonicalInstaller.FullName } | |
| foreach ($extraSetup in $otherSetupExecutables) { | |
| Write-Warning "Removing non-canonical setup executable before upload: $($extraSetup.FullName)" | |
| Remove-Item $extraSetup.FullName -Force -ErrorAction SilentlyContinue | |
| } | |
| $exeFiles = Get-ChildItem -Path $artifactsDir -File -Filter "*.exe" -ErrorAction SilentlyContinue | |
| foreach ($exe in $exeFiles) { | |
| if ($exe.FullName -eq $canonicalInstaller.FullName) { | |
| Write-Host "Keeping canonical Windows installer uncompressed for single-extract downloads: $($exe.Name)" | |
| continue | |
| } | |
| $zipPath = $exe.FullName + ".zip" | |
| Write-Host "Compressing $($exe.Name) -> $($exe.Name).zip" | |
| Compress-Archive -Path $exe.FullName -DestinationPath $zipPath -CompressionLevel Optimal | |
| Remove-Item $exe.FullName -Force | |
| $originalMB = [math]::Round($exe.Length / 1MB, 1) | |
| $compressedMB = [math]::Round((Get-Item $zipPath).Length / 1MB, 1) | |
| Write-Host "Compressed: ${originalMB}MB -> ${compressedMB}MB" | |
| } | |
| - name: Prepare public canary Windows installer artifact | |
| if: matrix.platform.os == 'windows' && needs.prepare.outputs.env == 'canary' && steps.build-electrobun-app.outputs.fallback != 'true' | |
| shell: pwsh | |
| run: | | |
| $artifactsDir = Join-Path $PWD "packages/app-core/platforms/electrobun/artifacts" | |
| $publicCanaryDir = Join-Path $artifactsDir "public-canary-installer" | |
| Remove-Item $publicCanaryDir -Recurse -Force -ErrorAction SilentlyContinue | |
| New-Item -ItemType Directory -Force -Path $publicCanaryDir | Out-Null | |
| $canonicalInstallers = Get-ChildItem -Path $artifactsDir -File -Filter "ElizaOSApp-Setup-*.exe" -ErrorAction SilentlyContinue | | |
| Sort-Object LastWriteTime -Descending | |
| if ($canonicalInstallers) { | |
| if ($canonicalInstallers.Count -gt 1) { | |
| Write-Error "Multiple canonical Windows installers found for canary artifact publishing." | |
| $canonicalInstallers | ForEach-Object { Write-Host " - $($_.FullName)" } | |
| exit 1 | |
| } | |
| $canonicalInstaller = $canonicalInstallers | Select-Object -First 1 | |
| Copy-Item $canonicalInstaller.FullName -Destination $publicCanaryDir -Force | |
| } else { | |
| $canonicalInstallerZips = Get-ChildItem -Path $artifactsDir -File -Filter "ElizaOSApp-Setup-*.exe.zip" -ErrorAction SilentlyContinue | | |
| Sort-Object LastWriteTime -Descending | |
| if (-not $canonicalInstallerZips) { | |
| Write-Error "No canonical Windows installer (or zip fallback) found for canary artifact publishing." | |
| exit 1 | |
| } | |
| if ($canonicalInstallerZips.Count -gt 1) { | |
| Write-Error "Multiple canonical Windows installer zips found for canary artifact publishing." | |
| $canonicalInstallerZips | ForEach-Object { Write-Host " - $($_.FullName)" } | |
| exit 1 | |
| } | |
| $canonicalInstallerZip = $canonicalInstallerZips | Select-Object -First 1 | |
| Expand-Archive -Path $canonicalInstallerZip.FullName -DestinationPath $publicCanaryDir -Force | |
| } | |
| $publicInstallers = Get-ChildItem -Path $publicCanaryDir -File -Filter "ElizaOSApp-Setup-*.exe" -ErrorAction SilentlyContinue | | |
| Sort-Object LastWriteTime -Descending | |
| if (-not $publicInstallers) { | |
| Write-Error "Failed to stage a public Windows installer under $publicCanaryDir" | |
| exit 1 | |
| } | |
| if ($publicInstallers.Count -gt 1) { | |
| Write-Error "Canary installer artifact contains multiple public installers." | |
| $publicInstallers | ForEach-Object { Write-Host " - $($_.FullName)" } | |
| exit 1 | |
| } | |
| $publicInstaller = $publicInstallers | Select-Object -First 1 | |
| Write-Host "Prepared public canary installer artifact: $($publicInstaller.FullName)" | |
| - name: Upload public canary installer artifact | |
| if: matrix.platform.os == 'windows' && needs.prepare.outputs.env == 'canary' && steps.build-electrobun-app.outputs.fallback != 'true' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: electrobun-${{ matrix.platform.artifact-name }}-public-installer | |
| path: packages/app-core/platforms/electrobun/artifacts/public-canary-installer/ElizaOSApp-Setup-*.exe | |
| if-no-files-found: error | |
| retention-days: 30 | |
| compression-level: 0 | |
| - name: Upload build artifacts | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: electrobun-${{ matrix.platform.artifact-name }} | |
| path: | | |
| packages/app-core/platforms/electrobun/artifacts/*.dmg | |
| packages/app-core/platforms/electrobun/artifacts/*.AppImage | |
| packages/app-core/platforms/electrobun/artifacts/*.deb | |
| packages/app-core/platforms/electrobun/artifacts/*.rpm | |
| packages/app-core/platforms/electrobun/artifacts/*.tar.zst | |
| packages/app-core/platforms/electrobun/artifacts/*.exe | |
| packages/app-core/platforms/electrobun/artifacts/*.exe.zip | |
| packages/app-core/platforms/electrobun/artifacts/*.tar.gz | |
| packages/app-core/platforms/electrobun/artifacts/*-update.json | |
| packages/app-core/platforms/electrobun/artifacts/*.patch | |
| if-no-files-found: error | |
| retention-days: 30 | |
| compression-level: 0 | |
| - name: Upload MSIX artifact | |
| if: matrix.platform.os == 'windows' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: electrobun-${{ matrix.platform.artifact-name }}-msix | |
| path: packages/app-core/platforms/electrobun/artifacts/*.msix | |
| if-no-files-found: warn | |
| retention-days: 30 | |
| compression-level: 6 | |
| release: | |
| name: Create Release | |
| if: >- | |
| ${{ | |
| (inputs.platform == '' || inputs.platform == 'all') && | |
| (github.event_name != 'workflow_call' || inputs.publish_release) && | |
| (github.event_name != 'push' || needs.prepare.outputs.env != 'canary') | |
| }} | |
| needs: [prepare, build] | |
| runs-on: ${{ vars.RUNNER_UBUNTU || 'ubuntu-24.04' }} | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: write | |
| packages: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| path: release-artifacts | |
| pattern: electrobun-* | |
| merge-multiple: false | |
| - name: List artifacts | |
| run: find release-artifacts -type f | sort 2>/dev/null || echo "No artifacts" | |
| - name: Collect public release files | |
| run: | | |
| mkdir -p release-files | |
| # Public GitHub releases should expose end-user installables, not the | |
| # updater transport files. Windows publishes the standalone Inno Setup | |
| # installer, not the raw Electrobun bootstrap stub. | |
| find release-artifacts -type f \( \ | |
| -name "*.dmg" -o \ | |
| -name "*.app.tar.gz" -o \ | |
| -name "*.AppImage" -o \ | |
| -name "*.deb" -o \ | |
| -name "*.rpm" -o \ | |
| -name "ElizaOSApp-Setup-*.exe" -o \ | |
| -name "ElizaOSApp-Setup-*.exe.zip" -o \ | |
| -name "eliza-canary-windows-*.exe.zip" -o \ | |
| -name "*Setup*.tar.gz" -o \ | |
| -name "*.tar.zst" -o \ | |
| -name "*.flatpak" -o \ | |
| -name "*.msix" \ | |
| \) -exec cp {} release-files/ \; 2>/dev/null || true | |
| # Decompress Windows .exe.zip back to .exe for end-user downloads | |
| # (zip was only used for inter-job artifact transfer to avoid SAS token timeout) | |
| for f in release-files/ElizaOSApp-Setup-*.exe.zip; do | |
| [ -f "$f" ] || continue | |
| echo "Decompressing $f" | |
| cd release-files && unzip -o "$(basename "$f")" && rm "$(basename "$f")" && cd .. | |
| done | |
| echo "=== Public release files ===" | |
| ls -lh release-files/ 2>/dev/null || echo "None" | |
| - name: Collect update channel files | |
| run: | | |
| mkdir -p update-channel | |
| find release-artifacts -type f \( \ | |
| -name "*.tar.zst" -o \ | |
| -name "*.patch" -o \ | |
| -name "*-update.json" \ | |
| \) -exec cp {} update-channel/ \; 2>/dev/null || true | |
| echo "=== Update channel files ===" | |
| ls -lh update-channel/ 2>/dev/null || echo "None" | |
| - name: Generate checksums | |
| run: | | |
| cd release-files | |
| sha256sum -- * > SHA256SUMS.txt | |
| cat SHA256SUMS.txt | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v3 | |
| with: | |
| tag_name: ${{ needs.prepare.outputs.tag }} | |
| target_commitish: ${{ needs.prepare.outputs.source_sha }} | |
| name: Eliza ${{ needs.prepare.outputs.tag }} | |
| draft: ${{ inputs.draft || false }} | |
| prerelease: ${{ contains(needs.prepare.outputs.version, '-') }} | |
| generate_release_notes: true | |
| files: release-files/* | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # Upload platform-prefixed update manifests + tarballs to the eliza | |
| # releases host so the Electrobun updater can discover them via the | |
| # flat baseUrl contract in electrobun.config.ts: | |
| # ${baseUrl}/${platformPrefix}-update.json | |
| # ${baseUrl}/${platformPrefix}-${tarballFileName} | |
| # Requires RELEASE_UPLOAD_KEY secret (SSH private key for releases server). | |
| # Skipped for draft releases — test/preview builds must not pollute the production update channel. | |
| # TODO: configure releases.elizaos.ai (or chosen release host) and update | |
| # the rsync target / known_hosts entry below before enabling. | |
| - name: Upload update channel files to release host | |
| if: ${{ !inputs.draft }} | |
| env: | |
| RELEASE_UPLOAD_KEY: ${{ secrets.RELEASE_UPLOAD_KEY }} | |
| RELEASE_HOST_FINGERPRINT: ${{ secrets.RELEASE_HOST_FINGERPRINT }} | |
| run: | | |
| if [ -z "$RELEASE_UPLOAD_KEY" ]; then | |
| echo "No RELEASE_UPLOAD_KEY secret — skipping upload" | |
| exit 0 | |
| fi | |
| echo "$RELEASE_UPLOAD_KEY" > /tmp/release_key | |
| chmod 600 /tmp/release_key | |
| # Pin the host fingerprint from RELEASE_HOST_FINGERPRINT so SSH | |
| # verifies the server identity without disabling host-key checking. | |
| # To rotate: run `ssh-keyscan -H <release-host>` and update the secret. | |
| mkdir -p ~/.ssh && chmod 700 ~/.ssh | |
| echo "$RELEASE_HOST_FINGERPRINT" >> ~/.ssh/known_hosts | |
| # Electrobun resolves update metadata and tarballs at the flat | |
| # release root, keyed by platform prefix, not inside version folders. | |
| rsync -av --delete-after \ | |
| -e "ssh -i /tmp/release_key" \ | |
| update-channel/ \ | |
| releases@releases.elizaos.ai:/var/www/releases/ | |
| rm -f /tmp/release_key | |
| publish-browser-companions: | |
| name: Publish Agent Browser Bridge companions | |
| if: ${{ (github.event_name != 'workflow_call' || inputs.publish_release) && needs.build-browser-companions.outputs.packaged == 'true' && needs.release.result == 'success' }} | |
| needs: [prepare, build-browser-companions, release] | |
| runs-on: ${{ vars.RUNNER_UBUNTU || 'ubuntu-24.04' }} | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Download Agent Browser Bridge artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| path: browser-bridge-release | |
| pattern: browser-bridge-* | |
| merge-multiple: false | |
| - name: List Agent Browser Bridge artifacts | |
| run: find browser-bridge-release -type f | sort 2>/dev/null || echo "No Agent Browser Bridge artifacts" | |
| - name: Attach Agent Browser Bridge assets to GitHub release | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GH_REPO: ${{ github.repository }} | |
| run: | | |
| find browser-bridge-release -type f \ | |
| \( \ | |
| -name "browser-bridge-chrome-v*.zip" -o \ | |
| -name "browser-bridge-safari-v*.zip" -o \ | |
| -name "browser-bridge-release-manifest-v*.json" \ | |
| \) \ | |
| -print0 | xargs -0 -r gh release upload "${{ needs.prepare.outputs.tag }}" --repo "$GH_REPO" --clobber | |
| ota-publish: | |
| name: Publish OTA Channel Manifest | |
| if: ${{ success() && needs.release.result == 'success' }} | |
| needs: [prepare, release] | |
| runs-on: ubuntu-24.04 | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Generate channel manifest | |
| env: | |
| TAG: ${{ needs.prepare.outputs.tag }} | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| BUILD_ENV: ${{ needs.prepare.outputs.env }} | |
| run: | | |
| CHANNEL="${BUILD_ENV}" | |
| UPDATED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ) | |
| BASE_URL="https://github.com/${{ github.repository }}/releases/download/${TAG}" | |
| cat > "latest-${CHANNEL}.json" <<JSON | |
| { | |
| "channel": "${CHANNEL}", | |
| "version": "${VERSION}", | |
| "updatedAt": "${UPDATED_AT}", | |
| "platforms": { | |
| "macos-arm64": "${BASE_URL}/macos-arm64-update.json", | |
| "macos-x64": "${BASE_URL}/macos-x64-update.json", | |
| "windows-x64": "${BASE_URL}/windows-x64-update.json", | |
| "linux-x64": "${BASE_URL}/linux-x64-update.json" | |
| } | |
| } | |
| JSON | |
| echo "Channel manifest:" | |
| cat "latest-${CHANNEL}.json" | |
| - name: Attach channel manifest to GitHub Release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ needs.prepare.outputs.tag }} | |
| BUILD_ENV: ${{ needs.prepare.outputs.env }} | |
| run: | | |
| CHANNEL="${BUILD_ENV}" | |
| if ! gh release view "$TAG" >/dev/null 2>&1; then | |
| echo "::warning::No GitHub release found for $TAG; skipping OTA manifest upload." | |
| exit 0 | |
| fi | |
| gh release upload "$TAG" "latest-${CHANNEL}.json" --clobber | |
| echo "OTA manifest attached: latest-${CHANNEL}.json" | |
| echo "Update check URL: https://github.com/${{ github.repository }}/releases/download/${TAG}/latest-${CHANNEL}.json" | |
| - name: Upload via SSH (optional) | |
| if: ${{ env.RELEASE_UPLOAD_KEY != '' }} | |
| env: | |
| RELEASE_UPLOAD_KEY: ${{ secrets.RELEASE_UPLOAD_KEY }} | |
| RELEASE_HOST_FINGERPRINT: ${{ secrets.RELEASE_HOST_FINGERPRINT }} | |
| TAG: ${{ needs.prepare.outputs.tag }} | |
| BUILD_ENV: ${{ needs.prepare.outputs.env }} | |
| run: | | |
| mkdir -p ~/.ssh | |
| echo "$RELEASE_UPLOAD_KEY" > ~/.ssh/release_key | |
| chmod 600 ~/.ssh/release_key | |
| if [[ -n "$RELEASE_HOST_FINGERPRINT" ]]; then | |
| echo "$RELEASE_HOST_FINGERPRINT" >> ~/.ssh/known_hosts | |
| fi | |
| CHANNEL="${BUILD_ENV}" | |
| DEST_PATH="/srv/releases/elizaos/${TAG}/" | |
| rsync -av --mkpath \ | |
| -e "ssh -i ~/.ssh/release_key -o StrictHostKeyChecking=yes" \ | |
| "latest-${CHANNEL}.json" \ | |
| "releases@${RELEASE_HOST}:${DEST_PATH}" | |
| rm -f ~/.ssh/release_key |