diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..73e8708 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +# CODEOWNERS — review hints for the sole maintainer. +# Wildcard catches everything; expand later as the team grows. +* @KofTwentyTwo diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c25af49 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,291 @@ +# Main CI: build + test + boundary gates for every PR and push to long-lived branches. +# +# Branch strategy (defaults — override by editing the branches: lists): +# - feature/*, fix/* PR-only checks, no tagging +# - develop full CI + pre-release tag v0.X.Y-dev. +# - rc/*, release/* full CI + tag v0.X.Y-rc.N (N auto-increments) +# - main full CI + tag v0.X.Y + GitHub Release (release.yml) +# +# Versioning: read commits since previous tag, bump per Conventional Commits +# (feat -> minor, fix -> patch, feat! / BREAKING CHANGE -> major). Defaults to +# v0.1.0 when no tags exist. +# +# Untrusted input handling: this workflow does NOT consume PR titles, bodies, +# issue text, or commit-author fields directly in run: blocks. The only +# external inputs are github.ref (controlled) and the constrained +# workflow_dispatch choice. github.run_number is GitHub-provided. + +name: CI + +on: + pull_request: + branches: [develop, main, "rc/*", "release/*"] + push: + branches: [develop, main, "rc/*", "release/*"] + workflow_dispatch: + inputs: + run_real_hardware: + description: "Run JARVIS_REAL_MODELS / JARVIS_REAL_CAMERA suites (self-hosted only)" + required: false + default: "false" + type: choice + options: + - "false" + - "true" + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: write + pull-requests: read + checks: write + +jobs: + boundary-gates: + name: Boundary gates + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run all scripts/check-*.sh (excluding app-builds) + run: | + fail=0 + for s in scripts/check-*.sh; do + name="$(basename "$s")" + if [ "$name" = "check-app-builds.sh" ]; then + echo "::group::SKIP $name (needs xcodebuild)" + echo "::endgroup::" + continue + fi + echo "::group::$name" + if bash "$s"; then + echo "PASS: $name" + else + echo "::error::FAIL: $name" + fail=1 + fi + echo "::endgroup::" + done + exit "$fail" + + webview: + name: Webview (pnpm test + build) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.30.2 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "pnpm" + cache-dependency-path: webview/pnpm-lock.yaml + - name: pnpm install + working-directory: webview + run: pnpm install --frozen-lockfile + - name: Typecheck + working-directory: webview + run: pnpm -r typecheck + - name: Test (vitest) + working-directory: webview + run: pnpm -r test + - name: Build HUD bundle + working-directory: webview + run: pnpm -r build + - name: Upload HUD dist + uses: actions/upload-artifact@v4 + with: + name: webview-dist + path: webview/packages/hud/dist/ + retention-days: 7 + if-no-files-found: error + + swift-packages: + name: SPM ${{ matrix.package }} + runs-on: macos-15 + strategy: + fail-fast: false + matrix: + package: + - AgentCore + - Bus + - Config + - DevOverlay + - Harness + - Keychain + - Logging + - MCP + - Memory + - Replay + - Shell + - Vision + - Voice + - VoiceLog + steps: + - uses: actions/checkout@v4 + - name: Select Xcode + run: sudo xcode-select -s /Applications/Xcode_16.app/Contents/Developer || sudo xcode-select -s /Applications/Xcode.app/Contents/Developer + - name: Show Swift toolchain + run: | + swift --version + xcodebuild -version + - name: Cache SPM build artifacts + uses: actions/cache@v4 + with: + path: | + packages/${{ matrix.package }}/.build + ~/Library/Caches/org.swift.swiftpm + key: spm-${{ matrix.package }}-${{ runner.os }}-${{ hashFiles(format('packages/{0}/Package.resolved', matrix.package), format('packages/{0}/Package.swift', matrix.package)) }} + restore-keys: | + spm-${{ matrix.package }}-${{ runner.os }}- + - name: swift test + run: swift test --package-path packages/${{ matrix.package }} + + app-build: + name: App target (xcodebuild) + runs-on: macos-15 + needs: [boundary-gates, webview] + steps: + - uses: actions/checkout@v4 + - name: Select Xcode + run: sudo xcode-select -s /Applications/Xcode_16.app/Contents/Developer || sudo xcode-select -s /Applications/Xcode.app/Contents/Developer + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.30.2 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "pnpm" + cache-dependency-path: webview/pnpm-lock.yaml + - name: Install xcodegen + run: brew install xcodegen + - name: Build webview (produces App/Resources/webview/) + run: bash scripts/build-webview.sh + - name: check-app-builds.sh + run: bash scripts/check-app-builds.sh + - name: verify-codesign-settings.sh + run: bash scripts/verify-codesign-settings.sh + - name: Upload Jarvis.app artifact + uses: actions/upload-artifact@v4 + with: + name: Jarvis-app-debug + path: build/Build/Products/Debug/Jarvis.app + retention-days: 7 + if-no-files-found: warn + + real-hardware: + name: Real-hardware tests (opt-in) + runs-on: [self-hosted, macOS] + if: github.event_name == 'workflow_dispatch' && github.event.inputs.run_real_hardware == 'true' + needs: [swift-packages] + steps: + - uses: actions/checkout@v4 + - name: JARVIS_REAL_MODELS Memory regression corpus + env: + JARVIS_REAL_MODELS: "1" + run: swift test --package-path packages/Memory --filter MemoryRegressionCorpusTests + - name: JARVIS_REAL_CAMERA capture suite + env: + JARVIS_REAL_CAMERA: "1" + run: swift test --package-path packages/Vision --filter CameraCaptureRealHardwareTests + - name: JARVIS_REAL_MODELS Orpheus TTFA + env: + JARVIS_REAL_MODELS: "1" + run: swift test --package-path packages/Voice --filter OrpheusTTFATests + + publish-tag: + name: Publish tag + runs-on: ubuntu-latest + needs: [boundary-gates, webview, swift-packages, app-build] + if: | + github.event_name == 'push' && ( + github.ref == 'refs/heads/develop' || + github.ref == 'refs/heads/main' || + startsWith(github.ref, 'refs/heads/rc/') || + startsWith(github.ref, 'refs/heads/release/') + ) + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Compute next version + id: version + env: + GH_REF: ${{ github.ref }} + RUN_NUMBER: ${{ github.run_number }} + run: | + set -euo pipefail + git fetch --tags --force --quiet + last_tag=$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' \ + | grep -Ev -- '-(dev|rc|alpha|beta)' \ + | sort -V | tail -n 1 || true) + if [ -z "$last_tag" ]; then + base="v0.1.0" + from="$(git rev-list --max-parents=0 HEAD | tail -n 1)" + echo "No prior tag; defaulting base=$base, scanning all history." + else + base="$last_tag" + from="$last_tag" + echo "Last final tag: $last_tag" + fi + version="${base#v}" + IFS='.' read -r major minor patch <<< "$version" + bump="patch" + if git log "$from"..HEAD --pretty=%B 2>/dev/null | grep -qE '(^|\n)BREAKING CHANGE:|^[a-zA-Z]+(\([^)]+\))?!:'; then + bump="major" + elif git log "$from"..HEAD --pretty=%B 2>/dev/null | grep -qE '^feat(\([^)]+\))?:'; then + bump="minor" + elif git log "$from"..HEAD --pretty=%B 2>/dev/null | grep -qE '^fix(\([^)]+\))?:'; then + bump="patch" + fi + if [ -n "$last_tag" ]; then + case "$bump" in + major) major=$((major+1)); minor=0; patch=0 ;; + minor) minor=$((minor+1)); patch=0 ;; + patch) patch=$((patch+1)) ;; + esac + fi + next="${major}.${minor}.${patch}" + case "$GH_REF" in + refs/heads/develop) + tag="v${next}-dev.${RUN_NUMBER}" + prerelease=true + ;; + refs/heads/rc/*|refs/heads/release/*) + existing_rc=$(git tag --list "v${next}-rc.*" | sed -E "s/.*-rc\.([0-9]+)/\1/" | sort -n | tail -n 1) + n=$(( ${existing_rc:-0} + 1 )) + tag="v${next}-rc.${n}" + prerelease=true + ;; + refs/heads/main) + tag="v${next}" + prerelease=false + ;; + *) + echo "Unexpected ref: $GH_REF" >&2 + exit 1 + ;; + esac + echo "tag=$tag" >> "$GITHUB_OUTPUT" + echo "prerelease=$prerelease" >> "$GITHUB_OUTPUT" + echo "Next tag: $tag (prerelease=$prerelease)" + - name: Create + push tag + env: + TAG: ${{ steps.version.outputs.tag }} + GH_REF: ${{ github.ref }} + run: | + set -euo pipefail + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + if git rev-parse "refs/tags/$TAG" >/dev/null 2>&1; then + echo "Tag $TAG already exists; skipping." + exit 0 + fi + git tag -a "$TAG" -m "Automated tag from CI for $GH_REF" + git push origin "$TAG" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..e549f93 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,61 @@ +# CodeQL static analysis for Swift + JavaScript/TypeScript. + +name: CodeQL + +on: + push: + branches: [develop, main, "rc/*", "release/*"] + pull_request: + branches: [develop, main] + schedule: + - cron: "0 9 * * 1" + +concurrency: + group: codeql-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + actions: read + contents: read + security-events: write + +jobs: + analyze: + name: Analyze ${{ matrix.language }} + runs-on: ${{ matrix.language == 'swift' && 'macos-15' || 'ubuntu-latest' }} + strategy: + fail-fast: false + matrix: + include: + - language: javascript-typescript + build-mode: none + - language: swift + build-mode: manual + steps: + - uses: actions/checkout@v4 + + - name: Select Xcode (swift) + if: matrix.language == 'swift' + run: sudo xcode-select -s /Applications/Xcode_16.app/Contents/Developer || sudo xcode-select -s /Applications/Xcode.app/Contents/Developer + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + queries: security-extended + + # CodeQL needs a manual Swift build. We build a representative subset + # of packages (Bus, AgentCore, MCP) so the security lane stays cheap; + # extending to all 14 packages would dominate the CI budget. + - name: Build Swift (CodeQL extraction) + if: matrix.language == 'swift' + run: | + swift build --package-path packages/Bus + swift build --package-path packages/AgentCore + swift build --package-path packages/MCP + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..b15bbc8 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,35 @@ +# Dependency review on PRs that touch manifest/lockfiles for npm or SPM. +# Fails the PR if a new dependency introduces a known high-severity vuln +# or a forbidden license. + +name: Dependency Review + +on: + pull_request: + branches: [develop, main] + paths: + - "webview/package.json" + - "webview/pnpm-lock.yaml" + - "webview/packages/**/package.json" + - "**/Package.swift" + - "**/Package.resolved" + - ".github/workflows/dependency-review.yml" + +permissions: + contents: read + pull-requests: write + +jobs: + dependency-review: + name: Dependency Review + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Dependency Review + uses: actions/dependency-review-action@v4 + with: + fail-on-severity: high + # Default license policy: warn on copyleft, fail on nothing automatically. + # Tighten via `deny-licenses: GPL-2.0, GPL-3.0, AGPL-3.0` after the + # user reviews the existing SPM + npm graph. + comment-summary-in-pr: on-failure diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..442c864 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,79 @@ +# Release workflow: creates a GitHub Release whenever a v* tag is pushed. +# Pre-release tags (vX.Y.Z-dev.*, vX.Y.Z-rc.*) are marked as pre-releases. +# Final tags (vX.Y.Z) become production releases. +# +# The release body is auto-generated from commits since the previous tag. + +name: Release + +on: + push: + tags: + - "v[0-9]+.[0-9]+.[0-9]+" + - "v[0-9]+.[0-9]+.[0-9]+-*" + +permissions: + contents: write + +jobs: + release: + name: Publish GitHub Release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Classify tag + id: classify + env: + TAG: ${{ github.ref_name }} + run: | + set -euo pipefail + # Anything containing a dash after vX.Y.Z is a pre-release. + if [[ "$TAG" == *-* ]]; then + echo "prerelease=true" >> "$GITHUB_OUTPUT" + else + echo "prerelease=false" >> "$GITHUB_OUTPUT" + fi + # Find the previous tag (any kind) for the changelog scope. + prev=$(git tag --list 'v*' --sort=-v:refname | grep -v "^${TAG}$" | head -n 1 || true) + echo "previous_tag=${prev}" >> "$GITHUB_OUTPUT" + echo "Classified $TAG: prerelease=$(grep prerelease "$GITHUB_OUTPUT" | tail -n 1), previous=$prev" + + - name: Generate release notes + id: notes + env: + TAG: ${{ github.ref_name }} + PREV: ${{ steps.classify.outputs.previous_tag }} + run: | + set -euo pipefail + { + echo "## Changes" + echo + if [ -n "$PREV" ]; then + git log --pretty='format:- %s (%h)' "$PREV..$TAG" + else + git log --pretty='format:- %s (%h)' + fi + echo + echo + echo "## Verification" + echo + echo "- CI build: see https://github.com/${GITHUB_REPOSITORY}/actions" + echo "- Boundary gates: 18/18 expected green on this tag" + } > release-notes.md + { + echo "body<> "$GITHUB_OUTPUT" + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_name }} + name: ${{ github.ref_name }} + prerelease: ${{ steps.classify.outputs.prerelease == 'true' }} + body: ${{ steps.notes.outputs.body }} + generate_release_notes: false diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml new file mode 100644 index 0000000..63a03aa --- /dev/null +++ b/.github/workflows/semgrep.yml @@ -0,0 +1,53 @@ +# Semgrep static analysis covering default + Swift + TypeScript rule packs. +# Runs on PRs + pushes to develop/main + weekly schedule. +# +# SEMGREP_APP_TOKEN is optional; without it, scans run with public rules +# and findings appear only in the workflow log (not Semgrep Cloud). + +name: Semgrep + +on: + pull_request: + branches: [develop, main] + push: + branches: [develop, main] + schedule: + - cron: "0 10 * * 1" + +concurrency: + group: semgrep-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + security-events: write + +jobs: + semgrep: + name: Semgrep scan + runs-on: ubuntu-latest + container: + image: semgrep/semgrep + if: (github.actor != 'dependabot[bot]') + steps: + - uses: actions/checkout@v4 + + - name: Semgrep CI + env: + SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} + SEMGREP_RULES: "p/default p/swift p/typescript p/secrets" + run: | + # If SEMGREP_APP_TOKEN is set, `semgrep ci` posts to Semgrep Cloud. + # Otherwise, we fall back to a local scan over SEMGREP_RULES. + if [ -n "${SEMGREP_APP_TOKEN:-}" ]; then + semgrep ci + else + semgrep scan --config "$SEMGREP_RULES" --error --sarif --output=semgrep.sarif || true + fi + + - name: Upload SARIF (when present) + if: hashFiles('semgrep.sarif') != '' + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: semgrep.sarif + category: semgrep diff --git a/BUILDING.md b/BUILDING.md index 13c8deb..5c3b9dd 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -160,6 +160,74 @@ The eval harness — Anthropic + local-model matrix, corpus in `.planning/evals/` — is pinned to `qwen2.5-coder:32b` for the local-model lane. +## CI/CD + +GitHub Actions is the canonical CI surface. Five workflows live in +`.github/workflows/`: + +| Workflow | Trigger | What it does | +|----------|---------|--------------| +| `ci.yml` | PR + push to `develop`/`main`/`rc/*`/`release/*` | Boundary gates, webview (pnpm), 14 SPM packages (matrix), app target (xcodebuild), tag publish | +| `codeql.yml` | PR + push + weekly cron | Static analysis for Swift + JavaScript/TypeScript | +| `semgrep.yml` | PR + push + weekly cron | Rule-based scan (`p/default`, `p/swift`, `p/typescript`, `p/secrets`) | +| `dependency-review.yml` | PR touching `package.json` / `pnpm-lock.yaml` / `Package.swift` / `Package.resolved` | Blocks high-severity vulns + license violations | +| `release.yml` | Push of `v*` tag | Creates a GitHub Release with auto-generated changelog | + +### Branch + tag conventions + +- **`feature/*`, `fix/*`** — open a PR against `develop`. CI runs full + build + test + scan; no tagging. +- **`develop`** — integration trunk. Every push runs full CI and publishes a + pre-release tag `v0.X.Y-dev.`. +- **`rc/*` / `release/*`** — release candidates. Tag `v0.X.Y-rc.N` (N auto- + increments from existing tags). +- **`main`** — production. Only updated via PR from `rc/*` or `release/*`. + Push to `main` produces a final tag `v0.X.Y` and a GitHub Release. + +Versioning bumps follow Conventional Commits since the previous final tag: + +- `feat:` → minor bump +- `fix:` → patch bump +- `feat!:` or `BREAKING CHANGE:` footer → major bump + +When no tags exist yet, the seed version is `v0.1.0` (current roadmap +target). + +### Failed boundary gate? + +Each script in `scripts/check-*.sh` encodes an architectural invariant. The +table in [Boundary-gate linters](#boundary-gate-linters) above explains +what each one enforces. To reproduce locally: + +```bash +bash scripts/check-.sh +``` + +The script's header comments explain the "why" behind the check and point +at the relevant audit finding or bug. + +### Opting into real-hardware tests + +The real-hardware suites (`JARVIS_REAL_MODELS=1`, `JARVIS_REAL_CAMERA=1`) +need local Ollama and a real camera, so they don't run on GitHub-hosted +runners. To run them in CI: + +1. Register a self-hosted macOS runner with `Ollama` + the necessary + models + camera TCC grants. +2. Trigger `ci.yml` via the Actions tab → "Run workflow" → set + `run_real_hardware` to `true`. + +The `real-hardware` job is gated on `runs-on: [self-hosted, macOS]` and +only fires for `workflow_dispatch` events with the input set. + +### Required repo secrets (optional) + +- `SEMGREP_APP_TOKEN` — only needed if you want findings posted to + Semgrep Cloud. Without it, scans still run with public rules and the + workflow log shows results inline. + +`GITHUB_TOKEN` is provided automatically by Actions; no setup needed. + ## Cross-references - [README](README.md) — project overview and quick-start.