Skip to content

Commit

Permalink
Smart synchronize and (couldn't help myself) more cleanup (#74)
Browse files Browse the repository at this point in the history
* Implement synchronize

* Cleanup

* Don't update if it's the same

* Don't update if it's the same

* Add logging

* Testing and cleanup
  • Loading branch information
philip-gai authored Apr 5, 2022
1 parent 06e8484 commit aba71c7
Show file tree
Hide file tree
Showing 6 changed files with 374 additions and 146 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

A 🤖  for drafting new GitHub announcements using pull requests

Do you use GitHub discussions? Do you create announcements for your open-source projects or org team posts? How do you get feedback or peer reviews on your post before creating it?
Do you use GitHub discussions? Do you create announcements for your open-source projects or team posts? How do you get feedback or peer reviews on your post before creating it?

Now you can:

Expand Down
207 changes: 102 additions & 105 deletions web-app/src/eventHandlers/pullRequestEventHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Context } from "probot";
import { DeprecatedLogger } from "probot/lib/types";
import { AppConfig } from "../models/appConfig";
import { AppSettings } from "../models/appSettings";
import { PullRequestComment } from "../models/githubModels";
import { ConfigService } from "../services/configService";
import { GitHubService, OctokitPlus } from "../services/githubService";
import { HelperService } from "../services/helperService";
Expand Down Expand Up @@ -45,9 +46,10 @@ export class PullRequestEventHandler {
return new PullRequestEventHandler(tokenService, configService);
}

