Automate Github Pull Requests With NodeJS API
Tags: nextjs, javascript, programming, typescript
Published: 2024-07-08 19:07:42

Integrating with the GitHub API using Node.js, TypeScript, and NestJS involves several steps. Here's a detailed guide to achieve this:

### 1. Set Up a New NestJS Project

First, create a new NestJS project if you don't have one already:

```bash
npm i -g @nestjs/cli
nest new github-pr-automation
cd github-pr-automation
```

### 2. Install Axios and Other Necessary Dependencies

You need Axios to make HTTP requests:

```bash
npm install axios
npm install @nestjs/config
npm install @types/axios --save-dev
```

### 3. Configure Environment Variables

Create a `.env` file in the root of your project and add your GitHub personal access token and other necessary configurations:

```env
GITHUB_TOKEN=your_personal_access_token
GITHUB_OWNER=your_github_username_or_org
GITHUB_REPO=your_repository_name
```

### 4. Create a Configuration Module

Set up a configuration module to read environment variables. Update `app.module.ts` to import the configuration module:

```typescript
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { GithubModule } from './github/github.module';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
    }),
    GithubModule,
  ],
})
export class AppModule {}
```

### 5. Create the GitHub Module

Generate a GitHub module and service:

```bash
nest generate module github
nest generate service github
```

### 6. Implement the GitHub Service

In `src/github/github.service.ts`, implement the service to create a pull request:

```typescript
import { Injectable, HttpService } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import axios from 'axios';

@Injectable()
export class GithubService {
  private readonly githubToken: string;
  private readonly owner: string;
  private readonly repo: string;

  constructor(private configService: ConfigService) {
    this.githubToken = this.configService.get<string>('GITHUB_TOKEN');
    this.owner = this.configService.get<string>('GITHUB_OWNER');
    this.repo = this.configService.get<string>('GITHUB_REPO');
  }

  async createPullRequest(head: string, base: string, title: string, body: string): Promise<void> {
    const url = `https://api.github.com/repos/${this.owner}/${this.repo}/pulls`;

    const data = {
      title,
      head,
      base,
      body,
    };

    const headers = {
      Authorization: `token ${this.githubToken}`,
      Accept: 'application/vnd.github.v3+json',
    };

    try {
      const response = await axios.post(url, data, { headers });
      console.log('Pull request created successfully:', response.data.html_url);
    } catch (error) {
      console.error('Error creating pull request:', error.response ? error.response.data : error.message);
    }
  }
}
```

### 7. Create a Controller to Trigger the Pull Request

Generate a GitHub controller:

```bash
nest generate controller github
```

In `src/github/github.controller.ts`, implement the controller:

```typescript
import { Controller, Post, Body } from '@nestjs/common';
import { GithubService } from './github.service';

@Controller('github')
export class GithubController {
  constructor(private readonly githubService: GithubService) {}

  @Post('create-pull-request')
  async createPullRequest(
    @Body('head') head: string,
    @Body('base') base: string,
    @Body('title') title: string,
    @Body('body') body: string,
  ): Promise<void> {
    await this.githubService.createPullRequest(head, base, title, body);
  }
}
```

### 8. Testing the API

You can test the API using tools like Postman or curl. Start your NestJS application:

```bash
npm run start
```

Send a POST request to `http://localhost:3000/github/create-pull-request` with the required data:

```json
{
  "head": "feature-branch",
  "base": "main",
  "title": "Automated Pull Request",
  "body": "This is an automated pull request."
}
```

### 9. Full Code Structure

Here’s a summary of the code structure:

```
src/
├── app.module.ts
├── github/
│   ├── github.controller.ts
│   ├── github.module.ts
│   ├── github.service.ts
├── main.ts
.env
```

### `app.module.ts`

```typescript
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { GithubModule } from './github/github.module';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
    }),
    GithubModule,
  ],
})
export class AppModule {}
```

### `github.module.ts`

