# CircleCI Client

Trigger pipelines, manage jobs, and interact with CircleCI's CI/CD platform.

## Methods

| Method                                   | Description                                          |
| ---------------------------------------- | ---------------------------------------------------- |
| `apiRequest(options, schema, metadata?)` | Make any CircleCI API request with schema validation |

## Usage

### Trigger a Pipeline

```typescript
import { api, z, circleci } from "@superblocksteam/sdk-api";

// Integration ID from the integrations panel
const PROD_CIRCLECI = "a1b2c3d4-5678-90ab-cdef-circleci0001";

const PipelineSchema = z.object({
  id: z.string(),
  number: z.number(),
  state: z.string(),
  created_at: z.string(),
  trigger: z.object({
    type: z.string(),
  }),
  vcs: z
    .object({
      branch: z.string().optional(),
      tag: z.string().optional(),
    })
    .optional(),
});

export default api({
  name: "CircleCIExample",
  integrations: {
    circleci: circleci(PROD_CIRCLECI),
  },
  input: z.object({
    projectSlug: z.string(), // e.g., "gh/owner/repo"
    branch: z.string(),
  }),
  output: z.object({
    pipelineId: z.string(),
    number: z.number(),
  }),
  async run(ctx, { projectSlug, branch }) {
    const result = await ctx.integrations.circleci.apiRequest(
      {
        method: "POST",
        path: `/project/${projectSlug}/pipeline`,
        body: {
          branch: branch,
          parameters: {
            run_integration_tests: true,
          },
        },
      },
      { response: PipelineSchema },
    );

    return { pipelineId: result.id, number: result.number };
  },
});
```

### Get Pipeline Status

```typescript
const result = await ctx.integrations.circleci.apiRequest(
  {
    method: "GET",
    path: `/pipeline/${pipelineId}`,
  },
  { response: PipelineSchema },
);

console.log(`Pipeline #${result.number}: ${result.state}`);
```

### List Pipeline Workflows

```typescript
const WorkflowSchema = z.object({
  id: z.string(),
  name: z.string(),
  status: z.string(), // success, failed, running, etc.
  created_at: z.string(),
  stopped_at: z.string().nullable(),
  pipeline_id: z.string(),
  pipeline_number: z.number(),
});

const ListWorkflowsResponseSchema = z.object({
  items: z.array(WorkflowSchema),
  next_page_token: z.string().nullable(),
});

const result = await ctx.integrations.circleci.apiRequest(
  {
    method: "GET",
    path: `/pipeline/${pipelineId}/workflow`,
  },
  { response: ListWorkflowsResponseSchema },
);

result.items.forEach((wf) => {
  console.log(`${wf.name}: ${wf.status}`);
});
```

### Get Workflow Jobs

```typescript
const JobSchema = z.object({
  id: z.string(),
  name: z.string(),
  type: z.string(),
  status: z.string(), // success, failed, running, blocked, etc.
  started_at: z.string().nullable(),
  stopped_at: z.string().nullable(),
  job_number: z.number().optional(),
});

const ListJobsResponseSchema = z.object({
  items: z.array(JobSchema),
  next_page_token: z.string().nullable(),
});

const result = await ctx.integrations.circleci.apiRequest(
  {
    method: "GET",
    path: `/workflow/${workflowId}/job`,
  },
  { response: ListJobsResponseSchema },
);

result.items.forEach((job) => {
  console.log(`${job.name}: ${job.status}`);
});
```

### Get Job Details

```typescript
const JobDetailSchema = z.object({
  number: z.number(),
  status: z.string(),
  started_at: z.string(),
  stopped_at: z.string().nullable(),
  duration: z.number().nullable(),
  executor: z.object({
    type: z.string(),
    resource_class: z.string(),
  }),
  parallel_runs: z.array(z.object({ index: z.number(), status: z.string() })),
});

const result = await ctx.integrations.circleci.apiRequest(
  {
    method: "GET",
    path: `/project/${projectSlug}/job/${jobNumber}`,
  },
  { response: JobDetailSchema },
);
```

### Retry a Workflow

```typescript
const RetryResponseSchema = z.object({
  workflow_id: z.string(),
});

const result = await ctx.integrations.circleci.apiRequest(
  {
    method: "POST",
    path: `/workflow/${workflowId}/rerun`,
    body: {
      from_failed: true, // Only rerun failed jobs
    },
  },
  { response: RetryResponseSchema },
);

console.log(`Retrying workflow: ${result.workflow_id}`);
```

### Cancel a Workflow

```typescript
const CancelResponseSchema = z.object({
  message: z.string(),
});

const result = await ctx.integrations.circleci.apiRequest(
  {
    method: "POST",
    path: `/workflow/${workflowId}/cancel`,
  },
  { response: CancelResponseSchema },
);
```

### List Project Pipelines

```typescript
const ListPipelinesResponseSchema = z.object({
  items: z.array(PipelineSchema),
  next_page_token: z.string().nullable(),
});

const result = await ctx.integrations.circleci.apiRequest(
  {
    method: "GET",
    path: `/project/${projectSlug}/pipeline`,
    params: {
      branch: "main",
      "page-token": undefined, // For pagination
    },
  },
  { response: ListPipelinesResponseSchema },
);

