diff --git a/.env.sample b/.env.sample
index e1d19a1..acb8908 100644
--- a/.env.sample
+++ b/.env.sample
@@ -3,6 +3,14 @@ LOGIN_USER=username
LOGIN_PASSWORD=strongpassword
DEFAULT_GITHUB_ORG=Git-Commit-Show
ONE_CLA_PER_ORG=true
+DOCS_AGENT_API_URL=http://localhost:3001
+DOCS_AGENT_API_KEY=random
+DOCS_AGENT_API_REVIEW_URL=#full url to review endpoint of docs agent e.g. http://localhost:3001/review, overrides `DOCS_AGENT_API_URL` base url config
+DOCS_AGENT_API_PRIORITIZE_URL=#full url
+DOCS_AGENT_API_EDIT_URL=
+DOCS_AGENT_API_LINK_URL=
+DOCS_AGENT_API_AUDIT_URL=
+DOCS_REPOS= #repos separated by comma
SLACK_DEFAULT_MESSAGE_CHANNEL_WEBHOOK_URL=https://hooks.slack.com/services/T05487DUMMY/B59DUMMY1U/htdsEdsdf7CNeDUMMY
GITHUB_BOT_USERS=dependabot[bot],devops-github-rudderstack
GITHUB_ORG_MEMBERS=
diff --git a/app.js b/app.js
index bab4e74..ab5fa99 100644
--- a/app.js
+++ b/app.js
@@ -13,6 +13,7 @@ import {
isMessageAfterMergeRequired,
getWebsiteAddress,
} from "./src/helpers.js";
+import DocsAgent from "./src/services/DocsAgent.js";
try {
const packageJson = await import("./package.json", {
@@ -112,6 +113,49 @@ GitHub.app.webhooks.on("pull_request.labeled", async ({ octokit, payload }) => {
const message = `:mag: <${pull_request.html_url}|#${pull_request.number}: ${pull_request.title}> by ${pull_request.user?.login}`;
await Slack.sendMessage(message);
}
+ if(label.name?.toLowerCase() === "docs review") {
+ console.log("Processing docs review for this PR");
+ try {
+ const DOCS_REPOS = process.env.DOCS_REPOS?.split(",")?.map((item) => item?.trim()) || [];
+ if(DOCS_REPOS?.length > 0 && !DOCS_REPOS.includes(repository.name)) {
+ throw new Error("Docs agent review is not available for this repository");
+ }
+ if(!DocsAgent.isConfigured()) {
+ throw new Error("Docs agent service is not configured");
+ }
+ console.log("Going to analyze the docs pages in this PR");
+ // Get PR changes
+ const prChanges = await GitHub.getPRChanges(
+ repository.owner.login,
+ repository.name,
+ pull_request.number
+ );
+ const docsFiles = prChanges.files.filter((file) => file.filename.endsWith(".md"));
+ console.log(`Found ${docsFiles.length} docs files being changed`);
+ if(docsFiles.length === 0) {
+ throw new Error("No docs files being changed in this PR");
+ }
+ for(const file of docsFiles) {
+ const content = file.content;
+ // Convert relative file path to full remote github file path using PR head commit SHA https://raw.githubusercontent.com/gitcommitshow/rudder-github-app/e14433e76d74dc680b8cf9102d39f31970e8b794/.codesandbox/tasks.json
+ const relativePath = file.filename;
+ const fullPath = `https://raw.githubusercontent.com/${repository.owner.login}/${repository.name}/${prChanges.headCommit}/${relativePath}`;
+ const webhookUrl = getWebsiteAddress() + "/api/comment";
+ DocsAgent.reviewDocs(content, fullPath, {
+ webhookUrl: webhookUrl,
+ webhookMetadata: {
+ issue_number: pull_request.number,
+ repo: repository.name,
+ owner: repository.owner.login,
+ },
+ });
+ console.log(`Successfully started docs review for ${fullPath}, results will be handled by webhook: ${webhookUrl}`);
+ }
+ console.log(`Successfully started all necessary docs reviews for PR ${repository.name} #${pull_request.number}`);
+ } catch (error) {
+ console.error(error);
+ }
+ }
} catch (error) {
if (error.response) {
console.error(
@@ -216,6 +260,9 @@ const server = http
case "POST /api/webhook":
githubWebhookRequestHandler(req, res);
break;
+ case "POST /api/comment":
+ routes.addCommentToGitHubIssueOrPR(req, res);
+ break;
case "GET /":
routes.home(req, res);
break;
diff --git a/src/routes.js b/src/routes.js
index 07c9fc5..69d9956 100644
--- a/src/routes.js
+++ b/src/routes.js
@@ -385,6 +385,40 @@ export const routes = {
res.writeHead(200, { "Content-Type": "text/html" });
res.write("Cache cleared");
},
+ async addCommentToGitHubIssueOrPR(req, res) {
+ let body = "";
+ req.on("data", (chunk) => {
+ body += chunk.toString(); // convert Buffer to string
+ });
+
+ req.on("end", async () => {
+ let bodyJson = null;
+ try {
+ bodyJson = JSON.parse(body) || {};
+ } catch (err) {
+ res.writeHead(400);
+ return res.end("Please add owner, repo, issue_number and result parameters in the request body in order to add a comment to a GitHub issue or PR");
+ }
+ const owner = sanitizeInput(bodyJson.owner);
+ const repo = sanitizeInput(bodyJson.repo);
+ const issue_number = bodyJson.issue_number;
+ const result = bodyJson.result;
+ if (!owner || !repo || !issue_number || !result) {
+ res.writeHead(400);
+ return res.end("Please add owner, repo, issue_number and result parameters in the request body in order to add a comment to a GitHub issue or PR");
+ }
+ try {
+ console.log("Adding comment to ", owner, repo, "issue/PR #",issue_number);
+ await GitHub.addCommentToIssueOrPR(owner, repo, issue_number, result);
+ res.writeHead(200);
+ res.write("Comment added to GitHub issue or PR");
+ return res.end();
+ } catch (err) {
+ res.writeHead(500);
+ return res.end("Failed to add comment to GitHub issue or PR");
+ }
+ });
+ },
// ${!Array.isArray(prs) || prs?.length < 1 ? "No contributions found! (Might be an access issue)" : prs?.map(pr => `
${pr?.user?.login} contributed a PR - ${pr?.title} [${pr?.labels?.map(label => label?.name).join('] [')}] updated ${timeAgo(pr?.updated_at)}`).join('')}
default(req, res) {
res.writeHead(404);
diff --git a/src/services/DocsAgent.js b/src/services/DocsAgent.js
new file mode 100644
index 0000000..213184e
--- /dev/null
+++ b/src/services/DocsAgent.js
@@ -0,0 +1,155 @@
+/**
+ * Service for interacting with external APIs to get next actions
+ */
+export class DocsAgent {
+ constructor() {
+ this.apiUrl = process.env.DOCS_AGENT_API_URL;
+ this.apiKey = process.env.DOCS_AGENT_API_KEY;
+ this.reviewDocsApiUrl = process.env.DOCS_AGENT_API_REVIEW_URL;
+ this.auditDocsApiUrl = process.env.DOCS_AGENT_API_AUDIT_URL;
+ this.prioritizeDocsApiUrl = process.env.DOCS_AGENT_API_PRIORITIZE_URL;
+ this.editDocsApiUrl = process.env.DOCS_AGENT_API_EDIT_URL;
+ this.linkDocsApiUrl = process.env.DOCS_AGENT_API_LINK_URL;
+ this.timeout = parseInt(process.env.DOCS_AGENT_API_TIMEOUT) || 350000; // 5+ minutes default
+ }
+
+ /**
+ * For comprehensiveness and standardization of the docs
+ * @param {*} content
+ * @param {*} filepath - complete remote file path
+ * @returns {Promise} - Comment text for the PR
+ */
+ async reviewDocs(content, filepath, webhookInfoForResults) {
+ return this.makeAPICall(this.reviewDocsApiUrl || "/review", {
+ content,
+ filepath,
+ webhookUrl: webhookInfoForResults?.webhookUrl,
+ webhookMetadata: webhookInfoForResults?.webhookMetadata,
+ });
+ }
+
+ /**
+ * For technical accuracy of the docs
+ * @param {*} content
+ * @param {*} filepath - complete remote file path
+ * @returns {Promise} - Comment text for the PR
+ */
+ async auditDocs(content, filepath, webhookInfoForResults) {
+ return this.makeAPICall(this.auditDocsApiUrl || "/audit", {
+ content,
+ filepath,
+ webhookUrl: webhookInfoForResults?.webhookUrl,
+ webhookMetadata: webhookInfoForResults?.webhookMetadata,
+ });
+ }
+
+ /**
+ * Get next actions from external API
+ * @param {Object} changes - Formatted PR changes
+ * @returns {Promise} - Comment text for the PR
+ */
+ async getAffectedDocsPages(changes, webhookInfoForResults) {
+ throw new Error("Not implemented");
+ if (!this.apiUrl || !this.apiKey) {
+ throw new Error("External API configuration missing. Please set EXTERNAL_API_URL and EXTERNAL_API_KEY environment variables.");
+ }
+
+ try {
+ const response = await this.makeAPICall("/getAffectedDocsPages", changes, webhookInfoForResults);
+ return this.validateResponse(response);
+ } catch (error) {
+ console.error("External API call failed:", error);
+ throw new Error(`Failed to get next actions: ${error.message}`);
+ }
+ }
+
+ /**
+ * Make the actual API call to the docs agent api
+ * @param {Object} requestBody - the request body as JSON
+ * @returns {Promise