# GitHub Client

Manage repositories, issues, pull requests, and interact with GitHub's APIs.

## Methods

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

## Usage

### Create an Issue

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

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

const IssueSchema = z.object({
  id: z.number(),
  number: z.number(),
  title: z.string(),
  state: z.enum(["open", "closed"]),
  html_url: z.string(),
  created_at: z.string(),
  user: z.object({
    login: z.string(),
    id: z.number(),
  }),
  labels: z.array(z.object({ name: z.string() })),
});

export default api({
  name: "GitHubExample",
  integrations: {
    repo: github(PROD_GITHUB),
  },
  input: z.object({
    owner: z.string(),
    repo: z.string(),
    title: z.string(),
    body: z.string(),
  }),
  output: z.object({
    issueNumber: z.number(),
    url: z.string(),
  }),
  async run(ctx, { owner, repo, title, body }) {
    const result = await ctx.integrations.repo.apiRequest(
      {
        method: "POST",
        path: `/repos/${owner}/${repo}/issues`,
        body: {
          title: title,
          body: body,
          labels: ["bug"],
        },
      },
      { response: IssueSchema },
    );

    return { issueNumber: result.number, url: result.html_url };
  },
});
```

### List Repository Issues

```typescript
const ListIssuesSchema = z.array(IssueSchema);

const issues = await ctx.integrations.repo.apiRequest(
  {
    method: "GET",
    path: `/repos/${owner}/${repo}/issues`,
    params: {
      state: "open",
      labels: "bug",
      sort: "created",
      direction: "desc",
      per_page: 30,
    },
  },
  { response: ListIssuesSchema },
);

issues.forEach((issue) => {
  console.log(`#${issue.number}: ${issue.title}`);
});
```

### Get a Repository

```typescript
const RepositorySchema = z.object({
  id: z.number(),
  name: z.string(),
  full_name: z.string(),
  description: z.string().nullable(),
  html_url: z.string(),
  stargazers_count: z.number(),
  forks_count: z.number(),
  default_branch: z.string(),
  owner: z.object({
    login: z.string(),
  }),
});

const repo = await ctx.integrations.repo.apiRequest(
  {
    method: "GET",
    path: `/repos/${owner}/${repo}`,
  },
  { response: RepositorySchema },
);

console.log(`${repo.full_name}: ${repo.stargazers_count} stars`);
```

### Create a Pull Request

```typescript
const PullRequestSchema = z.object({
  id: z.number(),
  number: z.number(),
  title: z.string(),
  state: z.enum(["open", "closed"]),
  html_url: z.string(),
  head: z.object({ ref: z.string() }),
  base: z.object({ ref: z.string() }),
  mergeable: z.boolean().nullable(),
});

const pr = await ctx.integrations.repo.apiRequest(
  {
    method: "POST",
    path: `/repos/${owner}/${repo}/pulls`,
    body: {
      title: "Add new feature",
      body: "This PR adds...",
      head: "feature-branch",
      base: "main",
    },
  },
  { response: PullRequestSchema },
);

console.log(`Created PR #${pr.number}: ${pr.html_url}`);
```

### Merge a Pull Request

```typescript
const MergeResponseSchema = z.object({
  sha: z.string(),
  merged: z.boolean(),
  message: z.string(),
});

const result = await ctx.integrations.repo.apiRequest(
  {
    method: "PUT",
    path: `/repos/${owner}/${repo}/pulls/${prNumber}/merge`,
    body: {
      commit_title: "Merge PR #123",
      merge_method: "squash", // merge, squash, or rebase
    },
  },
  { response: MergeResponseSchema },
);

if (result.merged) {
  console.log(`Merged with SHA: ${result.sha}`);
}
```

### List Commits

```typescript
const CommitSchema = z.object({
  sha: z.string(),
  commit: z.object({
    message: z.string(),
    author: z.object({
      name: z.string(),
      date: z.string(),
    }),
  }),
  html_url: z.string(),
});

const commits = await ctx.integrations.repo.apiRequest(
  {
    method: "GET",
    path: `/repos/${owner}/${repo}/commits`,
    params: {
      sha: "main",
      per_page: 10,
    },
  },
  { response: z.array(CommitSchema) },
);

commits.forEach((commit) => {
  console.log(
    `${commit.sha.slice(0, 7)}: ${commit.commit.message.split("\n")[0]}`,
  );
});
```

### Get File Contents

```typescript
const FileContentSchema = z.object({
  name: z.string(),
  path: z.string(),
  sha: z.string(),
  content: z.string(), // Base64 encoded
  encoding: z.string(),
});

const file = await ctx.integrations.repo.apiRequest(
  {
    method: "GET",
    path: `/repos/${owner}/${repo}/contents/README.md`,
    params: {
      ref: "main",
    },
  },
  { response: FileContentSchema },
);

