From 250a7c27900ea9d1ce22ee5a98f03cb2d1c70dea Mon Sep 17 00:00:00 2001 From: Liang Jiaqing Date: Fri, 16 Oct 2026 13:43:12 +0800 Subject: [PATCH] ci: assemble GA source deployments from standalone desktop components --- .github/workflows/desktop-ci.yml | 159 +-- .github/workflows/desktop-release-package.yml | 966 ++---------------- docs/desktop_components.md | 33 + docs/macos_desktop_installation_zh.md | 2 + frontends/tests/test_bridge_capabilities.py | 2 +- 5 files changed, 154 insertions(+), 1008 deletions(-) create mode 100644 docs/desktop_components.md diff --git a/.github/workflows/desktop-ci.yml b/.github/workflows/desktop-ci.yml index 9706d3f4..c6cc8cf1 100644 --- a/.github/workflows/desktop-ci.yml +++ b/.github/workflows/desktop-ci.yml @@ -1,86 +1,23 @@ -name: Desktop 2.0 Quality Gates +name: GA backend contracts -on: +"on": push: branches: [main] - paths: - - "agentmain.py" - - "llmcore.py" - - "llmcore/**" - - "frontends/conductor.py" - - "frontends/cost_tracker.py" - - "frontends/data_backup.py" - - "frontends/desktop_bridge.py" - - "frontends/desktop_settings.py" - - "frontends/ga_contract_probe.py" - - "frontends/tests/**" - - "frontends/desktop/**" - - ".gitattributes" - - ".github/workflows/desktop-ci.yml" - - ".github/workflows/desktop-release-package.yml" + paths: ["agentmain.py", "llmcore.py", "llmcore/**", "frontends/**", ".github/workflows/**"] pull_request: branches: [main] - paths: - - "agentmain.py" - - "llmcore.py" - - "llmcore/**" - - "frontends/conductor.py" - - "frontends/cost_tracker.py" - - "frontends/data_backup.py" - - "frontends/desktop_bridge.py" - - "frontends/desktop_settings.py" - - "frontends/ga_contract_probe.py" - - "frontends/tests/**" - - "frontends/desktop/**" - - ".gitattributes" - - ".github/workflows/desktop-ci.yml" - - ".github/workflows/desktop-release-package.yml" + paths: ["agentmain.py", "llmcore.py", "llmcore/**", "frontends/**", ".github/workflows/**"] workflow_dispatch: -concurrency: - group: desktop-v2-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - permissions: contents: read -env: - PYTHONUNBUFFERED: "1" - CARGO_TERM_COLOR: always +concurrency: + group: ga-backend-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: - distribution-contract: - name: Compiled distribution contract - runs-on: ubuntu-24.04 - timeout-minutes: 10 - steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 - with: - node-version: 22 - - name: Verify tracked React distribution and package metadata - working-directory: frontends/desktop - run: npm run test:dist - - name: Verify packaging helper syntax - run: | - bash -n frontends/desktop/packaging/scripts/macos/install_macos.sh - bash -n frontends/desktop/scripts/post-dmg.sh - python3 -m py_compile frontends/desktop/scripts/gen_ds_store.py - - name: Reject source and local-development leakage - run: | - test -z "$(git ls-files -- \ - 'frontends/desktop/src/**' \ - 'frontends/desktop/public/**' \ - 'frontends/desktop/vite.config.*' \ - 'frontends/desktop/tsconfig*.json' \ - '.agents/**' '.codex/**' '.trellis/**')" - test -z "$(find frontends/desktop/dist -type f \ - \( -name '*.map' -o -name '*.ts' -o -name '*.tsx' -o -name '*.jsx' \) -print)" - ! git grep -nE '^(<<<<<<<|=======|>>>>>>>)' -- .github frontends - python-contracts: - name: Python ${{ matrix.python-version }} - needs: distribution-contract runs-on: ubuntu-24.04 timeout-minutes: 15 strategy: @@ -88,81 +25,25 @@ jobs: matrix: python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd + with: + persist-credentials: false + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 with: python-version: ${{ matrix.python-version }} cache: pip cache-dependency-path: frontends/tests/requirements.txt - - name: Install Desktop test dependencies - run: python -m pip install -r frontends/tests/requirements.txt + - run: python -m pip install -r frontends/tests/requirements.txt - name: Run bridge, conductor, cost and backup contracts + # These two legacy suites describe the removed bundled Desktop shell, + # not the GA backend. Do not fetch private Desktop source to run them. run: >- python -m pytest frontends/tests -q --ignore=frontends/tests/test_release_qualification.py - - name: Run release qualification tooling contracts - if: matrix.python-version == '3.12' - run: python -m pytest frontends/tests/test_release_qualification.py -q - - rust-contracts: - name: Rust ${{ matrix.feature.name }} - needs: distribution-contract - runs-on: ubuntu-24.04 - timeout-minutes: 35 - strategy: - fail-fast: false - matrix: - feature: - - name: production - args: "" - - name: e2e - args: "--features e2e" - steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - - name: Install Tauri system dependencies - run: >- - sudo apt-get update && sudo apt-get install -y - libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf - - uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable action snapshot - with: - toolchain: "1.95.0" - components: clippy, rustfmt - - uses: Swatinem/rust-cache@63fed3e2fecf6f7b51dc6f043341b79ef82a9ae7 # v2.9.2 - with: - workspaces: frontends/desktop/src-tauri -> target - - name: Verify Rust formatting - run: cargo fmt --manifest-path frontends/desktop/src-tauri/Cargo.toml --check - - name: Lint Tauri shell - run: cargo clippy --manifest-path frontends/desktop/src-tauri/Cargo.toml --all-targets ${{ matrix.feature.args }} -- -D warnings - - name: Run Tauri shell contracts - run: cargo test --manifest-path frontends/desktop/src-tauri/Cargo.toml ${{ matrix.feature.args }} - - linux-tauri-build: - name: Linux Tauri build smoke - needs: [distribution-contract, python-contracts, rust-contracts] - runs-on: ubuntu-24.04 - timeout-minutes: 45 - steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - - name: Install Tauri system dependencies - run: >- - sudo apt-get update && sudo apt-get install -y - libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf - - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 - with: - node-version: 22 - - uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable action snapshot - with: - toolchain: "1.95.0" - - uses: Swatinem/rust-cache@63fed3e2fecf6f7b51dc6f043341b79ef82a9ae7 # v2.9.2 - with: - workspaces: frontends/desktop/src-tauri -> target - - name: Install Tauri CLI only - working-directory: frontends/desktop + --ignore=frontends/tests/test_tauri_security_contract.py + - name: Verify Desktop remains an unlinked placeholder + shell: bash run: | - npm install --package-lock=false - node -e "const v=require('./node_modules/@tauri-apps/cli/package.json').version; if(v!=='2.11.4') throw new Error('unexpected Tauri CLI '+v)" - test ! -e package-lock.json - - name: Build tracked Desktop 2.0 distribution into the native shell - working-directory: frontends/desktop - run: npm run tauri build -- --no-bundle + set -euo pipefail + expected=$(printf '%s\n' frontends/desktop/.gitignore frontends/desktop/README.md) + test "$(git ls-files frontends/desktop)" = "$expected" diff --git a/.github/workflows/desktop-release-package.yml b/.github/workflows/desktop-release-package.yml index c24050b4..bdbd8ac0 100644 --- a/.github/workflows/desktop-release-package.yml +++ b/.github/workflows/desktop-release-package.yml @@ -1,887 +1,117 @@ -name: Build Desktop Portable Packages +name: Assemble GA source deployment -# Triggers: -# - workflow_dispatch -> build CI artifacts for testing (choose a target platform) -# - push tag desktop-portable-* -> build all three platforms and publish them -# together into ONE GitHub Release (tag = the pushed tag) -# Plain pushes to branches do NOT build (avoids 3 heavy Rust builds per commit). +# Windows publication still follows the existing release SOP. +# This workflow assembles source + a prebuilt component, never private source. "on": - push: - tags: - - "desktop-portable-*" workflow_dispatch: inputs: - target: - description: "Which platform(s) to build (artifacts only; release happens on tag push)" - required: false - default: "all" + platform: + description: Desktop component platform + required: true type: choice - options: - - "all" - - "windows" - - "linux" - - "macos" + options: [windows, macos, linux] + component_url: + description: Public HTTPS URL of the exe, signed app ZIP, or AppImage + required: true + type: string + component_sha256: + description: Expected SHA256 from the Desktop component build + required: true + type: string permissions: contents: read -env: - NODE_VERSION: "22.23.2" - RUST_TOOLCHAIN: "1.95.0" - PBS_RELEASE: "20260814" - PBS_PYTHON_VERSION: "3.12.14" - MACOS_PACKAGING_PYTHON_VERSION: "3.12.10" - PYTHONDONTWRITEBYTECODE: "1" - jobs: - # ---------------------------------------------------------------------------- - build-windows: - name: Windows portable - if: ${{ (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/desktop-portable-')) || (github.event_name == 'workflow_dispatch' && (github.event.inputs.target == 'all' || github.event.inputs.target == 'windows')) }} - runs-on: windows-2025 - permissions: - contents: read - steps: - - name: Checkout repository - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - with: - persist-credentials: false - - - name: Set up Node.js - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 - with: - node-version: ${{ env.NODE_VERSION }} - - - name: Set up Rust - uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable action snapshot - with: - toolchain: ${{ env.RUST_TOOLCHAIN }} - - - name: Cache Rust build - uses: Swatinem/rust-cache@63fed3e2fecf6f7b51dc6f043341b79ef82a9ae7 # v2.9.2 - with: - workspaces: frontends/desktop/src-tauri -> target - - - name: Install desktop dependencies - working-directory: frontends/desktop - run: | - npm install --package-lock=false - node -e "const v=require('./node_modules/@tauri-apps/cli/package.json').version; if(v!=='2.11.4') throw new Error('unexpected Tauri CLI '+v)" - test ! -e package-lock.json - - - name: Verify tracked compiled renderer bytes - working-directory: frontends/desktop - run: npm run test:dist - - - name: Build Windows desktop exe - working-directory: frontends/desktop - run: npm run tauri build -- --bundles nsis - - - name: Assemble self-contained portable bundle - shell: bash - run: | - set -euo pipefail - - purge_runtime_bytecode() { - local runtime_root="$1" - find "$runtime_root" -type d -name '__pycache__' -prune -exec rm -rf {} + - find "$runtime_root" -type f \( -name '*.pyc' -o -name '*.pyo' \) -delete - if find "$runtime_root" -type d -name '__pycache__' -print -quit | grep -q .; then - echo "Python bytecode cache directory remains in packaged runtime: $runtime_root" >&2 - return 1 - fi - if find "$runtime_root" -type f \( -name '*.pyc' -o -name '*.pyo' \) -print -quit | grep -q .; then - echo "Python bytecode remains in packaged runtime: $runtime_root" >&2 - return 1 - fi - } - - EXE_SRC="$(find frontends/desktop/src-tauri/target/release -maxdepth 1 -type f -name 'ga-desktop.exe' | head -n 1)" - [[ -n "$EXE_SRC" ]] || { echo "ga-desktop.exe not found" >&2; exit 1; } - - PKG="artifacts/windows/GenericAgent-Desktop-Windows-Portable" - RUNTIME="$PKG/runtime" - mkdir -p "$RUNTIME" - - # exe at the top level, named GenericAgent (typical portable form) - cp "$EXE_SRC" "$PKG/GenericAgent.exe" - cp frontends/desktop/packaging/scripts/windows/install_windows.ps1 "$RUNTIME/install_windows.ps1" - cp frontends/desktop/packaging/scripts/merge_desktop_settings.py "$RUNTIME/merge_desktop_settings.py" - # Uninstaller: double-click entry at top level, worker script under runtime/. - cp frontends/desktop/packaging/scripts/windows/uninstall.bat "$PKG/uninstall.bat" - cp frontends/desktop/packaging/scripts/windows/uninstall_windows.ps1 "$RUNTIME/uninstall_windows.ps1" - - # Fixed python-build-standalone Windows x86_64 archive. - PBS_URL="https://github.com/astral-sh/python-build-standalone/releases/download/${PBS_RELEASE}/cpython-${PBS_PYTHON_VERSION}%2B${PBS_RELEASE}-x86_64-pc-windows-msvc-install_only.tar.gz" - PBS_SHA256="7330282b47cd43a66b702d39078d2e5a88e580cee351d82f95045f21f5ee042a" - command -v cygpath >/dev/null 2>&1 || { echo "cygpath not found on Windows runner" >&2; exit 1; } - RUNNER_TEMP_POSIX="$(cygpath -u "$RUNNER_TEMP")" - [[ "$RUNNER_TEMP_POSIX" == /* ]] || { echo "cygpath did not return an absolute POSIX path" >&2; exit 1; } - PBS_ARCHIVE="${RUNNER_TEMP_POSIX}/pbs-windows-x86_64.tar.gz" - echo "Python build-standalone: $PBS_URL" - curl --proto '=https' --tlsv1.2 --fail --location --retry 3 --output "$PBS_ARCHIVE" "$PBS_URL" - printf '%s %s\n' "$PBS_SHA256" "$PBS_ARCHIVE" | sha256sum --check --strict - - tar -xzf "$PBS_ARCHIVE" -C "$RUNTIME" # extracts a 'python/' dir (python.exe at top) - PY="$RUNTIME/python/python.exe" - PYTHONDONTWRITEBYTECODE=1 "$PY" -c 'import platform, sys; assert sys.version_info[:3] == (3, 12, 14); assert platform.machine().lower() in ("amd64", "x86_64"); print(sys.version)' - - # App-local UCRT: python-build-standalone links the Universal CRT dynamically - # and does NOT bundle it. Win10/11 ship UCRT in-box, but stripped/older images - # may not -> python.exe fails to load (missing api-ms-win-crt-*.dll). Copy the - # redistributable UCRT DLLs next to python.exe (Microsoft-supported app-local - # deployment). Pure SDK source, no System32 fallback, hard-fail if absent so we - # never ship a bundle from an unexpected source. - UCRT_SRC="$(ls -d "/c/Program Files (x86)/Windows Kits/10/Redist/"*/ucrt/DLLs/x64 2>/dev/null | sort -V | tail -1)" - [[ -n "$UCRT_SRC" ]] || { echo "Windows SDK UCRT redist dir not found on runner" >&2; exit 1; } - echo "UCRT source: $UCRT_SRC" - cp "$UCRT_SRC"/api-ms-win-crt-*.dll "$RUNTIME/python/" - cp "$UCRT_SRC"/ucrtbase.dll "$RUNTIME/python/" - test -f "$RUNTIME/python/api-ms-win-crt-runtime-l1-1-0.dll" \ - || { echo "UCRT DLLs missing after copy" >&2; exit 1; } - - # Offline binary-only wheels. requirements.txt pins direct and transitive versions. - mkdir -p "$RUNTIME/wheels" - cp frontends/desktop/packaging/python-runtime-requirements.txt "$RUNTIME/wheels/requirements.txt" - PYTHONDONTWRITEBYTECODE=1 "$PY" -m pip download --only-binary=:all: --dest "$RUNTIME/wheels" \ - --requirement "$RUNTIME/wheels/requirements.txt" - - # Runtime source (project root minus heavy/dev/build dirs) - mkdir -p "$RUNTIME/app" - tar \ - --exclude='./.git' --exclude='./.github' \ - --exclude='./frontends/tests' \ - --exclude='./frontends/desktop/src-tauri' \ - --exclude='./frontends/desktop/src' \ - --exclude='./frontends/desktop/public' \ - --exclude='./frontends/desktop/scripts' \ - --exclude='./frontends/desktop/release_qualification' \ - --exclude='./frontends/desktop/tests' \ - --exclude='./frontends/desktop/testing' \ - --exclude='./frontends/desktop/spec' \ - --exclude='./frontends/desktop/node_modules' \ - --exclude='./frontends/desktop/dist' \ - --exclude='./frontends/desktop/DESIGN.md' \ - --exclude='./frontends/desktop/package.json' \ - --exclude='./frontends/desktop/package-lock.json' \ - --exclude='./frontends/desktop/.npmrc' \ - --exclude='./frontends/desktop/index.html' \ - --exclude='./frontends/desktop/loading.html' \ - --exclude='./frontends/desktop/setup.html' \ - --exclude='./frontends/desktop/tsconfig*.json' \ - --exclude='./frontends/desktop/vite.config.ts' \ - --exclude='./frontends/desktop/packaging' --exclude='./docs' \ - --exclude='./assets/demo' --exclude='./assets/images' \ - --exclude='./assets/GenericAgent_Technical_Report.pdf' \ - --exclude='./artifacts' \ - --exclude='*/node_modules' --exclude='*/target' \ - --exclude='*/.venv' --exclude='./.venv' \ - --exclude='*/__pycache__' --exclude='*.pyc' \ - -cf - . | tar -xf - -C "$RUNTIME/app" - test -f "$RUNTIME/app/agentmain.py" - test -f "$RUNTIME/app/frontends/desktop_bridge.py" - test -f "$RUNTIME/app/frontends/desktop/static/index.html" - test ! -e "$RUNTIME/app/frontends/desktop/dist" - test ! -e "$RUNTIME/app/frontends/desktop/src" - test ! -e "$RUNTIME/app/frontends/desktop/public" - test ! -e "$RUNTIME/app/frontends/desktop/package-lock.json" - test ! -e "$RUNTIME/app/frontends/desktop/node_modules" - - # Drop python debug symbols (.pdb) to slim the package (~80MB) - find "$RUNTIME/python" -name '*.pdb' -delete 2>/dev/null || true - purge_runtime_bytecode "$RUNTIME" - - cat > "$PKG/readme.txt" <<'EOF' - ================ 中文 ================ - GenericAgent Desktop — Windows 便携版(自包含) - - 无需安装 Python、无需联网装依赖、无需源码仓库——全部已内置。 - - 前置条件 - - Windows 10/11 x64 - - Microsoft Edge WebView2 运行时(Win11 一般自带;缺失时首次运行会提示安装) - - 使用 - 1. 解压到任意目录(路径建议不含特殊字符)。 - 2. 双击 GenericAgent.exe。 - 3. 首次启动会离线校验并补齐随包 Python 运行时依赖,界面显示进度,完成后进入主界面。 - 4. 之后启动直接秒进。 - - 说明 - - 想真正对话,仍需在程序里配置模型 / API Key。 - - 如果 Windows Defender 防火墙拦截 `127.0.0.1` / `localhost` 回环连接,程序可能无法启动或连接本机服务。请允许 `GenericAgent.exe` 和 `runtime\python\python.exe` 通过防火墙,然后重启桌面端。 - - 本便携版尚未使用 Windows 代码签名证书;SmartScreen 可能显示警告。运行前请用随包发布的 SHA256SUMS-windows.txt 核对 ZIP。 - - 可整体移动本文件夹;重启后会自动刷新路径。若需强制重新准备,请删除 runtime\.prepared 后重启。 - - runtime\ 是内置运行环境与源码,正常使用无需改动。 - - ================ English ================ - GenericAgent Desktop — Windows Portable (self-contained) - - No Python install, no internet for dependencies, no source checkout — everything is bundled. - - Requirements - - Windows 10/11 x64 - - Microsoft Edge WebView2 runtime (usually preinstalled on Win11; you are prompted if missing) - - Usage - 1. Extract anywhere (a path without special characters is recommended). - 2. Double-click GenericAgent.exe. - 3. The first launch verifies and completes the bundled Python runtime offline with a - progress UI, then opens the main window. - 4. Subsequent launches start instantly. - - Notes - - To actually chat, configure a model / API key inside the app. - - If Windows Defender Firewall blocks the `127.0.0.1` / `localhost` loopback connection, the app may fail to start or reach its local service. Allow `GenericAgent.exe` and `runtime\python\python.exe` through the firewall, then restart the desktop app. - - This portable build is not Windows code-signed, so SmartScreen may warn. Verify the ZIP against the published SHA256SUMS-windows.txt before running it. - - You may move the whole folder; relaunching refreshes its saved paths. To force a new - preparation pass, delete runtime\.prepared and relaunch. - - runtime\ holds the bundled runtime and source; no need to touch it. - EOF - sed -i 's/^ //' "$PKG/readme.txt" - - echo "Package tree (top levels):" - find "$PKG" -maxdepth 2 -printf '%y %p\n' | sort | head -40 - - - name: Zip portable package - shell: pwsh - run: | - New-Item -ItemType Directory -Force -Path artifacts/windows/out | Out-Null - Compress-Archive -Path artifacts/windows/GenericAgent-Desktop-Windows-Portable ` - -DestinationPath artifacts/windows/out/GenericAgent-Desktop-Windows-Portable.zip -Force - $h = Get-FileHash artifacts/windows/out/GenericAgent-Desktop-Windows-Portable.zip -Algorithm SHA256 - "{0} GenericAgent-Desktop-Windows-Portable.zip" -f $h.Hash.ToLowerInvariant() | - Set-Content -Encoding ASCII artifacts/windows/out/SHA256SUMS-windows.txt - Get-ChildItem artifacts/windows/out | Format-Table Name, Length - - - name: Upload workflow artifact - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: GenericAgent-Desktop-Windows-Portable-${{ github.run_number }} - path: | - artifacts/windows/out/GenericAgent-Desktop-Windows-Portable.zip - artifacts/windows/out/SHA256SUMS-windows.txt - if-no-files-found: error - - # ---------------------------------------------------------------------------- - build-linux: - name: Linux portable - if: ${{ (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/desktop-portable-')) || (github.event_name == 'workflow_dispatch' && (github.event.inputs.target == 'all' || github.event.inputs.target == 'linux')) }} - runs-on: ubuntu-22.04 - permissions: - contents: read - steps: - - name: Checkout repository - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - with: - persist-credentials: false - - - name: Set up Node.js - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 - with: - node-version: ${{ env.NODE_VERSION }} - - - name: Set up Rust - uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable action snapshot - with: - toolchain: ${{ env.RUST_TOOLCHAIN }} - - - name: Cache Rust build - uses: Swatinem/rust-cache@63fed3e2fecf6f7b51dc6f043341b79ef82a9ae7 # v2.9.2 - with: - # Keep registry/git downloads, but never restore binaries built on a newer glibc image. - prefix-key: "v1-rust-release-ubuntu-22.04-glibc-2.35" - workspaces: frontends/desktop/src-tauri -> target - cache-targets: "false" - cache-bin: "false" - - - name: Install Tauri Linux dependencies - run: | - sudo apt-get update - sudo apt-get install -y \ - libwebkit2gtk-4.1-dev \ - libgtk-3-dev \ - libayatana-appindicator3-dev \ - librsvg2-dev \ - patchelf \ - binutils \ - file \ - xvfb - - - name: Install desktop dependencies - working-directory: frontends/desktop - run: | - npm install --package-lock=false - node -e "const v=require('./node_modules/@tauri-apps/cli/package.json').version; if(v!=='2.11.4') throw new Error('unexpected Tauri CLI '+v)" - test ! -e package-lock.json - - - name: Verify tracked compiled renderer bytes - working-directory: frontends/desktop - run: npm run test:dist - - - name: Build Linux AppImage - working-directory: frontends/desktop - run: npm run tauri build -- --bundles appimage - - - name: Assemble self-contained portable bundle - shell: bash - run: | - set -euo pipefail - - purge_runtime_bytecode() { - local runtime_root="$1" - find "$runtime_root" -type d -name '__pycache__' -prune -exec rm -rf {} + - find "$runtime_root" -type f \( -name '*.pyc' -o -name '*.pyo' \) -delete - if find "$runtime_root" -type d -name '__pycache__' -print -quit | grep -q .; then - echo "Python bytecode cache directory remains in packaged runtime: $runtime_root" >&2 - return 1 - fi - if find "$runtime_root" -type f \( -name '*.pyc' -o -name '*.pyo' \) -print -quit | grep -q .; then - echo "Python bytecode remains in packaged runtime: $runtime_root" >&2 - return 1 - fi - } - - APPIMAGE_SRC="$(find frontends/desktop/src-tauri/target/release/bundle/appimage -maxdepth 1 -type f -name '*.AppImage' | head -n 1)" - [[ -n "$APPIMAGE_SRC" ]] || { echo "No AppImage found" >&2; exit 1; } - - PKG="artifacts/linux/GenericAgent-Desktop-Linux-Portable" - RUNTIME="$PKG/runtime" - mkdir -p "$RUNTIME" - - # Stable internal name so the generated .desktop Exec= never changes on upgrades. - cp "$APPIMAGE_SRC" "$PKG/GenericAgent.AppImage" - chmod +x "$PKG/GenericAgent.AppImage" - # Icon for the generated .desktop launcher (Icon= needs a real image file on Linux). - cp frontends/desktop/src-tauri/icons/icon.png "$PKG/GenericAgent.png" - cp frontends/desktop/packaging/scripts/linux/install_linux.sh "$RUNTIME/install_linux.sh" - cp frontends/desktop/packaging/scripts/merge_desktop_settings.py "$RUNTIME/merge_desktop_settings.py" - chmod +x "$RUNTIME/install_linux.sh" - # Uninstaller: top-level entry next to the AppImage. - cp frontends/desktop/packaging/scripts/linux/uninstall.sh "$PKG/uninstall.sh" - chmod +x "$PKG/uninstall.sh" - - # Fixed python-build-standalone Linux x86_64 archive. - PBS_URL="https://github.com/astral-sh/python-build-standalone/releases/download/${PBS_RELEASE}/cpython-${PBS_PYTHON_VERSION}%2B${PBS_RELEASE}-x86_64-unknown-linux-gnu-install_only.tar.gz" - PBS_SHA256="3297691ae34f75fed81ac424e040145fccb0bafe8e581cd5cadbddfa1c0766c0" - PBS_ARCHIVE="${RUNNER_TEMP}/pbs-linux-x86_64.tar.gz" - echo "Python build-standalone: $PBS_URL" - curl --proto '=https' --tlsv1.2 --fail --location --retry 3 --output "$PBS_ARCHIVE" "$PBS_URL" - printf '%s %s\n' "$PBS_SHA256" "$PBS_ARCHIVE" | sha256sum --check --strict - - tar -xzf "$PBS_ARCHIVE" -C "$RUNTIME" # extracts a 'python/' dir - PY="$RUNTIME/python/bin/python3" - PYTHONDONTWRITEBYTECODE=1 "$PY" -c 'import platform, sys; assert sys.version_info[:3] == (3, 12, 14); assert platform.machine() == "x86_64"; print(sys.version)' - - # Offline binary-only wheels. requirements.txt pins direct and transitive versions. - mkdir -p "$RUNTIME/wheels" - cp frontends/desktop/packaging/python-runtime-requirements.txt "$RUNTIME/wheels/requirements.txt" - PYTHONDONTWRITEBYTECODE=1 "$PY" -m pip download --only-binary=:all: --dest "$RUNTIME/wheels" \ - --requirement "$RUNTIME/wheels/requirements.txt" - - # Runtime source (project root minus heavy/dev/build dirs) - mkdir -p "$RUNTIME/app" - tar \ - --exclude='./.git' --exclude='./.github' \ - --exclude='./frontends/tests' \ - --exclude='./frontends/desktop/src-tauri' \ - --exclude='./frontends/desktop/src' \ - --exclude='./frontends/desktop/public' \ - --exclude='./frontends/desktop/scripts' \ - --exclude='./frontends/desktop/release_qualification' \ - --exclude='./frontends/desktop/tests' \ - --exclude='./frontends/desktop/testing' \ - --exclude='./frontends/desktop/spec' \ - --exclude='./frontends/desktop/node_modules' \ - --exclude='./frontends/desktop/dist' \ - --exclude='./frontends/desktop/DESIGN.md' \ - --exclude='./frontends/desktop/package.json' \ - --exclude='./frontends/desktop/package-lock.json' \ - --exclude='./frontends/desktop/.npmrc' \ - --exclude='./frontends/desktop/index.html' \ - --exclude='./frontends/desktop/loading.html' \ - --exclude='./frontends/desktop/setup.html' \ - --exclude='./frontends/desktop/tsconfig*.json' \ - --exclude='./frontends/desktop/vite.config.ts' \ - --exclude='./frontends/desktop/packaging' --exclude='./docs' \ - --exclude='./assets/demo' --exclude='./assets/images' \ - --exclude='./assets/GenericAgent_Technical_Report.pdf' \ - --exclude='./artifacts' \ - --exclude='*/node_modules' --exclude='*/target' \ - --exclude='*/.venv' --exclude='./.venv' \ - --exclude='*/__pycache__' --exclude='*.pyc' \ - -cf - . | tar -xf - -C "$RUNTIME/app" - test -f "$RUNTIME/app/agentmain.py" - test -f "$RUNTIME/app/frontends/desktop_bridge.py" - test -f "$RUNTIME/app/frontends/desktop/static/index.html" - test ! -e "$RUNTIME/app/frontends/desktop/dist" - test ! -e "$RUNTIME/app/frontends/desktop/src" - test ! -e "$RUNTIME/app/frontends/desktop/public" - test ! -e "$RUNTIME/app/frontends/desktop/package-lock.json" - test ! -e "$RUNTIME/app/frontends/desktop/node_modules" - - purge_runtime_bytecode "$RUNTIME" - - cat > "$PKG/readme.txt" <<'EOF' - ================ 中文 ================ - GenericAgent Desktop — Linux 便携版(自包含) - - 无需安装 Python、无需联网装依赖、无需源码仓库——全部已内置。 - - 前置条件 - - Linux x86_64,glibc 2.35 或更高版本(如 Ubuntu 22.04、Debian 12) - - 桌面环境 + FUSE(运行 AppImage 所需;多数发行版自带) - - 使用 - 1. 解压到任意目录(路径建议不含特殊字符)。 - 2. 给 AppImage 执行权限并运行: - chmod +x GenericAgent.AppImage - ./GenericAgent.AppImage - 3. 首次启动会离线校验并补齐随包 Python 运行时依赖,界面显示进度,完成后进入主界面。 - 4. 之后启动直接秒进。 - - 说明 - - 想真正对话,仍需在程序里配置模型 / API Key。 - - 可整体移动本文件夹;重启后会自动刷新路径。若需强制重新准备,请删除 runtime/.prepared 后重启。 - - runtime/ 是内置运行环境与源码,正常使用无需改动。 - - ================ English ================ - GenericAgent Desktop — Linux Portable (self-contained) - - No Python install, no internet for dependencies, no source checkout — everything is bundled. - - Requirements - - Linux x86_64 with glibc 2.35 or newer (for example Ubuntu 22.04 or Debian 12) - - A desktop environment + FUSE (required to run the AppImage; preinstalled on most distros) - - Usage - 1. Extract anywhere (a path without special characters is recommended). - 2. Make the AppImage executable and run it: - chmod +x GenericAgent.AppImage - ./GenericAgent.AppImage - 3. The first launch verifies and completes the bundled Python runtime offline with a - progress UI, then opens the main window. - 4. Subsequent launches start instantly. - - Notes - - To actually chat, configure a model / API key inside the app. - - You may move the whole folder; relaunching refreshes its saved paths. To force a new - preparation pass, delete runtime/.prepared and relaunch. - - runtime/ holds the bundled runtime and source; no need to touch it. - EOF - sed -i 's/^ //' "$PKG/readme.txt" - - mkdir -p artifacts/linux/out - tar -C artifacts/linux -czf artifacts/linux/out/GenericAgent-Desktop-Linux-Portable.tar.gz GenericAgent-Desktop-Linux-Portable - ( cd artifacts/linux/out && sha256sum GenericAgent-Desktop-Linux-Portable.tar.gz > SHA256SUMS-linux.txt ) - - tar -tzf artifacts/linux/out/GenericAgent-Desktop-Linux-Portable.tar.gz > /tmp/pkglist.txt - echo "Package contents (first 40 of $(wc -l < /tmp/pkglist.txt) entries):" - head -40 /tmp/pkglist.txt - ls -lh artifacts/linux/out - - # Exercise the exact archived candidate on the oldest supported builder. - SMOKE_ROOT="${RUNNER_TEMP}/ga-linux-package-smoke" - mkdir -p "$SMOKE_ROOT" - tar -xzf artifacts/linux/out/GenericAgent-Desktop-Linux-Portable.tar.gz -C "$SMOKE_ROOT" - SMOKE_PACKAGE="$SMOKE_ROOT/GenericAgent-Desktop-Linux-Portable" - APPIMAGE_ABS="$(realpath "$SMOKE_PACKAGE/GenericAgent.AppImage")" - APPIMAGE_SCAN="$SMOKE_ROOT/appimage-scan" - mkdir -p "$APPIMAGE_SCAN" - (cd "$APPIMAGE_SCAN" && "$APPIMAGE_ABS" --appimage-extract >/dev/null) - MAX_GLIBC="" - ELF_COUNT=0 - while IFS= read -r -d '' candidate; do - file -b "$candidate" | grep -q '^ELF' || continue - ELF_COUNT=$((ELF_COUNT + 1)) - candidate_max="$(readelf --version-info "$candidate" 2>/dev/null \ - | sed -n 's/.*Name: GLIBC_\([0-9][0-9.]*\).*/\1/p' \ - | sort -V | tail -n 1)" - [[ -n "$candidate_max" ]] || continue - if [[ -z "$MAX_GLIBC" \ - || "$(printf '%s\n%s\n' "$MAX_GLIBC" "$candidate_max" | sort -V | tail -n 1)" == "$candidate_max" ]]; then - MAX_GLIBC="$candidate_max" - fi - done < <(find "$SMOKE_PACKAGE" "$APPIMAGE_SCAN/squashfs-root" -type f -print0) - [[ "$ELF_COUNT" -gt 0 && -n "$MAX_GLIBC" ]] \ - || { echo "No GLIBC symbol versions found in final Linux package ELFs" >&2; exit 1; } - [[ "$(printf '%s\n%s\n' "$MAX_GLIBC" '2.35' | sort -V | tail -n 1)" == "2.35" ]] \ - || { echo "Final package requires GLIBC_$MAX_GLIBC; maximum allowed is GLIBC_2.35" >&2; exit 1; } - echo "Scanned $ELF_COUNT final package ELFs; maximum symbol is GLIBC_$MAX_GLIBC" - APPIMAGE_EXTRACT_AND_RUN=1 WEBKIT_DISABLE_COMPOSITING_MODE=1 \ - xvfb-run -a "$SMOKE_PACKAGE/GenericAgent.AppImage" >"$SMOKE_ROOT/app.log" 2>&1 & - SMOKE_PID=$! - cleanup_smoke() { - kill "$SMOKE_PID" 2>/dev/null || true - wait "$SMOKE_PID" 2>/dev/null || true - } - trap cleanup_smoke EXIT - IDENTITY="" - for _ in $(seq 1 120); do - IDENTITY="$(curl --fail --silent --max-time 1 http://127.0.0.1:14168/services/identity || true)" - [[ -n "$IDENTITY" ]] && break - sleep 1 - done - [[ -n "$IDENTITY" ]] || { cat "$SMOKE_ROOT/app.log" >&2; exit 1; } - EXPECTED_APP_DIR="$SMOKE_PACKAGE/runtime/app/frontends" \ - IDENTITY="$IDENTITY" python3 - <<'PY' - import json, os, pathlib - identity = json.loads(os.environ["IDENTITY"]) - actual = pathlib.Path(identity["app_dir"]).resolve() - expected = pathlib.Path(os.environ["EXPECTED_APP_DIR"]).resolve() - assert actual == expected, (actual, expected) - PY - curl --fail --silent --max-time 3 -X POST http://127.0.0.1:14168/services/bridge/exit >/dev/null - cleanup_smoke - trap - EXIT - - - name: Upload workflow artifact - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: GenericAgent-Desktop-Linux-Portable-${{ github.run_number }} - path: | - artifacts/linux/out/GenericAgent-Desktop-Linux-Portable.tar.gz - artifacts/linux/out/SHA256SUMS-linux.txt - if-no-files-found: error - - # ---------------------------------------------------------------------------- - build-macos: - name: macOS arm64 DMG - if: ${{ (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/desktop-portable-')) || (github.event_name == 'workflow_dispatch' && (github.event.inputs.target == 'all' || github.event.inputs.target == 'macos')) }} - runs-on: macos-26 - permissions: - contents: read + assemble: + runs-on: ${{ inputs.platform == 'macos' && 'macos-14' || 'ubuntu-24.04' }} + timeout-minutes: 20 env: - DEVELOPER_DIR: /Applications/Xcode_26.5.app/Contents/Developer + PLATFORM: ${{ inputs.platform }} + COMPONENT_URL: ${{ inputs.component_url }} + COMPONENT_SHA256: ${{ inputs.component_sha256 }} steps: - - name: Checkout repository - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd with: persist-credentials: false - - - name: Verify pinned macOS build environment - run: | - test "$(uname -m)" = arm64 - test "$(xcodebuild -version | sed -n '1p')" = "Xcode 26.5" - test "$(xcodebuild -version | sed -n '2p')" = "Build version 17F42" - test "$(xcrun --sdk macosx --show-sdk-version)" = "26.5" - - - name: Set up Node.js - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 - with: - node-version: ${{ env.NODE_VERSION }} - - - name: Set up Rust - uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable action snapshot - with: - toolchain: ${{ env.RUST_TOOLCHAIN }} - - - name: Cache Rust build - uses: Swatinem/rust-cache@63fed3e2fecf6f7b51dc6f043341b79ef82a9ae7 # v2.9.2 - with: - workspaces: frontends/desktop/src-tauri -> target - - - name: Install desktop dependencies - working-directory: frontends/desktop - run: | - npm install --package-lock=false - node -e "const v=require('./node_modules/@tauri-apps/cli/package.json').version; if(v!=='2.11.4') throw new Error('unexpected Tauri CLI '+v)" - test ! -e package-lock.json - - - name: Set up Python DMG tooling runtime - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - with: - python-version: ${{ env.MACOS_PACKAGING_PYTHON_VERSION }} - - - name: Install Python packaging tools - run: >- - python3 -m pip install --require-hashes --only-binary=:all: - --requirement frontends/desktop/packaging/dmg-build-requirements.txt - - - name: Verify tracked compiled renderer bytes - working-directory: frontends/desktop - run: npm run test:dist - - - name: Build macOS .app - working-directory: frontends/desktop - run: npm run tauri build -- --bundles app - - - name: Assemble self-contained DMG app + - name: Fetch fixed component and export GA source shell: bash run: | set -euo pipefail - - purge_runtime_bytecode() { - local runtime_root="$1" - find "$runtime_root" -type d -name '__pycache__' -prune -exec rm -rf {} + - find "$runtime_root" -type f \( -name '*.pyc' -o -name '*.pyo' \) -delete - if find "$runtime_root" -type d -name '__pycache__' -print -quit | grep -q .; then - echo "Python bytecode cache directory remains in packaged runtime: $runtime_root" >&2 - return 1 - fi - if find "$runtime_root" -type f \( -name '*.pyc' -o -name '*.pyo' \) -print -quit | grep -q .; then - echo "Python bytecode remains in packaged runtime: $runtime_root" >&2 - return 1 - fi - } - - APP_SRC="$(find frontends/desktop/src-tauri/target/release/bundle/macos -maxdepth 1 -name '*.app' -type d | head -n 1)" - [[ -n "$APP_SRC" ]] || { echo "No .app found" >&2; exit 1; } - - # Build one runtime tree and embed it only in the final DMG application. - RUNTIME_SRC="artifacts/macos/runtime-src" - mkdir -p "$RUNTIME_SRC" - cp frontends/desktop/packaging/scripts/macos/install_macos.sh "$RUNTIME_SRC/install_macos.sh" - cp frontends/desktop/packaging/scripts/merge_desktop_settings.py "$RUNTIME_SRC/merge_desktop_settings.py" - chmod +x "$RUNTIME_SRC/install_macos.sh" - - # Fixed python-build-standalone macOS arm64 archive. - PBS_URL="https://github.com/astral-sh/python-build-standalone/releases/download/${PBS_RELEASE}/cpython-${PBS_PYTHON_VERSION}%2B${PBS_RELEASE}-aarch64-apple-darwin-install_only.tar.gz" - PBS_SHA256="4572133a5542f306b9bdb155da5800f9e38950cd0a98d469b832ce256fe299ea" - PBS_ARCHIVE="${RUNNER_TEMP}/pbs-macos-aarch64.tar.gz" - echo "Python build-standalone: $PBS_URL" - curl --proto '=https' --tlsv1.2 --fail --location --retry 3 --output "$PBS_ARCHIVE" "$PBS_URL" - printf '%s %s\n' "$PBS_SHA256" "$PBS_ARCHIVE" | shasum -a 256 --check - - tar -xzf "$PBS_ARCHIVE" -C "$RUNTIME_SRC" # extracts a 'python/' dir - PY="$RUNTIME_SRC/python/bin/python3" - PYTHONDONTWRITEBYTECODE=1 "$PY" -c 'import platform, sys; assert sys.version_info[:3] == (3, 12, 14); assert platform.machine() == "arm64"; print(sys.version)' - - # Offline binary-only wheels. requirements.txt pins direct and transitive versions. - mkdir -p "$RUNTIME_SRC/wheels" - cp frontends/desktop/packaging/python-runtime-requirements.txt "$RUNTIME_SRC/wheels/requirements.txt" - PYTHONDONTWRITEBYTECODE=1 "$PY" -m pip download --only-binary=:all: --dest "$RUNTIME_SRC/wheels" \ - --requirement "$RUNTIME_SRC/wheels/requirements.txt" - - # macOS applications must be immutable at first launch. Install from the downloaded - # wheelhouse before the runtime enters the signed .app, then carry a durable marker. - # The binary-only offline repair path needs neither setuptools nor wheel. Remove any - # copies shipped in the base interpreter; bytecode writes are disabled at runtime too. - PYTHONDONTWRITEBYTECODE=1 "$PY" -m pip install --no-compile --no-index --find-links "$RUNTIME_SRC/wheels" \ - --requirement "$RUNTIME_SRC/wheels/requirements.txt" - PYTHONDONTWRITEBYTECODE=1 "$PY" -m pip uninstall --yes setuptools wheel - PYTHONDONTWRITEBYTECODE=1 "$PY" -m pip check - - PYTHONDONTWRITEBYTECODE=1 "$PY" -c 'import aiohttp, fastapi, importlib.util, pydantic, uvicorn, websockets; assert all(importlib.util.find_spec(name) is None for name in ("setuptools", "wheel", "pkg_resources"))' - purge_runtime_bytecode "$RUNTIME_SRC" - printf 'ok\n' > "$RUNTIME_SRC/.prepared" - test -s "$RUNTIME_SRC/.prepared" - - # Runtime source (project root minus heavy/dev/build dirs) — bsdtar exclude syntax. - mkdir -p "$RUNTIME_SRC/app" - tar \ - --exclude='.git' --exclude='.github' \ - --exclude='frontends/tests' \ - --exclude='frontends/desktop/src-tauri' \ - --exclude='frontends/desktop/src' \ - --exclude='frontends/desktop/public' \ - --exclude='frontends/desktop/scripts' \ - --exclude='frontends/desktop/release_qualification' \ - --exclude='frontends/desktop/tests' \ - --exclude='frontends/desktop/testing' \ - --exclude='frontends/desktop/spec' \ - --exclude='frontends/desktop/node_modules' \ - --exclude='frontends/desktop/dist' \ - --exclude='frontends/desktop/DESIGN.md' \ - --exclude='frontends/desktop/package.json' \ - --exclude='frontends/desktop/package-lock.json' \ - --exclude='frontends/desktop/.npmrc' \ - --exclude='frontends/desktop/index.html' \ - --exclude='frontends/desktop/loading.html' \ - --exclude='frontends/desktop/setup.html' \ - --exclude='frontends/desktop/tsconfig*.json' \ - --exclude='frontends/desktop/vite.config.ts' \ - --exclude='frontends/desktop/packaging' --exclude='docs' \ - --exclude='assets/demo' --exclude='assets/images' \ - --exclude='assets/GenericAgent_Technical_Report.pdf' \ - --exclude='artifacts' \ - --exclude='*/node_modules' --exclude='*/target' \ - --exclude='*/.venv' --exclude='.venv' \ - --exclude='*/__pycache__' --exclude='*.pyc' \ - -cf - . | tar -xf - -C "$RUNTIME_SRC/app" - test -f "$RUNTIME_SRC/app/agentmain.py" - test -f "$RUNTIME_SRC/app/frontends/desktop_bridge.py" - test -f "$RUNTIME_SRC/app/frontends/desktop/static/index.html" - test ! -e "$RUNTIME_SRC/app/frontends/desktop/dist" - test ! -e "$RUNTIME_SRC/app/frontends/desktop/src" - test ! -e "$RUNTIME_SRC/app/frontends/desktop/public" - test ! -e "$RUNTIME_SRC/app/frontends/desktop/package-lock.json" - test ! -e "$RUNTIME_SRC/app/frontends/desktop/node_modules" - - DMG_STAGE="artifacts/macos/dmg-stage" - DMG_APP="$DMG_STAGE/GenericAgent.app" - mkdir -p "$DMG_STAGE" - - # Standard DMG: GenericAgent.app + Applications alias. The post-build - # repackager below writes the curated two-icon Finder layout. - ditto "$APP_SRC" "$DMG_APP" - mkdir -p "$DMG_APP/Contents/Resources" - ditto "$RUNTIME_SRC" "$DMG_APP/Contents/Resources/runtime" - test -s "$DMG_APP/Contents/Resources/runtime/.prepared" - DMG_RUNTIME="$DMG_APP/Contents/Resources/runtime" - PYTHONDONTWRITEBYTECODE=1 "$DMG_RUNTIME/python/bin/python3" \ - -c 'import aiohttp, fastapi, pydantic, uvicorn, websockets' - purge_runtime_bytecode "$DMG_RUNTIME" - # Ad-hoc signing only: this is not Developer ID signing or notarization. - codesign --force --deep --sign - "$DMG_APP" - codesign --verify --deep --strict "$DMG_APP" - ln -s /Applications "$DMG_STAGE/Applications" - - mkdir -p artifacts/macos/out - hdiutil create \ - -volname "GenericAgent Desktop" \ - -srcfolder "$DMG_STAGE" \ - -ov \ - -format UDZO \ - "artifacts/macos/out/GenericAgent-Desktop-macOS-aarch64.dmg" - - # Restore the intentionally minimal Finder presentation: only the app - # and Applications shortcut, with the established window/icon layout. - bash frontends/desktop/scripts/post-dmg.sh "artifacts/macos/out/GenericAgent-Desktop-macOS-aarch64.dmg" - - ( - cd artifacts/macos/out - shasum -a 256 "GenericAgent-Desktop-macOS-aarch64.dmg" \ - > "GenericAgent-Desktop-macOS-aarch64.dmg.sha256" - ) - - echo "DMG stage tree:" - find "$DMG_STAGE" -maxdepth 2 -print | sort - - - name: Upload workflow artifact - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + [[ "$COMPONENT_URL" == https://* ]] + [[ "$COMPONENT_SHA256" =~ ^[0-9a-fA-F]{64}$ ]] + mkdir -p staging/GenericAgent out + curl --fail --location --retry 3 --proto '=https' --proto-redir '=https' \ + --output component.bin "$COMPONENT_URL" + python3 - <<'PY' + import hashlib, os + from pathlib import Path + actual = hashlib.sha256(Path('component.bin').read_bytes()).hexdigest() + assert actual == os.environ['COMPONENT_SHA256'].lower(), 'Component SHA256 mismatch' + PY + git archive HEAD | tar -x -C staging/GenericAgent + mkdir -p staging/GenericAgent/frontends + - name: Install component into source tree + shell: bash + run: | + set -euo pipefail + FRONTENDS=staging/GenericAgent/frontends + case "$PLATFORM" in + windows) + cp component.bin "$FRONTENDS/GenericAgent.exe" + ;; + macos) + # Reject archive traversal; ditto preserves app modes, links and signature. + python3 - <<'PY' + import zipfile + from pathlib import PurePosixPath + with zipfile.ZipFile('component.bin') as archive: + for name in archive.namelist(): + path = PurePosixPath(name) + assert not path.is_absolute() and '..' not in path.parts and '\\' not in name, name + assert path.parts and path.parts[0] in ('GenericAgent.app', '__MACOSX'), name + PY + ditto -x -k component.bin "$FRONTENDS" + test -d "$FRONTENDS/GenericAgent.app" + rm -rf "$FRONTENDS/__MACOSX" + codesign --verify --deep --strict "$FRONTENDS/GenericAgent.app" + ;; + linux) + cp component.bin "$FRONTENDS/GenericAgent.AppImage" + chmod +x "$FRONTENDS/GenericAgent.AppImage" + ;; + esac + python3 - <<'PY' + import json, os + from pathlib import Path + record = {'ga_commit': os.environ['GITHUB_SHA'], 'platform': os.environ['PLATFORM'], + 'component_url': os.environ['COMPONENT_URL'], 'component_sha256': os.environ['COMPONENT_SHA256'].lower()} + Path('staging/GenericAgent/DESKTOP_COMPONENT.json').write_text(json.dumps(record, indent=2) + '\n') + PY + - name: Archive and verify deployment + shell: bash + run: | + set -euo pipefail + NAME="GenericAgent-Source-$PLATFORM" + if [[ "$PLATFORM" == macos ]]; then + ditto -c -k --sequesterRsrc --keepParent staging/GenericAgent "out/$NAME.zip" + ditto -x -k "out/$NAME.zip" "$RUNNER_TEMP/deployment-verify" + codesign --verify --deep --strict "$RUNNER_TEMP/deployment-verify/GenericAgent/frontends/GenericAgent.app" + elif [[ "$PLATFORM" == windows ]]; then + (cd staging && zip -qr "../out/$NAME.zip" GenericAgent) + else + tar -czf "out/$NAME.tar.gz" -C staging GenericAgent + fi + python3 - <<'PY' + import hashlib + from pathlib import Path + for path in list(Path('out').iterdir()): + digest = hashlib.sha256(path.read_bytes()).hexdigest() + path.with_name(path.name + '.sha256').write_text(digest + ' ' + path.name + '\n') + PY + - uses: actions/upload-artifact@v4 with: - name: GenericAgent-Desktop-macOS-aarch64-DMG-${{ github.run_number }} - path: | - artifacts/macos/out/GenericAgent-Desktop-macOS-aarch64.dmg - artifacts/macos/out/GenericAgent-Desktop-macOS-aarch64.dmg.sha256 + name: ga-source-${{ inputs.platform }} + path: out/* if-no-files-found: error - - # ---------------------------------------------------------------------------- - publish-release: - name: Publish one atomic prerelease - needs: [build-windows, build-linux, build-macos] - if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/desktop-portable-') && needs.build-windows.result == 'success' && needs.build-linux.result == 'success' && needs.build-macos.result == 'success' }} - runs-on: ubuntu-24.04 - permissions: - contents: write - steps: - - name: Download Windows candidate - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 - with: - name: GenericAgent-Desktop-Windows-Portable-${{ github.run_number }} - path: ${{ runner.temp }}/release-assets - - - name: Download Linux candidate - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 - with: - name: GenericAgent-Desktop-Linux-Portable-${{ github.run_number }} - path: ${{ runner.temp }}/release-assets - - - name: Download macOS arm64 candidate - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 - with: - name: GenericAgent-Desktop-macOS-aarch64-DMG-${{ github.run_number }} - path: ${{ runner.temp }}/release-assets - - - name: Validate six assets and publish prerelease - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - TAG_NAME: ${{ github.ref_name }} - TARGET_SHA: ${{ github.sha }} - shell: bash - run: | - set -euo pipefail - - ASSET_DIR="${RUNNER_TEMP}/release-assets" - EXPECTED=( - GenericAgent-Desktop-Linux-Portable.tar.gz - GenericAgent-Desktop-Windows-Portable.zip - GenericAgent-Desktop-macOS-aarch64.dmg - GenericAgent-Desktop-macOS-aarch64.dmg.sha256 - SHA256SUMS-linux.txt - SHA256SUMS-windows.txt - ) - - if find "$ASSET_DIR" -mindepth 1 -maxdepth 1 ! -type f -print -quit | grep -q .; then - echo "Release staging contains a non-regular entry" >&2 - find "$ASSET_DIR" -mindepth 1 -maxdepth 1 -print >&2 - exit 1 - fi - mapfile -t ACTUAL < <(find "$ASSET_DIR" -mindepth 1 -maxdepth 1 -type f -printf '%f\n' | LC_ALL=C sort) - if ! diff -u <(printf '%s\n' "${EXPECTED[@]}") <(printf '%s\n' "${ACTUAL[@]}"); then - echo "Release staging must contain exactly the six expected files" >&2 - exit 1 - fi - for filename in "${EXPECTED[@]}"; do - test -s "$ASSET_DIR/$filename" || { echo "Missing or empty release asset: $filename" >&2; exit 1; } - done - - verify_checksum_manifest() { - local manifest="$1" - local payload="$2" - local normalized="${RUNNER_TEMP}/${manifest}.normalized" - tr -d '\r' < "$ASSET_DIR/$manifest" > "$normalized" - awk -v expected="$payload" ' - NF == 2 && $1 ~ /^[0-9a-fA-F]{64}$/ && $2 == expected { valid += 1 } - END { exit !(NR == 1 && valid == 1) } - ' "$normalized" - (cd "$ASSET_DIR" && sha256sum --check --strict "$normalized") - } - verify_checksum_manifest SHA256SUMS-windows.txt GenericAgent-Desktop-Windows-Portable.zip - verify_checksum_manifest SHA256SUMS-linux.txt GenericAgent-Desktop-Linux-Portable.tar.gz - verify_checksum_manifest GenericAgent-Desktop-macOS-aarch64.dmg.sha256 GenericAgent-Desktop-macOS-aarch64.dmg - - [[ "$TAG_NAME" =~ ^desktop-portable-[A-Za-z0-9._-]+$ ]] \ - || { echo "Invalid release tag: $TAG_NAME" >&2; exit 1; } - [[ "$TARGET_SHA" =~ ^[0-9a-f]{40}$ ]] \ - || { echo "Invalid release target SHA" >&2; exit 1; } - TITLE="GenericAgent Desktop ${TAG_NAME}" - NOTES_FILE="${RUNNER_TEMP}/ga-release-notes.md" - cat > "$NOTES_FILE" <<'EOF' - GenericAgent Desktop 桌面版 / Desktop - - 三个平台产物来自同一提交,并在公开发布前统一验证。All three platform artifacts come from one commit and are validated before publication. - - ## 安装方法 / Installation - - Windows x64:下载 `GenericAgent-Desktop-Windows-Portable.zip`,解压后双击 `GenericAgent.exe`。卸载时运行 `uninstall.bat`,再删除解压目录。 - Windows x64: extract `GenericAgent-Desktop-Windows-Portable.zip`, then launch `GenericAgent.exe`. Run `uninstall.bat` before deleting the extracted directory. - - Linux x86_64:下载并解压 `GenericAgent-Desktop-Linux-Portable.tar.gz`,执行 `chmod +x GenericAgent.AppImage` 后运行它。卸载时运行 `uninstall.sh`,再删除解压目录。 - Linux x86_64: extract `GenericAgent-Desktop-Linux-Portable.tar.gz`, run `chmod +x GenericAgent.AppImage`, then launch it. Run `uninstall.sh` before deleting the extracted directory. - - macOS Apple silicon (arm64):打开 `GenericAgent-Desktop-macOS-aarch64.dmg`,把 `GenericAgent.app` 拖入 Applications。 - macOS Apple silicon (arm64): open `GenericAgent-Desktop-macOS-aarch64.dmg` and drag `GenericAgent.app` to Applications. - - ## 签名状态 / Signing status - Windows 便携版尚未使用代码签名证书,SmartScreen 可能显示警告;运行前请核对 `SHA256SUMS-windows.txt`。 - The Windows portable build is not code-signed, so SmartScreen may warn; verify `SHA256SUMS-windows.txt` before running it. - - macOS 应用仅使用 ad-hoc 签名,未使用 Apple Developer ID,也未 notarize。首次打开可能需要右键选择“打开”或在“隐私与安全性”中允许。 - The macOS app is ad-hoc signed only. It is neither Developer ID signed nor notarized; first launch may require Open from the context menu or approval in Privacy & Security. - - SHA-256 清单与各产物一同发布。SHA-256 manifests are included with the artifacts. - EOF - sed -i 's/^ //' "$NOTES_FILE" - - if gh release view "$TAG_NAME" >/dev/null 2>&1; then - echo "A release already exists for $TAG_NAME; refusing to overwrite it" >&2 - exit 1 - fi - - ASSETS=() - for filename in "${EXPECTED[@]}"; do - ASSETS+=("$ASSET_DIR/$filename") - done - # Keep the release invisible while all assets upload; only the final edit publishes it. - gh release create "$TAG_NAME" "${ASSETS[@]}" \ - --target "$TARGET_SHA" \ - --title "$TITLE" \ - --notes-file "$NOTES_FILE" \ - --draft \ - --prerelease - - mapfile -t REMOTE_ASSETS < <(gh release view "$TAG_NAME" --json assets --jq '.assets[].name' | LC_ALL=C sort) - diff -u <(printf '%s\n' "${EXPECTED[@]}") <(printf '%s\n' "${REMOTE_ASSETS[@]}") - test "$(gh release view "$TAG_NAME" --json isDraft --jq '.isDraft')" = true - test "$(gh release view "$TAG_NAME" --json isPrerelease --jq '.isPrerelease')" = true - - gh release edit "$TAG_NAME" --draft=false --prerelease - test "$(gh release view "$TAG_NAME" --json isDraft --jq '.isDraft')" = false - test "$(gh release view "$TAG_NAME" --json isPrerelease --jq '.isPrerelease')" = true + retention-days: 30 diff --git a/docs/desktop_components.md b/docs/desktop_components.md new file mode 100644 index 00000000..a215c392 --- /dev/null +++ b/docs/desktop_components.md @@ -0,0 +1,33 @@ +# Desktop组件与源码部署 + +GA主仓拥有后端与整体分发,独立私有Desktop仓只构建桌面组件。两仓不使用子模块,也不互相复制源码。 + +## Desktop构建 + +在Desktop仓手动运行 **Build standalone Desktop components**,选择平台;也可使用 `desktop-component-*` 标签。 + +| 平台 | 组件产物 | 源码部署位置 | +| --- | --- | --- | +| Windows x64 | `GenericAgent.exe` | `frontends/GenericAgent.exe` | +| macOS ARM64 | `GenericAgent-Desktop-macOS-arm64.zip` | 解压为 `frontends/GenericAgent.app` | +| Linux x64 | `GenericAgent-Linux-x86_64.AppImage` | `frontends/GenericAgent.AppImage` | + +UI由Tauri内嵌,不依赖 `frontends/desktop/static/`。组件不包含GA、Python或Python依赖。启动时配置已有GA根目录和Python解释器。 + +mac组件沿用ad-hoc签名,签名校验失败会中止构建;压缩解压后再次验签。它不是Developer ID签名或Apple公证,首次启动可能需要系统设置中手动允许。当前只构建Apple Silicon,不是Intel或Universal版。保持 `.app` 完整,不修改内部文件。 + +## GA源码包组装 + +Windows现有手工/VPS发布流程不变。以下是可选的CI组装入口,不会自动发布或覆盖现有Release: + +1. 下载Desktop Actions产物,核对其中记录的Desktop commit、目标平台及SHA256。 +2. 将**内层组件文件**与校验和放到现有公开二进制分发渠道。私有仓Actions下载需要认证,且产物只保留30天;不能把Actions外层ZIP或私有下载页当作公开组件地址。 +3. 在GA仓选择需要分发的源码版本,手动运行 **Assemble GA source deployment**,填写平台、固定HTTPS组件URL、预期SHA256。 +4. 工作流验证哈希,以 `git archive HEAD` 导出当前提交,注入对应 `frontends/` 路径,生成 `GenericAgent-Source-.zip`(Linux为 `.tar.gz`)。mac最终源码ZIP解压后再次验签。 +5. 下载 `ga-source-` Actions产物。包内 `DESKTOP_COMPONENT.json` 记录GA commit、组件URL与哈希。按原发布流程分发。 + +这些是源码部署包,不是离线portable运行时或DMG安装器。不会自动下载Python,也不会自动更新用户环境;旧portable标签不再触发本工作流。 + +## 后端CI + +主仓 **GA backend contracts** 测试Python 3.10–3.13,不安装Node/Rust,也不访问Desktop私有仓。两套依赖已移除桌面源码的旧壳/打包契约不再由主仓执行。Desktop仓独立运行前端测试、类型检查、构建及产物检查。 diff --git a/docs/macos_desktop_installation_zh.md b/docs/macos_desktop_installation_zh.md index 71b13fc8..79afdb29 100644 --- a/docs/macos_desktop_installation_zh.md +++ b/docs/macos_desktop_installation_zh.md @@ -1,5 +1,7 @@ # GenericAgent 桌面版安装指南 +> 以下为旧DMG说明。新的源码部署使用独立签名 `.app`,参见 [Desktop组件与源码部署](desktop_components.md)。 + ## 📦 安装步骤 ### 第一步:打开安装包 diff --git a/frontends/tests/test_bridge_capabilities.py b/frontends/tests/test_bridge_capabilities.py index 6f2afd36..7c11ee02 100644 --- a/frontends/tests/test_bridge_capabilities.py +++ b/frontends/tests/test_bridge_capabilities.py @@ -21,7 +21,7 @@ def test_real_aiohttp_routes_expose_data_backup_capability( await client.start_server() try: inspect_head = await client.head("/memory/import/inspect") - assert inspect_head.status == 404 + assert inspect_head.status == 405 # API exists, but accepts POST only. capabilities = await client.get("/services/capabilities") assert capabilities.status == 200