Together-Java/TJ-Bot

Sending PR notifications to project channels

surajkumar opened this issue · 0 comments

As a user that has a project posted in #projects I want to...

be able to link my GitHub repository to the project thread

So that...

I can receive pull request updates and stay informed about contributions.

Context

We have people posting projects within the discord server and for the ones that have active community development, to create enagagement and for continually receiving updates, we want notifications of PRs sent to their project channel.

Possible solution could be:

  1. The user setup may be a slash command e.g. /link-gh-project with the options: Repository Owner and Repository Name.
    In the backend, we can store the information [repositoryOwner, repositoryName, projectChannelId].
    A similar /unlink-gh-project to remove the association.

  2. Create a Routine that runs either every 15 minutes or every 1 hour to poll the GitHub API https://api.github.com/repo/<repositoryOwner>/<repositoryName> and send the PR information to the project channel on Discord.

The solution should ideally check the PR "created_at" field and if it is greater than the "last poll time", send information to discord.

The repository metadata obtained from /link-gh-project should be stored in the database.

The states to report on will most likely be [opened, merged, closed].

Considerations

  • When implementing the solution, consider GitHub and Discord rate limits. GitHub has a limit of 5,000 requests per hour.
  • To authenticate with GitHub a Personal Access Token (PAT) is required so ensure to handle that safely
  • Linking a project should only be performed by either staff members (community ambassadors, moderators and administrators) or the Project thread creator (OP)
  • Validating that we can access the PRs in situations such as the repo is not public.
  • Handling token errors if the PAT expires.

Out of Scope

Using the GitHub web hook functionality because of the complexity this introduces on the VPS.
Using Discord web hooks as that is not supported for forums.

Acceptance Criteria

GIVEN a user has linked their projects GitHub repository
THEN a message of new PRs and their statuses is sent to the project channel as an embed

To make life easier, here's some sample code I have written to get the contributor of this started:

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;

public class PullRequestFetcher {
    private static final Logger LOGGER = Logger.getLogger(PullRequestFetcher.class.getName());
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    private static final String GITHUB_API_URL = "https://api.github.com";
    private final String githubPersonalAccessToken;
    private final HttpClient httpClient;

    public PullRequestFetcher(String githubPersonalAccessToken) {
        this.githubPersonalAccessToken = githubPersonalAccessToken;
        this.httpClient = HttpClient.newBuilder().build();
    }

    public List<PullRequest> fetchPullRequests(String repositoryOwner, String repositoryName) {
        List<PullRequest> pullRequests = new ArrayList<>();

        String repository = repositoryOwner + "/" + repositoryName;
        String apiURL = "%s/repos/%s/pulls".formatted(GITHUB_API_URL, repository);

        LOGGER.fine("Making request to " + apiURL);

        HttpRequest httpRequest = HttpRequest.newBuilder()
                .uri(URI.create(apiURL))
                .header("Accept", "application/vnd.github+json")
                .header("X-GitHub-Api-Version", "2022-11-28")
                .header("Authorization", githubPersonalAccessToken)
                .build();
        try {
            HttpResponse<String> response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());
            int statusCode = response.statusCode();

            LOGGER.fine("Received http status " + statusCode);

            if(statusCode != 200) {
                LOGGER.warning("Unexpected HTTP status "
                        + statusCode
                        + " while fetching pull requests for "
                        + repository + "\nbody="
                        + response.body());
            } else {
                try {
                    pullRequests = OBJECT_MAPPER.readValue(response.body(), new TypeReference<>() {});
                } catch (JsonProcessingException jpe) {
                    LOGGER.severe("Failed to parse JSON " + jpe.getMessage());
                }
            }

        } catch (IOException | InterruptedException e) {
            LOGGER.severe("Failed to fetch pull request from discord for "
                    + repository
                    + ":"
                    + e.getMessage());
        }

        return pullRequests;
    }
}
@JsonIgnoreProperties(ignoreUnknown = true)
public record PullRequest(
        @JsonProperty("html_url") String htmlUrl,
        @JsonProperty("number") int number,
        @JsonProperty("state") String state,
        @JsonProperty("title") String title,
        @JsonProperty("user") User user,
        @JsonProperty("body") String body,
        @JsonProperty("created_at") String createdAt,
        @JsonProperty("draft") boolean draft
) {}
@JsonIgnoreProperties(ignoreUnknown = true)
public record User(
        @JsonProperty("name") String name,
        @JsonProperty("id") int id,
        @JsonProperty("avatar_url") String avatarUrl
) {}