result.items.forEach((pipeline) => {
  console.log(`Pipeline #${pipeline.number}: ${pipeline.state}`);
});
```

### Get Project Settings

```typescript
const ProjectSchema = z.object({
  slug: z.string(),
  name: z.string(),
  organization_name: z.string(),
  vcs_info: z.object({
    vcs_url: z.string(),
    provider: z.string(),
    default_branch: z.string(),
  }),
});

const result = await ctx.integrations.circleci.apiRequest(
  {
    method: "GET",
    path: `/project/${projectSlug}`,
  },
  { response: ProjectSchema },
);
```

### Get Job Artifacts

```typescript
const ArtifactSchema = z.object({
  path: z.string(),
  node_index: z.number(),
  url: z.string(),
});

const ListArtifactsResponseSchema = z.object({
  items: z.array(ArtifactSchema),
  next_page_token: z.string().nullable(),
});

const result = await ctx.integrations.circleci.apiRequest(
  {
    method: "GET",
    path: `/project/${projectSlug}/${jobNumber}/artifacts`,
  },
  { response: ListArtifactsResponseSchema },
);

result.items.forEach((artifact) => {
  console.log(`Artifact: ${artifact.path} -> ${artifact.url}`);
});
```

## Trace Metadata

All methods accept an optional `metadata` parameter as the last argument for diagnostics labeling. See the [root SDK README](../../../README.md#trace-metadata) for details.

## Common Pitfalls

### No Specialized Methods

```typescript
// WRONG - These methods do not exist
await circleci.triggerPipeline({ ... });
await circleci.getWorkflow({ ... });

// CORRECT - Use apiRequest
await ctx.integrations.circleci.apiRequest(
  { method: "POST", path: `/project/${slug}/pipeline`, body: { ... } },
  { response: PipelineSchema }
);
```

### Project Slug Format

Project slugs follow the pattern `{vcs}/{org}/{repo}`:

```typescript
// GitHub
const slug = "gh/my-org/my-repo";

// Bitbucket
const slug = "bb/my-workspace/my-repo";

// GitLab
const slug = "gl/my-group/my-repo";
```

### API Version in Path

The base URL already includes `/api/v2`, so do not repeat it in paths:

```typescript
// CORRECT - No version prefix (base URL already has /api/v2)
const path = `/project/${slug}/pipeline`;

// WRONG - Duplicates version from base URL
const path = `/api/v2/project/${slug}/pipeline`;
```

### Pipeline Parameters

Pipeline parameters must be defined in your config.yml:

```typescript
// In .circleci/config.yml:
// parameters:
//   run_integration_tests:
//     type: boolean
//     default: false

const body = {
  branch: "main",
  parameters: {
    run_integration_tests: true, // Must match config.yml parameter
  },
};
```

### Status Values

Different resources have different status values:

```typescript
// Pipeline states
const pipelineStates = [
  "created",
  "errored",
  "setup",
  "setup-pending",
  "pending",
  "running",
];

// Workflow statuses
const workflowStatuses = [
  "success",
  "running",
  "not_run",
  "failed",
  "error",
  "failing",
  "on_hold",
  "canceled",
  "unauthorized",
];

// Job statuses
const jobStatuses = [
  "success",
  "running",
  "not_run",
  "failed",
  "retried",
  "queued",
  "not_running",
  "infrastructure_fail",
  "timedout",
  "on_hold",
  "terminated-unknown",
  "canceled",
  "unauthorized",
  "blocked",
];
```

### Pagination

CircleCI uses `next_page_token`:

```typescript
async function getAllPipelines(circleci: CircleCIClient, projectSlug: string) {
  const allPipelines: Pipeline[] = [];
  let pageToken: string | undefined;

  do {
    const result = await ctx.integrations.circleci.apiRequest(
      {
        method: "GET",
        path: `/project/${projectSlug}/pipeline`,
        params: pageToken ? { "page-token": pageToken } : {},
      },
      { response: ListPipelinesResponseSchema },
    );

    allPipelines.push(...result.items);
    pageToken = result.next_page_token ?? undefined;
  } while (pageToken);

  return allPipelines;
}
```

### Rerun Options

When rerunning workflows:

```typescript
// Rerun all jobs
await ctx.integrations.circleci.apiRequest(
  {
    method: "POST",
    path: `/workflow/${workflowId}/rerun`,
    body: {
      from_failed: false, // Rerun everything
    },
  },
  { response: RetryResponseSchema },
);

// Rerun only failed jobs
await ctx.integrations.circleci.apiRequest(
  {
    method: "POST",
    path: `/workflow/${workflowId}/rerun`,
    body: {
      from_failed: true, // Only failed jobs
    },
  },
  { response: RetryResponseSchema },
);
```

## Error Handling

```typescript
import { RestApiValidationError } from "@superblocksteam/sdk-api";

try {
  const result = await ctx.integrations.circleci.apiRequest(
    { method: "POST", path: `/project/${slug}/pipeline`, body: { ... } },
    { response: PipelineSchema }
  );
} catch (error) {
  if (error instanceof RestApiValidationError) {
    console.error("Validation failed:", error.details.zodError);
  }
}
```

## API Reference

- [CircleCI API Documentation](https://circleci.com/docs/api/v2/index.html)
- [Pipeline](https://circleci.com/docs/api/v2/index.html#tag/Pipeline)
- [Workflow](https://circleci.com/docs/api/v2/index.html#tag/Workflow)
- [Job](https://circleci.com/docs/api/v2/index.html#tag/Job)
