Claude-code-plugins-plus-skills onenote-deploy-integration
install
source · Clone the upstream repo
git clone https://github.com/jeremylongshore/claude-code-plugins-plus-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/jeremylongshore/claude-code-plugins-plus-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/saas-packs/onenote-pack/skills/onenote-deploy-integration" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-skills-onenote-deploy-integration && rm -rf "$T"
manifest:
plugins/saas-packs/onenote-pack/skills/onenote-deploy-integration/SKILL.mdsource content
OneNote Deploy Integration
Overview
Deploying OneNote integrations into containers breaks local development assumptions: MSAL token caches vanish on restart, health checks must validate Graph API connectivity (not just HTTP 200), and graceful shutdown must flush token state. This skill provides production-ready Dockerfile, Docker Compose, and Kubernetes manifests with MSAL token persistence, health/readiness probes that verify actual Graph reachability, and SIGTERM handling.
Prerequisites
- Docker 24+ and Docker Compose v2
- Node.js 20 LTS or Python 3.11+
- Azure AD app registration with delegated permissions (
,Notes.Read
)Notes.ReadWrite - Redis (recommended for multi-replica) or persistent volume for token cache
Instructions
Dockerfile
FROM node:20-slim AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY tsconfig.json src/ ./ RUN npm run build FROM node:20-slim WORKDIR /app RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/* COPY --from=builder /app/dist ./dist COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/package.json ./ RUN mkdir -p /app/.cache/msal && chown -R node:node /app/.cache USER node ENV NODE_ENV=production MSAL_CACHE_DIR=/app/.cache/msal HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \ CMD curl -sf http://localhost:3000/health || exit 1 EXPOSE 3000 CMD ["node", "dist/index.js"]
MSAL Token Cache Persistence
File-based (single replica):
// src/auth/token-cache.ts import { readFile, writeFile, mkdir } from "fs/promises"; import { existsSync } from "fs"; import path from "path"; const CACHE_DIR = process.env.MSAL_CACHE_DIR || "/app/.cache/msal"; const CACHE_FILE = path.join(CACHE_DIR, "token-cache.json"); export async function loadCache(): Promise<string | null> { try { if (existsSync(CACHE_FILE)) return await readFile(CACHE_FILE, "utf-8"); } catch (err) { console.error("Failed to load token cache:", err); } return null; } export async function saveCache(contents: string): Promise<void> { await mkdir(CACHE_DIR, { recursive: true }); await writeFile(CACHE_FILE, contents, { mode: 0o600 }); }
Redis-based (multi-replica):
// src/auth/redis-cache.ts import { createClient, RedisClientType } from "redis"; const CACHE_KEY = "msal:onenote:token-cache"; let redis: RedisClientType; export async function initRedisCache(): Promise<void> { redis = createClient({ url: process.env.REDIS_URL || "redis://localhost:6379" }); redis.on("error", (err) => console.error("Redis cache error:", err)); await redis.connect(); } export async function loadCache(): Promise<string | null> { return redis.get(CACHE_KEY); } export async function saveCache(contents: string): Promise<void> { await redis.set(CACHE_KEY, contents, { EX: 86400 }); // 24h TTL } export async function flushAndDisconnect(): Promise<void> { await redis.quit(); }
Health Check Endpoint
Validates Graph API connectivity, not just HTTP liveness:
// src/health.ts app.get("/health", async (_req, res) => { const checks: Record<string, string> = {}; let healthy = true; try { await getGraphClient(); checks.auth = "ok"; } catch { checks.auth = "failed"; healthy = false; } try { await (await getGraphClient()).api("/me/onenote/notebooks").top(1).get(); checks.graph_api = "ok"; } catch (err: any) { checks.graph_api = err?.statusCode === 429 ? "rate_limited" : "failed"; if (err?.statusCode !== 429) healthy = false; } res.status(healthy ? 200 : 503).json({ status: healthy ? "healthy" : "unhealthy", checks }); }); app.get("/ready", async (_req, res) => { try { await getGraphClient(); res.json({ ready: true }); } catch { res.status(503).json({ ready: false }); } });
Graceful Shutdown
// src/shutdown.ts export function registerShutdownHandlers(server: any, getCacheContents: () => string): void { let shuttingDown = false; const shutdown = async (signal: string) => { if (shuttingDown) return; shuttingDown = true; console.log(`${signal} received. Flushing token cache...`); try { await saveCache(getCacheContents()); } catch (e) { console.error("Cache flush failed:", e); } server.close(() => process.exit(0)); setTimeout(() => { console.error("Forced exit after 10s"); process.exit(1); }, 10_000); }; process.on("SIGTERM", () => shutdown("SIGTERM")); process.on("SIGINT", () => shutdown("SIGINT")); }
Docker Compose (Local Development)
version: "3.9" services: onenote-service: build: . ports: ["3000:3000"] environment: - AZURE_TENANT_ID=${AZURE_TENANT_ID} - AZURE_CLIENT_ID=${AZURE_CLIENT_ID} - REDIS_URL=redis://redis:6379 depends_on: redis: { condition: service_healthy } redis: image: redis:7-alpine ports: ["6379:6379"] healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s
Kubernetes Deployment
apiVersion: apps/v1 kind: Deployment metadata: { name: onenote-integration } spec: replicas: 2 selector: { matchLabels: { app: onenote-integration } } template: metadata: { labels: { app: onenote-integration } } spec: terminationGracePeriodSeconds: 15 containers: - name: onenote image: your-registry/onenote-integration:latest ports: [{ containerPort: 3000 }] env: - { name: AZURE_TENANT_ID, valueFrom: { secretKeyRef: { name: onenote-creds, key: tenant-id } } } - { name: AZURE_CLIENT_ID, valueFrom: { secretKeyRef: { name: onenote-creds, key: client-id } } } - { name: REDIS_URL, value: "redis://redis-service:6379" } livenessProbe: httpGet: { path: /health, port: 3000 } initialDelaySeconds: 15 periodSeconds: 30 readinessProbe: httpGet: { path: /ready, port: 3000 } initialDelaySeconds: 5 resources: requests: { memory: "128Mi", cpu: "100m" } limits: { memory: "256Mi" }
Output
— multi-stage build with MSAL cache directory and health checkDockerfile
— local development stack with Redisdocker-compose.yml
— Kubernetes deployment with liveness/readiness probesk8s/deployment.yaml
— file-based MSAL cache persistencesrc/auth/token-cache.ts
— Redis-backed MSAL cache for multi-replicasrc/auth/redis-cache.ts
— health and readiness endpoints validating Graph APIsrc/health.ts
— graceful shutdown with token cache flushsrc/shutdown.ts
Error Handling
| Deploy Error | Cause | Fix |
|---|---|---|
| Token cache empty after restart | Volume not mounted or permissions wrong | Verify volume mount; on cache dir |
| Health returns 503 | Graph API unreachable or token expired | Check ; re-authenticate if cache stale |
| Redis connection refused | Redis not started or wrong URL | Verify ; check Redis container health |
on cache file | Container running as wrong user | Ensure in Dockerfile |
| Liveness probe failing | too short | Increase to 20-30s for slow token acquisition |
Examples
# Build and run locally echo "AZURE_TENANT_ID=your-tenant" >> .env echo "AZURE_CLIENT_ID=your-client" >> .env docker compose up --build curl http://localhost:3000/health # Deploy to Kubernetes kubectl create secret generic onenote-creds \ --from-literal=tenant-id=$AZURE_TENANT_ID \ --from-literal=client-id=$AZURE_CLIENT_ID kubectl apply -f k8s/deployment.yaml
Resources
Next Steps
- Set up CI pipelines with
onenote-ci-integration - Monitor rate limits in production with
onenote-rate-limits - Add performance tuning with
onenote-performance-tuning