agentsclimarketplace

Attio ci integration

Skill jeremylongshore/claude-code-plugins-plus-skills/plugins/saas-packs/attio-pack/skills/attio-ci-integration

425 plugins, 2,810 skills, 200 agents for Claude Code. Open-source marketplace at tonsofskills.com with the ccpi CLI package manager.

Install
npx -y skills add jeremylongshore/claude-code-plugins-plus-skills --skill attio-ci-integration

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

What its author says it does

Copied from the file, not written here

'Configure CI/CD pipelines for Attio integrations with GitHub Actions,

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

7.4 KB, as published. Nobody here has run it

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 IssueCauseSolution
Integration tests flakyAttio rate limits in CIRun live tests only on main, not PRs
Secret not foundMissing GitHub secretgh secret set ATTIO_API_KEY_TEST
Live tests timeoutSlow API or networkAdd timeout-minutes: 5 and connectivity check
MSW not interceptingVersion mismatchMatch MSW v2 imports (msw/node)

Resources

Next Steps

For deployment patterns, see attio-deploy-integration.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.