Installs into .claude/skills of the current project.
Are you the author of Apple Notes Ci Integration?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/jeremylongshore-apple-notes-ci-integration-a6d6baec)
---
name: apple-notes-ci-integration
description: 'Run Apple Notes automation in CI on macOS runners.
Trigger: "apple notes CI".
'
allowed-tools: Read, Write, Edit, Bash(osascript:*), Grep
version: 1.6.0
license: MIT
author: Jeremy Longshore <jeremy@intentsolutions.io>
tags:
- saas
- macos
- apple-notes
- automation
compatibility: Designed for Claude Code
---
# 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`.