diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 03c5d5ea1..ecb768a81 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,8 +1,13 @@ -# Ouroboros CI — Three-tier cross-platform testing and release pipeline +# Ouroboros CI — Four-tier cross-platform testing and release pipeline # -# Tier 1: Every push to ouroboros / ouroboros-three-layer (code paths) → Ubuntu-only tests (~1 min) -# Tier 2: Push to ouroboros-stable / manual / tag → Full 3-OS matrix (~5 min) -# Tier 3: Tag v* → Full matrix + build artifacts + GitHub Release (~15 min) +# Tier 1 (Quick): Push to ouroboros (code paths) → Ubuntu-only tests (~1 min) +# Tier 2 (Full): Push to ouroboros-stable / manual / tag → Full 3-OS matrix (~5 min) +# Tier 2.5 (Integration): Push to main / ouroboros / ouroboros-stable / manual / tag → Real-provider tests (~2 min) +# Tier 3 (Build+Release): Tag v* → PyInstaller + GitHub Release (~15 min) +# +# Tier 2.5 requires OPENROUTER_API_KEY / OPENAI_API_KEY / ANTHROPIC_API_KEY in +# repository secrets and runs the `integration` pytest marker; locally these +# tests are excluded by `addopts = -m 'not integration'` in pyproject.toml. name: CI @@ -10,7 +15,7 @@ name: CI # This ensures tag pushes always fire (even if only VERSION/README changed). on: push: - branches: [ouroboros, ouroboros-three-layer, ouroboros-stable] + branches: [main, ouroboros, ouroboros-stable] paths: - 'ouroboros/**' - 'supervisor/**' @@ -37,15 +42,12 @@ on: jobs: # ────────────────────────────────────────────────────────────────── - # Tier 1: Quick tests on Ubuntu (every push to ouroboros / ouroboros-three-layer) + # Tier 1: Quick tests on Ubuntu (every push to ouroboros) # ────────────────────────────────────────────────────────────────── quick-test: if: | github.event_name == 'push' - && ( - github.ref == 'refs/heads/ouroboros' - || github.ref == 'refs/heads/ouroboros-three-layer' - ) + && github.ref == 'refs/heads/ouroboros' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -86,6 +88,42 @@ jobs: - name: Run tests run: python -m pytest tests/ -q --tb=short + # ────────────────────────────────────────────────────────────────── + # Tier 2.5: Integration tests against real provider APIs + # Triggered on push to main / ouroboros / ouroboros-stable, manual, + # or tag v*. Requires OPENROUTER_API_KEY / OPENAI_API_KEY / + # ANTHROPIC_API_KEY in repository secrets. The `integration` pytest + # marker (in pyproject.toml) controls inclusion via `-m integration`; + # within an included test file, missing-key skipping is done by per- + # test `@pytest.mark.skipif(not os.environ.get(KEY))` decorators (see + # tests/test_provider_integration.py). NOT a `needs:` of build/ + # release: a provider outage must not block a tagged release. + # ────────────────────────────────────────────────────────────────── + integration-test: + if: | + github.event_name == 'workflow_dispatch' + || github.ref == 'refs/heads/main' + || github.ref == 'refs/heads/ouroboros' + || github.ref == 'refs/heads/ouroboros-stable' + || startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.10' + cache: 'pip' + - name: Install dependencies + run: | + pip install -r requirements.txt + pip install pytest + - name: Run integration tests + env: + OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + run: python -m pytest tests/test_provider_integration.py -m integration -q --tb=short + # ────────────────────────────────────────────────────────────────── # Tier 3: Build & Release (tag push only) # ────────────────────────────────────────────────────────────────── @@ -138,8 +176,37 @@ jobs: artifact: zip runs-on: ${{ matrix.os }} env: - OUROBOROS_MANAGED_SOURCE_BRANCH: ouroboros-three-layer + OUROBOROS_MANAGED_SOURCE_BRANCH: ouroboros OUROBOROS_RELEASE_TAG: ${{ github.ref_name }} + # Apple signing secrets at JOB LEVEL with a per-matrix-shard guard. + # + # Step-level `if:` conditions can only read `env.*`, never `secrets.*` + # directly (GitHub Actions rejects the workflow with "Unrecognized + # named-value: 'secrets'"). See docs/DEVELOPMENT.md::"GitHub Actions: + # secrets in step-level if conditions". + # + # The `matrix.os == 'macos-latest' && ... || ''` GHA expression keeps + # the Apple signing/notarization values **scoped to the macOS shard + # only** — Linux and Windows shards (which run `build_linux.sh` and + # `build_windows.ps1` respectively, neither of which needs Apple + # creds) receive empty strings. This avoids exposing the signing + # material to non-macOS build subprocesses where it has no business + # being. When a secret is not configured even on macOS, the value + # is also empty string (not unset), and the gate `env.X != ''` + # evaluates false — the signing/notarization steps skip cleanly. + BUILD_CERTIFICATE_BASE64: ${{ matrix.os == 'macos-latest' && secrets.BUILD_CERTIFICATE_BASE64 || '' }} + P12_PASSWORD: ${{ matrix.os == 'macos-latest' && secrets.P12_PASSWORD || '' }} + KEYCHAIN_PASSWORD: ${{ matrix.os == 'macos-latest' && secrets.KEYCHAIN_PASSWORD || '' }} + APPLE_TEAM_ID: ${{ matrix.os == 'macos-latest' && secrets.APPLE_TEAM_ID || '' }} + APPLE_ID: ${{ matrix.os == 'macos-latest' && secrets.APPLE_ID || '' }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ matrix.os == 'macos-latest' && secrets.APPLE_APP_SPECIFIC_PASSWORD || '' }} + # SIGN_IDENTITY is a forks-friendly override: when a fork configures + # a Developer ID secret whose CN differs from the upstream default + # (e.g. "Developer ID Application: ()"), + # they set `SIGN_IDENTITY` as a repository secret and codesign in + # build.sh picks it up via `${SIGN_IDENTITY:-...}`. Same matrix.os + # guard so Linux/Windows shards never see it. + SIGN_IDENTITY: ${{ matrix.os == 'macos-latest' && secrets.SIGN_IDENTITY || '' }} steps: - uses: actions/checkout@v4 with: @@ -184,10 +251,49 @@ jobs: shell: pwsh run: .\scripts\download_python_standalone.ps1 - # —— macOS build —— + # —— macOS: import signing certificate (only when ALL four signing + # secrets are present at job level — see env: block above) + - name: Import Apple signing certificate + if: matrix.os == 'macos-latest' && env.BUILD_CERTIFICATE_BASE64 != '' && env.P12_PASSWORD != '' && env.KEYCHAIN_PASSWORD != '' && env.APPLE_TEAM_ID != '' + run: | + set -euo pipefail + CERTIFICATE_PATH="$RUNNER_TEMP/build_certificate.p12" + KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db" + # Always remove the .p12 on EXIT, including failure mid-import: + # `set -e` would otherwise abort before the trailing `rm -f` and + # leave the certificate blob on the runner until cleanup. The + # later `Cleanup keychain` step only handles the keychain itself. + trap 'rm -f "$CERTIFICATE_PATH"' EXIT + echo "${BUILD_CERTIFICATE_BASE64}" | base64 --decode > "$CERTIFICATE_PATH" + security create-keychain -p "${KEYCHAIN_PASSWORD}" "$KEYCHAIN_PATH" + security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" + security unlock-keychain -p "${KEYCHAIN_PASSWORD}" "$KEYCHAIN_PATH" + security import "$CERTIFICATE_PATH" -P "${P12_PASSWORD}" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH" + security list-keychain -d user -s "$KEYCHAIN_PATH" + security set-key-partition-list -S apple-tool:,apple: -k "${KEYCHAIN_PASSWORD}" "$KEYCHAIN_PATH" >/dev/null + security find-identity -v -p codesigning "$KEYCHAIN_PATH" + + # —— macOS build (signed + optionally notarized when secrets are + # present, otherwise unsigned). build.sh reads the same + # env vars from the job-level env block above. - name: Build macOS app if: matrix.os == 'macos-latest' - run: OUROBOROS_SIGN=0 bash build.sh + run: | + if [ -n "${BUILD_CERTIFICATE_BASE64:-}" ] && [ -n "${P12_PASSWORD:-}" ] && [ -n "${KEYCHAIN_PASSWORD:-}" ] && [ -n "${APPLE_TEAM_ID:-}" ]; then + echo "Signing certificate detected — building with codesign + (optional) notarization" + bash build.sh + else + echo "No signing secrets — building unsigned (OUROBOROS_SIGN=0)" + OUROBOROS_SIGN=0 bash build.sh + fi + + # —— macOS: cleanup keychain (always, even on build failure) so the + # temporary signing material never persists across runs. + - name: Cleanup keychain + if: always() && matrix.os == 'macos-latest' && env.BUILD_CERTIFICATE_BASE64 != '' + run: | + KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db" + security delete-keychain "$KEYCHAIN_PATH" || true # —— Linux build —— - name: Build Linux binary diff --git a/README.md b/README.md index 49b3d57ab..3ba5ffb74 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![macOS 12+](https://img.shields.io/badge/macOS-12%2B-black.svg)](https://github.com/joi-lab/ouroboros-desktop/releases) [![Linux](https://img.shields.io/badge/Linux-x86__64-orange.svg)](https://github.com/joi-lab/ouroboros-desktop/releases) [![Windows](https://img.shields.io/badge/Windows-x64-blue.svg)](https://github.com/joi-lab/ouroboros-desktop/releases) -[![Version 5.0.0](https://img.shields.io/badge/version-5.0.0-green.svg)](VERSION) +[![Version 5.1.0](https://img.shields.io/badge/version-5.1.0-green.svg)](VERSION) A self-modifying AI agent that writes its own code, rewrites its own mind, and evolves autonomously. Born February 16, 2026. @@ -231,6 +231,37 @@ Output: `dist/Ouroboros-.dmg` configured local Developer ID identity; set `OUROBOROS_SIGN=0` for an unsigned local release. Unsigned builds require right-click → **Open** on first launch. +#### Optional signing & notarization (env vars) + +`build.sh` honours these env overrides so the same script ships local, +shared-machine, and CI builds without forking the script: + +| Env var | Effect | +|---------|--------| +| `OUROBOROS_SIGN=0` | Skip codesigning entirely (unsigned `.app` + `.dmg`). | +| `SIGN_IDENTITY="Developer ID Application: ()"` | Override the codesign identity. Useful for forks whose Developer ID is not the upstream default. | +| `APPLE_ID`, `APPLE_TEAM_ID`, `APPLE_APP_SPECIFIC_PASSWORD` | When all three are set, after codesign the DMG is submitted to Apple via `xcrun notarytool submit ... --wait` and stapled with `xcrun stapler staple` so receivers do not need right-click → **Open**. Missing any one falls back to "signed but not notarized" (no Apple-side ticket exists). | + +**Forks: enabling signed CI builds.** The CI release flow +(`.github/workflows/ci.yml::build`) wires the build-script env vars above +from GitHub repository secrets, plus a small set of CI-only secrets that +import the Developer ID certificate into a temporary keychain on the +macOS runner. To exercise the signed-build path in a fork, configure +**all four** of the following as repository secrets (Settings → Secrets +and variables → Actions): `BUILD_CERTIFICATE_BASE64` (base64-encoded +`.p12`), `P12_PASSWORD`, `KEYCHAIN_PASSWORD` (an arbitrary passphrase +the workflow uses for its temporary keychain), and `APPLE_TEAM_ID`. Add +`APPLE_ID` + `APPLE_APP_SPECIFIC_PASSWORD` to additionally enable +notarization. If your Developer ID identity differs from the upstream +default, also set `SIGN_IDENTITY` (e.g. +`Developer ID Application: ()`). With no +Apple secrets configured the build job falls through to +`OUROBOROS_SIGN=0 bash build.sh` and ships an unsigned DMG identical to +v5.0.0 behaviour. See `docs/ARCHITECTURE.md` §8.1 and +`docs/DEVELOPMENT.md::"GitHub Actions: secrets in step-level if conditions"` +for the rationale (job-level `env:` mapping so step-level `if:` can read +`env.*`; GHA rejects `secrets.*` in step `if:`). + ### Linux (.tar.gz) ```bash @@ -413,11 +444,11 @@ Full text: [BIBLE.md](BIBLE.md) | Version | Date | Description | |---------|------|-------------| +| 5.1.0 | 2026-04-26 | **feat(chat+ci): selective port from PR #25 + chat bottom-fade layer fix.** (1) **Clipboard image paste** — `web/modules/chat.js` registers a `paste` listener on `#chat-input` that scans `e.clipboardData.items` for `image/*`, calls `getAsFile()`, wraps the blob as `File("clipboard-.")`, and stages it through the same `pendingAttachment` slot the paperclip button uses (no inline upload — the file uploads when Send/Enter fires, with the same offline-WS guard). `e.preventDefault()` runs only when an image item is matched, so non-image clipboard payloads still paste natively. (2) **Browser-level mangling disabled on the chat textarea** — `#chat-input` gains `autocorrect="off" autocapitalize="off" spellcheck="false"` so code, identifiers, and slash-commands are not silently rewritten. (3) **Chat bottom gradient moved to its own layer** — previously `#chat-input-area`'s `background: linear-gradient(...)` painted directly behind the textarea, which made the lower edge of the input visually dissolve into the dense end of the gradient. `web/style.css` now strips the gradient/mask from `#chat-input-area` (which keeps `z-index: 5`), and a dedicated sibling element `