```typescript
import { Module } from '@nestjs/common';
import { GithubService } from './github.service';
import { GithubController } from './github.controller';

@Module({
  providers: [GithubService],
  controllers: [GithubController],
})
export class GithubModule {}
```

### `github.service.ts`

```typescript
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import axios from 'axios';

@Injectable()
export class GithubService {
  private readonly githubToken: string;
  private readonly owner: string;
  private readonly repo: string;

  constructor(private configService: ConfigService) {
    this.githubToken = this.configService.get<string>('GITHUB_TOKEN');
    this.owner = this.configService.get<string>('GITHUB_OWNER');
    this.repo = this.configService.get<string>('GITHUB_REPO');
  }

  async createPullRequest(head: string, base: string, title: string, body: string): Promise<void> {
    const url = `https://api.github.com/repos/${this.owner}/${this.repo}/pulls`;

    const data = {
      title,
      head,
      base,
      body,
    };

    const headers = {
      Authorization: `token ${this.githubToken}`,
      Accept: 'application/vnd.github.v3+json',
    };

    try {
      const response = await axios.post(url, data, { headers });
      console.log('Pull request created successfully:', response.data.html_url);
    } catch (error) {
      console.error('Error creating pull request:', error.response ? error.response.data : error.message);
    }
  }
}
```

### `github.controller.ts`

```typescript
import { Controller, Post, Body } from '@nestjs/common';
import { GithubService } from './github.service';

@Controller('github')
export class GithubController {
  constructor(private readonly githubService: GithubService) {}

  @Post('create-pull-request')
  async createPullRequest(
    @Body('head') head: string,
    @Body('base') base: string,
    @Body('title') title: string,
    @Body('body') body: string,
  ): Promise<void> {
    await this.githubService.createPullRequest(head, base, title, body);
  }
}
```

With this setup, you can automate pull requests using the GitHub API within a NestJS application written in TypeScript.


Further, here's how you can extend the existing NestJS service to include the following functions in order to make this more comprehensive. 

1. **Create Pull Requests**
2. **Approve Pull Requests**
3. **Close Pull Requests**
4. **Comment on Pull Requests**
5. **Request Changes on Pull Requests**

### 1. Extending the GitHub Service

Update the `GithubService` to include methods for each of these actions:

```typescript
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import axios from 'axios';

@Injectable()
export class GithubService {
  private readonly githubToken: string;
  private readonly owner: string;
  private readonly repo: string;

  constructor(private configService: ConfigService) {
    this.githubToken = this.configService.get<string>('GITHUB_TOKEN');
    this.owner = this.configService.get<string>('GITHUB_OWNER');
    this.repo = this.configService.get<string>('GITHUB_REPO');
  }

  private getHeaders() {
    return {
      Authorization: `token ${this.githubToken}`,
      Accept: 'application/vnd.github.v3+json',
    };
  }

  async createPullRequest(head: string, base: string, title: string, body: string): Promise<void> {
    const url = `https://api.github.com/repos/${this.owner}/${this.repo}/pulls`;

    const data = {
      title,
      head,
      base,
      body,
    };

    try {
      const response = await axios.post(url, data, { headers: this.getHeaders() });
      console.log('Pull request created successfully:', response.data.html_url);
    } catch (error) {
      console.error('Error creating pull request:', error.response ? error.response.data : error.message);
    }
  }

  async approvePullRequest(pullRequestNumber: number): Promise<void> {
    const url = `https://api.github.com/repos/${this.owner}/${this.repo}/pulls/${pullRequestNumber}/reviews`;
    const data = {
      event: 'APPROVE',
    };

    try {
      const response = await axios.post(url, data, { headers: this.getHeaders() });
      console.log('Pull request approved successfully:', response.data);
    } catch (error) {
      console.error('Error approving pull request:', error.response ? error.response.data : error.message);
    }
  }

