Claude-skill-registry evernote-ci-integration
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/evernote-ci-integration" ~/.claude/skills/majiayu000-claude-skill-registry-evernote-ci-integration && rm -rf "$T"
manifest:
skills/data/evernote-ci-integration/SKILL.mdsource content
Evernote CI Integration
Overview
Configure continuous integration pipelines for Evernote integrations, including test automation, credential management, and deployment workflows.
Prerequisites
- Git repository set up
- CI/CD platform (GitHub Actions, GitLab CI, etc.)
- Test suite implemented
- Sandbox API credentials
Instructions
Step 1: GitHub Actions Workflow
# .github/workflows/evernote-ci.yml name: Evernote Integration CI on: push: branches: [main, develop] pull_request: branches: [main] env: NODE_VERSION: '20' jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' - name: Install dependencies run: npm ci - name: Run linter run: npm run lint unit-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' - name: Install dependencies run: npm ci - name: Run unit tests run: npm run test:unit - name: Upload coverage uses: codecov/codecov-action@v3 with: files: ./coverage/lcov.info integration-tests: runs-on: ubuntu-latest needs: [lint, unit-tests] # Only run on main branch to preserve rate limits if: github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' - name: Install dependencies run: npm ci - name: Run integration tests env: EVERNOTE_CONSUMER_KEY: ${{ secrets.EVERNOTE_CONSUMER_KEY }} EVERNOTE_CONSUMER_SECRET: ${{ secrets.EVERNOTE_CONSUMER_SECRET }} EVERNOTE_ACCESS_TOKEN: ${{ secrets.EVERNOTE_SANDBOX_TOKEN }} EVERNOTE_SANDBOX: 'true' run: npm run test:integration security-scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run Snyk security scan uses: snyk/actions/node@master env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} with: args: --severity-threshold=high
Step 2: Test Configuration
// jest.config.js module.exports = { testEnvironment: 'node', testMatch: [ '**/tests/**/*.test.js' ], collectCoverageFrom: [ 'src/**/*.js', '!src/**/*.test.js' ], coverageThreshold: { global: { branches: 80, functions: 80, lines: 80, statements: 80 } }, setupFilesAfterEnv: ['./tests/setup.js'], // Separate test suites projects: [ { displayName: 'unit', testMatch: ['<rootDir>/tests/unit/**/*.test.js'] }, { displayName: 'integration', testMatch: ['<rootDir>/tests/integration/**/*.test.js'], testTimeout: 30000 } ] };
// tests/setup.js const dotenv = require('dotenv'); // Load test environment dotenv.config({ path: '.env.test' }); // Global test setup beforeAll(() => { // Verify test environment if (process.env.NODE_ENV === 'production') { throw new Error('Cannot run tests in production environment'); } // Ensure sandbox mode for Evernote tests if (process.env.EVERNOTE_SANDBOX !== 'true') { console.warn('WARNING: EVERNOTE_SANDBOX should be true for tests'); } }); // Global teardown afterAll(async () => { // Clean up test resources });
Step 3: Mock Evernote Client for Unit Tests
// tests/mocks/evernote-mock.js class MockNoteStore { constructor() { this.notebooks = []; this.notes = []; this.tags = []; } async listNotebooks() { return this.notebooks; } async createNotebook(notebook) { const created = { ...notebook, guid: `notebook-${Date.now()}`, created: Date.now(), updated: Date.now() }; this.notebooks.push(created); return created; } async listTags() { return this.tags; } async createNote(note) { // Validate ENML if (!note.content.includes('<?xml version="1.0"')) { const error = new Error('Invalid ENML'); error.errorCode = 1; error.parameter = 'Note.content'; throw error; } const created = { ...note, guid: `note-${Date.now()}`, created: Date.now(), updated: Date.now() }; this.notes.push(created); return created; } async getNote(guid, withContent, withResources) { const note = this.notes.find(n => n.guid === guid); if (!note) { const error = new Error('Note not found'); error.identifier = 'Note.guid'; error.key = guid; throw error; } return note; } async findNotesMetadata(filter, offset, maxNotes, spec) { let filtered = [...this.notes]; if (filter.words) { const words = filter.words.toLowerCase(); filtered = filtered.filter(n => n.title.toLowerCase().includes(words) || n.content.toLowerCase().includes(words) ); } return { notes: filtered.slice(offset, offset + maxNotes), totalNotes: filtered.length }; } // Reset for tests reset() { this.notebooks = []; this.notes = []; this.tags = []; } } class MockUserStore { async getUser() { return { id: 12345, username: 'testuser', email: 'test@example.com', name: 'Test User', privilege: 1, created: Date.now() - 86400000, updated: Date.now(), active: true, accounting: { uploadLimit: 62914560, uploaded: 1000000, uploadLimitEnd: Date.now() + 86400000 * 30 } }; } } class MockEvernoteClient { constructor(options = {}) { this.options = options; this._noteStore = new MockNoteStore(); this._userStore = new MockUserStore(); } getNoteStore() { return this._noteStore; } getUserStore() { return this._userStore; } // For OAuth testing getRequestToken(callbackUrl, callback) { callback(null, 'mock-oauth-token', 'mock-oauth-secret'); } getAuthorizeUrl(oauthToken) { return `https://sandbox.evernote.com/OAuth.action?oauth_token=${oauthToken}`; } getAccessToken(oauthToken, oauthTokenSecret, oauthVerifier, callback) { callback(null, 'mock-access-token', 'mock-access-secret', { edam_expires: String(Date.now() + 31536000000) }); } } module.exports = { MockEvernoteClient, MockNoteStore, MockUserStore };
Step 4: Unit Test Examples
// tests/unit/note-service.test.js const { MockEvernoteClient } = require('../mocks/evernote-mock'); const NoteService = require('../../src/services/note-service'); describe('NoteService', () => { let noteService; let mockClient; beforeEach(() => { mockClient = new MockEvernoteClient(); noteService = new NoteService(mockClient.getNoteStore()); }); describe('createNote', () => { it('should create a note with valid ENML', async () => { const result = await noteService.createNote({ title: 'Test Note', content: '<p>Test content</p>' }); expect(result.guid).toBeDefined(); expect(result.title).toBe('Test Note'); }); it('should sanitize note title', async () => { const result = await noteService.createNote({ title: 'Title\nwith\nnewlines', content: '<p>Content</p>' }); expect(result.title).not.toContain('\n'); }); it('should reject invalid ENML', async () => { // Remove ENML wrapping for this test const badService = { noteStore: mockClient.getNoteStore(), createNote: async (params) => { const note = { title: params.title, content: params.content }; return mockClient.getNoteStore().createNote(note); } }; await expect( badService.createNote({ title: 'Bad Note', content: '<p>No ENML wrapper</p>' }) ).rejects.toMatchObject({ errorCode: 1, parameter: 'Note.content' }); }); }); describe('searchNotes', () => { beforeEach(async () => { // Seed test data await noteService.createNote({ title: 'Meeting Notes', content: '<p>Q1 Review</p>' }); await noteService.createNote({ title: 'Todo List', content: '<p>Tasks</p>' }); }); it('should find notes by title', async () => { const results = await noteService.search('meeting'); expect(results.notes.length).toBe(1); expect(results.notes[0].title).toContain('Meeting'); }); }); });
Step 5: Integration Test Examples
// tests/integration/evernote-api.test.js const Evernote = require('evernote'); // Skip if no credentials const hasCredentials = process.env.EVERNOTE_ACCESS_TOKEN; (hasCredentials ? describe : describe.skip)('Evernote API Integration', () => { let client; let noteStore; let createdNoteGuids = []; beforeAll(() => { client = new Evernote.Client({ token: process.env.EVERNOTE_ACCESS_TOKEN, sandbox: process.env.EVERNOTE_SANDBOX === 'true' }); noteStore = client.getNoteStore(); }); afterAll(async () => { // Clean up created notes for (const guid of createdNoteGuids) { try { await noteStore.deleteNote(guid); } catch (error) { console.log(`Cleanup: Could not delete ${guid}`); } } }); it('should authenticate and get user info', async () => { const userStore = client.getUserStore(); const user = await userStore.getUser(); expect(user.id).toBeDefined(); expect(user.username).toBeDefined(); }); it('should list notebooks', async () => { const notebooks = await noteStore.listNotebooks(); expect(Array.isArray(notebooks)).toBe(true); expect(notebooks.length).toBeGreaterThan(0); }); it('should create and retrieve a note', async () => { const testTitle = `CI Test Note - ${Date.now()}`; const content = `<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd"> <en-note><p>Integration test content</p></en-note>`; const note = new Evernote.Types.Note(); note.title = testTitle; note.content = content; const created = await noteStore.createNote(note); createdNoteGuids.push(created.guid); expect(created.guid).toBeDefined(); expect(created.title).toBe(testTitle); // Retrieve and verify const retrieved = await noteStore.getNote(created.guid, true, false, false, false); expect(retrieved.content).toContain('Integration test content'); }); it('should handle rate limits gracefully', async () => { // This test intentionally makes multiple calls // to verify rate limit handling works const promises = []; for (let i = 0; i < 5; i++) { promises.push(noteStore.listNotebooks()); } const results = await Promise.all(promises); expect(results.every(r => Array.isArray(r))).toBe(true); }, 30000); });
Step 6: Secrets Management
# GitHub Actions Secrets Configuration # Go to Settings > Secrets and variables > Actions # Required secrets: # EVERNOTE_CONSUMER_KEY - Your API consumer key # EVERNOTE_CONSUMER_SECRET - Your API consumer secret # EVERNOTE_SANDBOX_TOKEN - Sandbox developer token for testing # Production secrets (separate environment): # EVERNOTE_PROD_CONSUMER_KEY # EVERNOTE_PROD_CONSUMER_SECRET
# Using environments for production # .github/workflows/deploy.yml jobs: deploy-production: runs-on: ubuntu-latest environment: production # Requires approval steps: - uses: actions/checkout@v4 - name: Deploy env: EVERNOTE_CONSUMER_KEY: ${{ secrets.EVERNOTE_PROD_CONSUMER_KEY }} EVERNOTE_CONSUMER_SECRET: ${{ secrets.EVERNOTE_PROD_CONSUMER_SECRET }} run: npm run deploy
Step 7: Package.json Scripts
{ "scripts": { "test": "jest", "test:unit": "jest --selectProjects unit", "test:integration": "jest --selectProjects integration --runInBand", "test:coverage": "jest --coverage", "lint": "eslint src/ tests/", "lint:fix": "eslint src/ tests/ --fix", "typecheck": "tsc --noEmit", "ci": "npm run lint && npm run typecheck && npm run test:unit", "ci:full": "npm run ci && npm run test:integration" } }
Output
- GitHub Actions workflow for Evernote CI
- Comprehensive test configuration
- Mock client for unit testing
- Integration test examples
- Secrets management setup
Best Practices
| Practice | Reason |
|---|---|
| Run unit tests on every PR | Fast feedback, no API usage |
| Limit integration tests | Preserve rate limits |
| Use sandbox for all CI | Never test against production |
| Store secrets securely | Protect API credentials |
| Clean up test data | Don't leave garbage in sandbox |
Resources
Next Steps
For deployment pipelines, see
evernote-deploy-integration.