Skillshub attio-ci-integration
install
source · Clone the upstream repo
git clone https://github.com/ComeOnOliver/skillshub
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/jeremylongshore/claude-code-plugins-plus-skills/attio-ci-integration" ~/.claude/skills/comeonoliver-skillshub-attio-ci-integration && rm -rf "$T"
manifest:
skills/jeremylongshore/claude-code-plugins-plus-skills/attio-ci-integration/SKILL.mdsource content
Attio CI Integration
Overview
Set up CI/CD pipelines that validate Attio integrations without burning API quota on every push. Uses MSW mocks for unit tests and gated live API tests for pre-release validation.
Prerequisites
- GitHub repository with Actions enabled
- Attio test workspace token (separate from production)
- Node.js project with vitest
Instructions
Step 1: GitHub Actions Workflow
# .github/workflows/attio-integration.yml name: Attio Integration on: push: branches: [main] pull_request: branches: [main] jobs: unit-tests: name: Unit Tests (mocked API) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: "20" cache: npm - run: npm ci - run: npm run typecheck - run: npm test -- --coverage - uses: actions/upload-artifact@v4 with: name: coverage path: coverage/ integration-tests: name: Integration Tests (live API) runs-on: ubuntu-latest # Only run on main branch pushes and manual triggers if: github.event_name == 'push' && github.ref == 'refs/heads/main' needs: unit-tests env: ATTIO_API_KEY: ${{ secrets.ATTIO_API_KEY_TEST }} ATTIO_LIVE: "1" steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: "20" cache: npm - run: npm ci - name: Verify Attio connectivity run: | STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ -H "Authorization: Bearer ${ATTIO_API_KEY}" \ https://api.attio.com/v2/objects) if [ "$STATUS" != "200" ]; then echo "Attio API unreachable (HTTP $STATUS). Skipping live tests." exit 0 fi - run: npm run test:integration timeout-minutes: 5
Step 2: Configure GitHub Secrets
# Use a dedicated test workspace token with minimal scopes gh secret set ATTIO_API_KEY_TEST --body "sk_test_workspace_token" # Optional: webhook secret for webhook handler tests gh secret set ATTIO_WEBHOOK_SECRET_TEST --body "whsec_test_secret"
Step 3: Unit Tests with MSW Mocks
// tests/unit/attio-service.test.ts import { describe, it, expect, beforeAll, afterAll, afterEach } from "vitest"; import { http, HttpResponse } from "msw"; import { setupServer } from "msw/node"; const BASE = "https://api.attio.com/v2"; const server = setupServer( http.get(`${BASE}/objects`, () => HttpResponse.json({ data: [ { api_slug: "people", singular_noun: "Person" }, { api_slug: "companies", singular_noun: "Company" }, ], }) ), http.post(`${BASE}/objects/people/records/query`, async ({ request }) => { const body = await request.json() as Record<string, unknown>; const limit = (body as any).limit || 10; return HttpResponse.json({ data: Array.from({ length: Math.min(limit as number, 3) }, (_, i) => ({ id: { object_id: "obj_people", record_id: `rec_${i}` }, values: { name: [{ full_name: `Person ${i}` }], email_addresses: [{ email_address: `person${i}@test.com` }], }, })), }); }), // Simulate rate limiting http.post(`${BASE}/objects/companies/records`, () => HttpResponse.json( { status_code: 429, type: "rate_limit_error", code: "rate_limit_exceeded", message: "Rate limited" }, { status: 429, headers: { "Retry-After": new Date(Date.now() + 1000).toUTCString() }, } ) ) ); beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); afterAll(() => server.close()); describe("Attio Service", () => { it("lists workspace objects", async () => { const res = await fetch(`${BASE}/objects`, { headers: { Authorization: "Bearer sk_test" }, }); const data = await res.json(); expect(data.data).toHaveLength(2); expect(data.data[0].api_slug).toBe("people"); }); it("handles rate limit responses", async () => { const res = await fetch(`${BASE}/objects/companies/records`, { method: "POST", headers: { Authorization: "Bearer sk_test", "Content-Type": "application/json" }, body: JSON.stringify({ data: { values: {} } }), }); expect(res.status).toBe(429); expect(res.headers.get("Retry-After")).toBeTruthy(); }); });
Step 4: Integration Tests (Live API)
// tests/integration/attio-live.test.ts import { describe, it, expect } from "vitest"; import { AttioClient } from "../../src/attio/client"; const LIVE = process.env.ATTIO_LIVE === "1" && !!process.env.ATTIO_API_KEY; const client = LIVE ? new AttioClient(process.env.ATTIO_API_KEY!) : null; describe.skipIf(!LIVE)("Attio Live API", () => { it("lists objects", async () => { const res = await client!.get<{ data: Array<{ api_slug: string }> }>("/objects"); expect(res.data.map((o) => o.api_slug)).toContain("people"); }); it("queries people with filter", async () => { const res = await client!.post<{ data: any[] }>( "/objects/people/records/query", { limit: 1 } ); expect(Array.isArray(res.data)).toBe(true); }); it("lists attributes on people object", async () => { const res = await client!.get<{ data: Array<{ api_slug: string; type: string }> }>( "/objects/people/attributes" ); const slugs = res.data.map((a) => a.api_slug); expect(slugs).toContain("name"); expect(slugs).toContain("email_addresses"); }); });
Step 5: Release Workflow with Attio Smoke Test
# .github/workflows/release.yml name: Release on: push: tags: ["v*"] jobs: release: runs-on: ubuntu-latest env: ATTIO_API_KEY: ${{ secrets.ATTIO_API_KEY_PROD }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: "20" - run: npm ci - run: npm test - name: Attio smoke test run: | curl -sf https://api.attio.com/v2/objects \ -H "Authorization: Bearer ${ATTIO_API_KEY}" \ | jq '.data | length' | xargs -I{} echo "Attio: {} objects accessible" - run: npm run build - run: npm publish env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Error Handling
| CI Issue | Cause | Solution |
|---|---|---|
| Integration tests flaky | Attio rate limits in CI | Run live tests only on main, not PRs |
| Secret not found | Missing GitHub secret | |
| Live tests timeout | Slow API or network | Add and connectivity check |
| MSW not intercepting | Version mismatch | Match MSW v2 imports () |
Resources
Next Steps
For deployment patterns, see
attio-deploy-integration.