  async closePullRequest(pullRequestNumber: number): Promise<void> {
    const url = `https://api.github.com/repos/${this.owner}/${this.repo}/pulls/${pullRequestNumber}`;

    const data = {
      state: 'closed',
    };

    try {
      const response = await axios.patch(url, data, { headers: this.getHeaders() });
      console.log('Pull request closed successfully:', response.data);
    } catch (error) {
      console.error('Error closing pull request:', error.response ? error.response.data : error.message);
    }
  }

  async commentOnPullRequest(pullRequestNumber: number, comment: string): Promise<void> {
    const url = `https://api.github.com/repos/${this.owner}/${this.repo}/issues/${pullRequestNumber}/comments`;

    const data = {
      body: comment,
    };

    try {
      const response = await axios.post(url, data, { headers: this.getHeaders() });
      console.log('Comment added successfully:', response.data);
    } catch (error) {
      console.error('Error adding comment:', error.response ? error.response.data : error.message);
    }
  }

  async requestChangesOnPullRequest(pullRequestNumber: number, comment: string): Promise<void> {
    const url = `https://api.github.com/repos/${this.owner}/${this.repo}/pulls/${pullRequestNumber}/reviews`;

    const data = {
      body: comment,
      event: 'REQUEST_CHANGES',
    };

    try {
      const response = await axios.post(url, data, { headers: this.getHeaders() });
      console.log('Requested changes successfully:', response.data);
    } catch (error) {
      console.error('Error requesting changes:', error.response ? error.response.data : error.message);
    }
  }
}
```

### 2. Update the GitHub Controller

Extend the controller to handle requests for these new methods:

```typescript
import { Controller, Post, Body, Param, Patch } from '@nestjs/common';
import { GithubService } from './github.service';

@Controller('github')
export class GithubController {
  constructor(private readonly githubService: GithubService) {}

  @Post('create-pull-request')
  async createPullRequest(
    @Body('head') head: string,
    @Body('base') base: string,
    @Body('title') title: string,
    @Body('body') body: string,
  ): Promise<void> {
    await this.githubService.createPullRequest(head, base, title, body);
  }

  @Post('approve-pull-request/:number')
  async approvePullRequest(@Param('number') pullRequestNumber: number): Promise<void> {
    await this.githubService.approvePullRequest(pullRequestNumber);
  }

  @Patch('close-pull-request/:number')
  async closePullRequest(@Param('number') pullRequestNumber: number): Promise<void> {
    await this.githubService.closePullRequest(pullRequestNumber);
  }

  @Post('comment-pull-request/:number')
  async commentOnPullRequest(
    @Param('number') pullRequestNumber: number,
    @Body('comment') comment: string,
  ): Promise<void> {
    await this.githubService.commentOnPullRequest(pullRequestNumber, comment);
  }

  @Post('request-changes/:number')
  async requestChangesOnPullRequest(
    @Param('number') pullRequestNumber: number,
    @Body('comment') comment: string,
  ): Promise<void> {
    await this.githubService.requestChangesOnPullRequest(pullRequestNumber, comment);
  }
}
```

### 3. Testing the API

You can now test the new endpoints using tools like Postman or curl.

- **Create Pull Request**
  ```json
  POST http://localhost:3000/github/create-pull-request
  {
    "head": "feature-branch",
    "base": "main",
    "title": "Automated Pull Request",
    "body": "This is an automated pull request."
  }
  ```

- **Approve Pull Request**
  ```json
  POST http://localhost:3000/github/approve-pull-request/1
  ```

- **Close Pull Request**
  ```json
  PATCH http://localhost:3000/github/close-pull-request/1
  ```

- **Comment on Pull Request**
  ```json
  POST http://localhost:3000/github/comment-pull-request/1
  {
    "comment": "This is a comment on the pull request."
  }
  ```

- **Request Changes on Pull Request**
  ```json
  POST http://localhost:3000/github/request-changes/1
  {
    "comment": "Please make the following changes..."
  }
  ```

With this setup, you can create, approve, close, comment on, and request changes on pull requests using the GitHub API within your NestJS application. What we are missing now are Jest / Vite tests in order to properly build out this system.