DEV Community

Cover image for Automate Github Pull Requests With NodeJS API
Christian
Christian

Posted on

Automate Github Pull Requests With NodeJS API

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:

npm i -g @nestjs/cli
nest new github-pr-automation
cd github-pr-automation
Enter fullscreen mode Exit fullscreen mode

2. Install Axios and Other Necessary Dependencies

You need Axios to make HTTP requests:

npm install axios
npm install @nestjs/config
npm install @types/axios --save-dev
Enter fullscreen mode Exit fullscreen mode

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:

GITHUB_TOKEN=your_personal_access_token
GITHUB_OWNER=your_github_username_or_org
GITHUB_REPO=your_repository_name
Enter fullscreen mode Exit fullscreen mode

4. Create a Configuration Module

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

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 {}
Enter fullscreen mode Exit fullscreen mode

5. Create the GitHub Module

Generate a GitHub module and service:

nest generate module github
nest generate service github
Enter fullscreen mode Exit fullscreen mode

6. Implement the GitHub Service

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

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);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

7. Create a Controller to Trigger the Pull Request

Generate a GitHub controller:

nest generate controller github
Enter fullscreen mode Exit fullscreen mode

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

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

8. Testing the API

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

npm run start
Enter fullscreen mode Exit fullscreen mode

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

{
  "head": "feature-branch",
  "base": "main",
  "title": "Automated Pull Request",
  "body": "This is an automated pull request."
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

app.module.ts

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 {}
Enter fullscreen mode Exit fullscreen mode

github.module.ts

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

@Module({
  providers: [GithubService],
  controllers: [GithubController],
})
export class GithubModule {}
Enter fullscreen mode Exit fullscreen mode

github.service.ts

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);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

github.controller.ts

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

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);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

2. Update the GitHub Controller

Extend the controller to handle requests for these new methods:

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Testing the API

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

  • Create Pull Request
  POST http://localhost:3000/github/create-pull-request
  {
    "head": "feature-branch",
    "base": "main",
    "title": "Automated Pull Request",
    "body": "This is an automated pull request."
  }
Enter fullscreen mode Exit fullscreen mode
  • Approve Pull Request
  POST http://localhost:3000/github/approve-pull-request/1
Enter fullscreen mode Exit fullscreen mode
  • Close Pull Request
  PATCH http://localhost:3000/github/close-pull-request/1
Enter fullscreen mode Exit fullscreen mode
  • Comment on Pull Request
  POST http://localhost:3000/github/comment-pull-request/1
  {
    "comment": "This is a comment on the pull request."
  }
Enter fullscreen mode Exit fullscreen mode
  • Request Changes on Pull Request
  POST http://localhost:3000/github/request-changes/1
  {
    "comment": "Please make the following changes..."
  }
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)