# Apple Notes CI Integration

> Run Apple Notes automation in CI on macOS runners. Trigger: "apple notes CI".

- Skill: `gabrielmoreira/apple-notes-ci-integration` (Agent Skill)
- Install (CLI): `npx skillmds@latest add gabrielmoreira/apple-notes-ci-integration`
- Raw SKILL.md: https://api.skillmd.com/api/skills/gabrielmoreira/apple-notes-ci-integration/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- License: MIT
- Author: gabrielmoreira (https://skillmd.com/u/gabrielmoreira)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/gabrielmoreira/apple-notes-ci-integration

---

# Apple Notes CI Integration

## Overview

Apple Notes automation is macOS-only because it depends on the Apple Events subsystem and Notes.app. CI pipelines must use GitHub Actions macOS runners (`macos-latest` or `macos-14`). However, macOS CI runners have restricted TCC (Transparency, Consent, and Control) permissions, which means direct Notes.app automation via `osascript` will fail in CI. The standard pattern is to run unit tests against a mock JXA client in CI, and reserve real Notes.app integration tests for local macOS machines or self-hosted runners with pre-granted automation permissions.

## Prerequisites

- A Node 20+ project with deterministic dependencies and a mockable Notes client boundary.
- A macOS runner only for syntax and mock tests; do not treat a GitHub-hosted runner as eligible for real Notes access.
- A separately administered self-hosted Mac for opt-in integration tests, with an interactive user session and TCC consent granted through normal macOS or MDM controls.

## Instructions

1. Keep unit tests independent of Notes.app by injecting the mock client shown below.
2. Run `npm ci`, lint, and mocked tests on every pull request.
3. Gate any real integration job behind an explicit repository environment and a self-hosted runner label; do not run it for forks or untrusted pull requests.
4. Record the macOS version and the job's test mode (mock or integration) in the job summary so a green mock job is not misread as device coverage.

## GitHub Actions Workflow

```yaml
# .github/workflows/notes-ci.yml
name: Notes Automation CI
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  unit-tests:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: "20", cache: "npm" }
      - run: npm ci
      - name: Verify macOS version
        run: sw_vers
      - name: Lint JXA scripts
        run: |
          # Validate JavaScript syntax in all .jxa files
          for f in scripts/*.jxa; do
            node --check "$f" 2>/dev/null || echo "WARN: $f is osascript-only"
          done
      - name: Unit tests (mocked Notes client)
        run: npm test
      - name: Validate JXA templates
        run: |
          # Ensure osascript can parse (but not execute) JXA scripts
          for f in scripts/*.jxa; do
            osascript -l JavaScript -e "$(cat "$f")" 2>&1 | grep -v "Not authorized" || true
          done
```

## Mock Client for CI

```typescript
// tests/mocks/notes-client.mock.ts
export class MockAppleNotesClient {
  private notes: Array<{ id: string; title: string; body: string; folder: string }> = [];

  createNote(title: string, body: string, folder = "Notes"): string {
    const id = `mock-note-${Date.now()}-${Math.random().toString(36).slice(2)}`;
    this.notes.push({ id, title, body, folder });
    return id;
  }

  listNotes() { return [...this.notes]; }
  getNote(id: string) { return this.notes.find(n => n.id === id) || null; }
  searchNotes(q: string) { return this.notes.filter(n => n.title.includes(q) || n.body.includes(q)); }
  deleteNote(id: string) { this.notes = this.notes.filter(n => n.id !== id); }
  getFolders() { return [...new Set(this.notes.map(n => n.folder))]; }
}
```

## Self-Hosted Runner with TCC Pre-Approval

```bash
# On a self-hosted macOS runner, pre-grant automation permissions:
# 1. Open System Settings > Privacy & Security > Automation
# 2. Grant your CI user's terminal access to Notes.app
# 3. Verify with:
osascript -l JavaScript -e 'Application("Notes").defaultAccount.notes.length'

# Do not alter the TCC database with undocumented tools or disable SIP. Use a
# managed PPPC/MDM profile or grant consent interactively on the owned runner.
```

## Error Handling

| Issue | Cause | Solution |
|-------|-------|----------|
| "Not authorized to send Apple events" in CI | TCC blocks automation on CI runners | Use mock client; real tests on self-hosted runner |
| `osascript` syntax errors not caught | JXA has no standalone linter | Use `node --check` for JS syntax; parse-only validation |
| Flaky tests on `macos-latest` | Runner image updates change Notes state | Pin to `macos-14`; always use mocked client |
| Tests pass locally, fail in CI | Different macOS version or missing app | Check `sw_vers` output; ensure Notes.app exists on runner |
| Timeout waiting for Notes.app | App launch delay on cold runner | Add `open -a Notes && sleep 3` before osascript calls |

## Output

The CI lane produces a reproducible unit-test result against the mock client and a job summary identifying the runner image and test mode. A self-hosted integration lane, when enabled, additionally reports whether its pre-authorized Notes smoke test ran; it must fail closed if authorization is absent.

## Examples

For a pull request, run only the mock lane: `npm ci && npm test`. For an approved release candidate on the protected self-hosted runner, run the same mocked tests first, then invoke one non-destructive read-only Notes smoke test. Never use CI to create or delete a user note merely to prove authorization.

## Resources

- [GitHub Actions macOS Runners](https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories)
- [Mac Automation Scripting Guide](https://developer.apple.com/library/archive/documentation/LanguagesUtilities/Conceptual/MacAutomationScriptingGuide/)
- [TCC Database Reference](https://www.rainforestqa.com/blog/macos-tcc-db-deep-dive)

## Next Steps

For diagnosing CI failures, see `apple-notes-common-errors`. For production deployment of automation scripts, see `apple-notes-deploy-integration`.