public onOpened = async (context: Context<EventPayloads.WebhookPayloadPullRequest>): Promise<void> => {
public onUpdated = async (context: Context<EventPayloads.WebhookPayloadPullRequest>): Promise<void> => {
const logger = context.log;
logger.info("Handling pull_request.opened event...");

logger.info(`Handling ${context.name} event...`);

const appConfig = this._configService.appConfig;
const appGitHubService = GitHubService.buildForApp(context.octokit as unknown as OctokitPlus, logger, appConfig);
Expand All @@ -61,28 +63,39 @@ export class PullRequestEventHandler {
};

const isDefaultBranch = payload.pull_request.base.ref === payload.repository.default_branch;

if (!isDefaultBranch) {
logger.info("The PR is not targeting the default branch, will not post anything");
return;
}

if (payload.pull_request.draft) {
logger.info("This is a draft PR. Exiting.");
return;
}

const pullRequestFiles = await appGitHubService.getPullRequestFiles(pullInfo);

const filesAdded = pullRequestFiles.filter((file) => file.status === "added");

logger.debug(`Number of files added in push: ${filesAdded.length}`);

const app = await this.getAuthenticatedApp(logger, context);
const { appName, appPublicPage } = {
const app = await appGitHubService.getAuthenticatedApp();
const { appName, appPublicPage, appLogin } = {
appName: app.name,
appPublicPage: app.html_url,
appLogin: `${app.slug}[bot]`,
};
const appLinkMarkdown = `[@${appName}](${appPublicPage})`;

const pullRequestComments = await appGitHubService.getPullRequestComments({
...pullInfo,
});
const existingBotComments = pullRequestComments.filter((comment) => comment.user.login === appLogin && !comment.in_reply_to_id && comment.path);

// Example filepath: "docs/team-posts/hello-world.md"
for (const file of filesAdded) {
const filepath = file.filename;
const mostRecentBotCommentForFile = this.getMostRecentBotCommentForFile(existingBotComments, filepath, logger);
const shouldCreateDiscussionForFile = this.shouldCreateDiscussionForFile(appConfig.appSettings, filepath);
if (shouldCreateDiscussionForFile) {
try {
Expand All @@ -100,9 +113,10 @@ export class PullRequestEventHandler {
throw new Error("Markdown is missing a repo or team to post the discussion to");
}

let commentBody = `⚠️ ${appLinkMarkdown} will create a discussion using this file once this PR is merged ⚠️
\n**IMPORTANT**:
\n- ${this.approverPrefix}${authorLogin} must react (not reply) to this comment with a ${this.approvalReaction.icon}`;
let commentBody =
`⚠️ ${appLinkMarkdown} will create a discussion using this file once this PR is merged ⚠️\n\n` +
"**IMPORTANT**:\n\n" +
`- ${this.approverPrefix}${authorLogin} must react (not reply) to this comment with a ${this.approvalReaction.icon}\n`;

const userRefreshToken = await this._tokenService.getRefreshToken({
userLogin: authorLogin,
Expand All @@ -116,19 +130,30 @@ export class PullRequestEventHandler {
}
if (!userRefreshToken || isNonProd) {
const fullAuthUrl = `${appConfig.base_url}${appConfig.auth_url}`;
commentBody += `\n- @${authorLogin} must [authenticate](${fullAuthUrl}) before merging this PR`;
commentBody += `- @${authorLogin} must [authenticate](${fullAuthUrl}) before merging this PR\n`;
}
commentBody +=
"- Do not use relative links to files in your repo. Instead, use full URLs and for media drag/drop or paste the file into the markdown. The link generated for media should contain `https://user-images.githubusercontent.com`\n";

if (!mostRecentBotCommentForFile) {
await appGitHubService.createPullRequestComment({
...pullInfo,
commit_id: payload.pull_request.head.sha,
start_line: 1,
end_line: 6,
body: commentBody,
filepath: filepath,
});
} else if (mostRecentBotCommentForFile.body !== commentBody) {
// If we've already commented on this file, and our new comment has new info, update the comment
await appGitHubService.updatePullRequestComment({
...pullInfo,
comment_id: mostRecentBotCommentForFile.id,
body: commentBody,
});
} else {
logger.info("Not updating the comment because nothing has changed.");
}

commentBody += `\n- Do not use relative links to files in your repo. Instead, use full URLs and for media drag/drop or paste the file into the markdown. The link generated for media should contain \`https://user-images.githubusercontent.com\``;

await appGitHubService.createPullRequestComment({
...pullInfo,
commit_id: payload.pull_request.head.sha,
start_line: 1,
end_line: 6,
body: commentBody,
filepath: filepath,
});

// Dry run createDiscussion to ensure it will work
await this.createDiscussion(appGitHubService, logger, appConfig, {
Expand All @@ -141,18 +166,26 @@ export class PullRequestEventHandler {
} catch (error) {
const exceptionMessage = HelperService.getErrorMessage(error);
logger.warn(exceptionMessage);
const errorMessage = `${this.errorIcon} ${appLinkMarkdown} will not be able to create a discussion for this file. ${this.errorIcon}\n
Please fix the issues and recreate a new PR:
> ${exceptionMessage}
`;
await appGitHubService.createPullRequestComment({
...pullInfo,
commit_id: payload.pull_request.head.sha,
start_line: 1,
end_line: 6,
body: errorMessage,
filepath: filepath,
});
const errorMessage =
`${this.errorIcon} ${appLinkMarkdown} will not be able to create a discussion for \`${filepath}\` ${this.errorIcon}\n\n` +
`Please fix the issues and update the PR:\n\n` +
`> ${exceptionMessage}\n`;
if (mostRecentBotCommentForFile) {
await appGitHubService.updatePullRequestComment({
...pullInfo,
comment_id: mostRecentBotCommentForFile.id,
body: errorMessage,
});
} else {
await appGitHubService.createPullRequestComment({
...pullInfo,
commit_id: payload.pull_request.head.sha,
start_line: 1,
end_line: 6,
body: errorMessage,
filepath: filepath,
});
}
}
}
}
Expand Down Expand Up @@ -182,7 +215,7 @@ Please fix the issues and recreate a new PR:
}

// 1. (Shortcut) Look for comments made by the app and which files they were made on
const app = await this.getAuthenticatedApp(logger, context);
const app = await appGitHubService.getAuthenticatedApp();
const { appLogin, postFooter } = {
appLogin: `${app.slug}[bot]`,
postFooter: `\n\n> Published with ❤️&nbsp;by [${app.name}](${app.html_url})\n`,
Expand Down Expand Up @@ -241,61 +274,16 @@ Please fix the issues and recreate a new PR:
logger.info("Exiting pull_request.closed handler");
};

private async getAuthenticatedApp(
logger: DeprecatedLogger,
context: Context<EventPayloads.WebhookPayloadPullRequest>
): Promise<{
id: number;
slug?: string | undefined;
node_id: string;
owner: {
name?: string | null | undefined;
email?: string | null | undefined;
login: string;
id: number;
node_id: string;
avatar_url: string;
gravatar_id: string | null;
url: string;
html_url: string;
followers_url: string;
following_url: string;
gists_url: string;
starred_url: string;
subscriptions_url: string;
organizations_url: string;
repos_url: string;
events_url: string;
received_events_url: string;
type: string;
site_admin: boolean;
starred_at?: string | undefined;
} | null;
name: string;
description: string | null;
external_url: string;
html_url: string;
created_at: string;
updated_at: string;
permissions: {
issues?: string | undefined;
checks?: string | undefined;
metadata?: string | undefined;
contents?: string | undefined;
deployments?: string | undefined;
} & { [key: string]: string };
events: string[];
installations_count?: number | undefined;
client_id?: string | undefined;
client_secret?: string | undefined;
webhook_secret?: string | null | undefined;
pem?: string | undefined;
}> {
logger.info(`Getting authenticated app...`);
const authenticatedApp = await context.octokit.apps.getAuthenticated();
logger.trace(`authenticatedApp:\n${JSON.stringify(authenticatedApp)}`);
logger.info(`Done.`);
return authenticatedApp.data;
private getMostRecentBotCommentForFile(existingBotComments: PullRequestComment[], filepath: string, logger: DeprecatedLogger): PullRequestComment | null {
const existingCommentForFile = existingBotComments.filter((comment) => comment.path === filepath);
if (existingCommentForFile.length === 0) {
return null;
} else if (existingCommentForFile.length > 1) {
logger.warn(`Found multiple comments for ${filepath}. Taking most recent.`);
}
const mostRecentBotComment = existingCommentForFile[0];
logger.debug(`Found most recent bot comment for ${filepath}: Updated at ${mostRecentBotComment.updated_at}`);
return mostRecentBotComment;
}

private async getParsedMarkdownDiscussion(
Expand Down Expand Up @@ -336,7 +324,9 @@ Please fix the issues and recreate a new PR:
const parsedItems = await this.getParsedMarkdownDiscussion(appGitHubService, logger, options);

// Appending footer here because it's not really parsed from the markdown
parsedItems.postBody += options.postFooter;
if (options.postFooter) {
parsedItems.postBody += options.postFooter;
}

logger.debug(`Parsed Document Items: ${JSON.stringify(parsedItems)}`);
const { repo, repoOwner, team, teamOwner } = parsedItems;
Expand Down Expand Up @@ -374,11 +364,11 @@ Please fix the issues and recreate a new PR:
) {
throw new Error(`The app is not installed for the organization or user "${teamOwner}"`);
}
await this.createOrgTeamDiscussion(userGithubService, appGitHubService, logger, options, parsedItems);
await this.createTeamPost(userGithubService, appGitHubService, logger, options, parsedItems);
}
}

private async createOrgTeamDiscussion(
private async createTeamPost(
userGithubService: GitHubService,
appGitHubService: GitHubService,
logger: DeprecatedLogger,
Expand All @@ -392,11 +382,12 @@ Please fix the issues and recreate a new PR:
},
parsedItems: ParsedMarkdownDiscussion
): Promise<void> {
const newDiscussion = await userGithubService.createOrgTeamDiscussion({
if (!parsedItems.team || !parsedItems.teamOwner) throw new Error("Missing team or team owner");
const newDiscussion = await userGithubService.createTeamPost({
...options,
...parsedItems,
team: parsedItems.team!,
owner: parsedItems.teamOwner!,
team: parsedItems.team,
owner: parsedItems.teamOwner,
});
if (!options.dryRun) {
if (newDiscussion) {
Expand All @@ -411,10 +402,11 @@ Please fix the issues and recreate a new PR:
appGitHubService: GitHubService,
options: { pullInfo: PullInfo; pullRequestCommentId?: number | undefined }
): Promise<void> {
if (!options.pullRequestCommentId) throw new Error("Missing pullRequestCommentId");
await appGitHubService.createPullRequestCommentReply({
...options.pullInfo,
comment_id: options.pullRequestCommentId!,
body: `⛔️ Something went wrong. Make sure that you have installed and authorized the app on any repository or team that you would like to post to. Then recreate this PR 👍🏼`,
comment_id: options.pullRequestCommentId,
body: "⛔️ Something went wrong. Make sure that you have installed and authorized the app on any repository or team that you would like to post to. Then recreate this PR 👍🏼",
});
}

Expand All @@ -429,9 +421,10 @@ Please fix the issues and recreate a new PR:
pullRequestCommentId?: number;
}
): Promise<void> {
if (!options.parsedItems.repo || !options.parsedItems.repoOwner) throw new Error("Missing repo or repo owner");
const repoData = await appGitHubService.getRepoData({
repoName: options.parsedItems.repo!,
owner: options.parsedItems.repoOwner!,
repoName: options.parsedItems.repo,
owner: options.parsedItems.repoOwner,
});
logger.trace(`repoData: ${JSON.stringify(repoData)}`);
const discussionCategoryMatch = await this.getDiscussionCategory(appGitHubService, options.parsedItems);
Expand Down Expand Up @@ -466,33 +459,37 @@ Please fix the issues and recreate a new PR:
): Promise<void> {
logger.info("Creating success comment reply on original PR comment...");
if (!options.pullRequestCommentId) {
logger.info("Skipping creating PR success comment reply. No PR Comment ID was provided.");
logger.info("Not creating PR success comment reply. No PR Comment ID was provided.");
return;
}
await appGitHubService.createPullRequestCommentReply({
...options.pullInfo,
comment_id: options.pullRequestCommentId!,
comment_id: options.pullRequestCommentId,
body: `🎉 This ${discussionType} discussion has been posted! 🎉\n> View it here: [${discussionTitle}](${discussionUrl})`,
});
logger.info("Done.");
}

private async getDiscussionCategory(appGitHubService: GitHubService, parsedItems: ParsedMarkdownDiscussion): Promise<DiscussionCategory> {
const { repo, repoOwner, discussionCategoryName } = parsedItems;
if (!repo || !repoOwner) throw new Error("Missing repo or repo owner");
if (!discussionCategoryName) throw new Error("Missing discussion category name");

const repoDiscussionCategories = await appGitHubService.getRepoDiscussionCategories({
repo: parsedItems.repo!,
owner: parsedItems.repoOwner!,
repo: repo,
owner: repoOwner,
});
if (!repoDiscussionCategories || repoDiscussionCategories.length === 0) {
throw new Error(`Discussions are not enabled on ${parsedItems.repoOwner}/${parsedItems.repo}`);
throw new Error(`Discussions are not enabled on ${repoOwner}/${repo}`);
}
const discussionCategoryMatch = repoDiscussionCategories.find(
(node) =>
node?.name.trim().localeCompare(parsedItems.discussionCategoryName!, undefined, {
node?.name.trim().localeCompare(discussionCategoryName, undefined, {
sensitivity: "accent",
}) === 0
);
if (!discussionCategoryMatch) {
throw new Error(`Could not find discussion category "${parsedItems.discussionCategoryName} in ${parsedItems.repoOwner}/${parsedItems.repo}".`);
throw new Error(`Could not find discussion category "${discussionCategoryName} in ${repoOwner}/${repo}".`);
}
return discussionCategoryMatch;
}
Expand Down
4 changes: 2 additions & 2 deletions web-app/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ export = async (app: Probot, options: ApplicationFunctionOptions): Promise<void>
.addHealthCheckRoute();
logger.debug("Done.");

app.on("pull_request.opened", async (context) => {
app.on(["pull_request.opened", "pull_request.synchronize", "pull_request.ready_for_review", "pull_request.reopened"], async (context) => {
const pullRequestEventHandler = await PullRequestEventHandler.build(context, tokenService);
await pullRequestEventHandler.onOpened(context);
await pullRequestEventHandler.onUpdated(context);
});
app.on("pull_request.closed", async (context) => {
if (!context.payload.pull_request.merged) {
Expand Down
Loading

0 comments on commit aba71c7

Please sign in to comment.