const content = Buffer.from(file.content, "base64").toString("utf-8");
```

### Create or Update File

```typescript
const CreateFileSchema = z.object({
  content: z.object({
    sha: z.string(),
    path: z.string(),
  }),
  commit: z.object({
    sha: z.string(),
  }),
});

const result = await ctx.integrations.repo.apiRequest(
  {
    method: "PUT",
    path: `/repos/${owner}/${repo}/contents/docs/new-file.md`,
    body: {
      message: "Add new documentation",
      content: Buffer.from("# New File\n\nContent here").toString("base64"),
      branch: "main",
      // For updates, include sha of existing file:
      // sha: existingFileSha,
    },
  },
  { response: CreateFileSchema },
);
```

### Trigger a Workflow

```typescript
await ctx.integrations.repo.apiRequest(
  {
    method: "POST",
    path: `/repos/${owner}/${repo}/actions/workflows/${workflowId}/dispatches`,
    body: {
      ref: "main",
      inputs: {
        environment: "production",
        debug: "false",
      },
    },
  },
  { response: z.object({}).optional() }, // Returns 204 No Content
);
```

## 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 github.createIssue({ ... });
await github.getPullRequest({ ... });

// CORRECT - Use apiRequest
await ctx.integrations.repo.apiRequest(
  { method: "POST", path: `/repos/${owner}/${repo}/issues`, body: { ... } },
  { response: IssueSchema }
);
```

### Rate Limiting

GitHub has rate limits (5000 requests/hour for authenticated users):

```typescript
// Check rate limit status
const RateLimitSchema = z.object({
  rate: z.object({
    limit: z.number(),
    remaining: z.number(),
    reset: z.number(), // Unix timestamp
  }),
});

const limits = await ctx.integrations.repo.apiRequest(
  { method: "GET", path: "/rate_limit" },
  { response: RateLimitSchema },
);

console.log(`${limits.rate.remaining}/${limits.rate.limit} requests remaining`);
```

### Pagination

GitHub uses Link headers for pagination:

```typescript
// Use per_page and page params
const params = {
  per_page: 100, // Max 100
  page: 1,
};

// For complete results, paginate:
async function getAllIssues(github: GitHubClient, owner: string, repo: string) {
  const allIssues: Issue[] = [];
  let page = 1;

  while (true) {
    const issues = await ctx.integrations.repo.apiRequest(
      {
        method: "GET",
        path: `/repos/${owner}/${repo}/issues`,
        params: { per_page: 100, page },
      },
      { response: z.array(IssueSchema) },
    );

    allIssues.push(...issues);
    if (issues.length < 100) break;
    page++;
  }

  return allIssues;
}
```

### Accept Header for Previews

Some features require special Accept headers:

```typescript
// For draft PRs
const result = await ctx.integrations.repo.apiRequest(
  {
    method: "POST",
    path: `/repos/${owner}/${repo}/pulls`,
    body: {
      title: "Draft PR",
      head: "feature",
      base: "main",
      draft: true,
    },
    headers: {
      Accept: "application/vnd.github.v3+json",
    },
  },
  { response: PullRequestSchema },
);
```

### Empty Responses

Some endpoints return 204 No Content:

```typescript
// DELETE requests typically return empty
await ctx.integrations.repo.apiRequest(
  {
    method: "DELETE",
    path: `/repos/${owner}/${repo}/issues/${number}/labels/${label}`,
  },
  { response: z.object({}).optional() },
);
```

### Path Parameters

Repository paths are case-sensitive in the URL:

```typescript
// owner and repo from URL should match exactly
const path = `/repos/${owner}/${repo}/issues`;
// "MyOrg/My-Repo" is different from "myorg/my-repo"
```

### File Content Encoding

File contents are Base64 encoded:

```typescript
// Reading
const content = Buffer.from(file.content, "base64").toString("utf-8");

// Writing
const encoded = Buffer.from("file content").toString("base64");
```

## Error Handling

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

try {
  const result = await ctx.integrations.repo.apiRequest(
    { method: "POST", path: `/repos/${owner}/${repo}/issues`, body: { ... } },
    { response: IssueSchema }
  );
} catch (error) {
  if (error instanceof RestApiValidationError) {
    console.error("Validation failed:", error.details.zodError);
  }
}
```

## API Reference

- [GitHub REST API Documentation](https://docs.github.com/en/rest)
- [Issues](https://docs.github.com/en/rest/issues)
- [Pull Requests](https://docs.github.com/en/rest/pulls)
- [Repositories](https://docs.github.com/en/rest/repos)
- [Rate Limits](https://docs.github.com/en/rest/rate-limit)
