From 6e65c4c42fcd1496895c40b71b49ab3e860f4b31 Mon Sep 17 00:00:00 2001 From: gitcommitshow <56937085+gitcommitshow@users.noreply.github.com> Date: Sat, 20 Sep 2025 12:02:07 +0530 Subject: [PATCH 01/10] feat: docs agent api integration with review docs feature enabled for now --- .env.sample | 3 + app.js | 47 ++++++++++ src/services/DocsAgent.js | 132 ++++++++++++++++++++++++++++ test/integration/docs-agent.test.js | 25 ++++++ 4 files changed, 207 insertions(+) create mode 100644 src/services/DocsAgent.js create mode 100644 test/integration/docs-agent.test.js diff --git a/.env.sample b/.env.sample index e1d19a1..04992e2 100644 --- a/.env.sample +++ b/.env.sample @@ -3,6 +3,9 @@ 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_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..2938c18 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,52 @@ 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( + app, + 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 review = await DocsAgent.reviewDocs(content, fullPath); + if(!review) { + throw new Error("Failed to review docs file: "+ file.filename); + } + // Post the affected docs pages comment to the PR + await octokit.rest.issues.createComment({ + owner: repository.owner.login, + repo: repository.name, + issue_number: pull_request.number, + body: review, + }); + console.log(`Successfully posted docs review comment for ${fullPath}`); + } + console.log(`Successfully posted docs review comments for PR ${repository.name} #${pull_request.number}`); + } catch (error) { + console.error(error); + } + } } catch (error) { if (error.response) { console.error( diff --git a/src/services/DocsAgent.js b/src/services/DocsAgent.js new file mode 100644 index 0000000..b2d41ab --- /dev/null +++ b/src/services/DocsAgent.js @@ -0,0 +1,132 @@ +/** + * 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.timeout = parseInt(process.env.DOCS_AGENT_API_TIMEOUT) || 350000; // 5+ minutes default + } + + /** + * For comprehensiveness and standardization of the docs + * @param {*} content + * @param {*} fullPath + * @returns + */ + reviewDocs(content, fullPath) { + return this.makeAPICall("/review", { + content, + fullPath, + }); + } + + /** + * For technical accuracy of the docs + * @param {*} content + * @param {*} fullPath + * @returns + */ + auditDocs(content, fullPath) { + return this.makeAPICall("/audit", { + content, + fullPath, + }); + } + + /** + * Get next actions from external API + * @param {Object} changes - Formatted PR changes + * @returns {Promise} - Comment text for the PR + */ + async getAffectedDocsPages(changes) { + 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(changes); + 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} - API response + */ + async makeAPICall(endpoint, requestBody) { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), this.timeout); + + try { + const response = await fetch(this.apiUrl + endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.apiKey}`, + 'User-Agent': 'rudder-github-app/1.0', + }, + body: JSON.stringify(requestBody), + signal: controller.signal, + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + throw new Error(`API request failed with status ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + return data; + } catch (error) { + clearTimeout(timeoutId); + if (error.name === 'AbortError') { + throw new Error('API request timed out'); + } + throw error; + } + } + + /** + * Validate and extract comment from API response + * @param {Object} response - API response + * @returns {string} - Comment text + */ + validateResponse(response) { + if (!response) { + throw new Error("Empty response from external API"); + } + + // Expected response format: { comment: "..." } + if (response.comment && typeof response.comment === 'string') { + return response.comment; + } + + // Fallback: if response is a string, use it directly + if (typeof response === 'string') { + return response; + } + + // Fallback: if response has a message field + if (response.message && typeof response.message === 'string') { + return response.message; + } + + throw new Error("Invalid response format from external API. Expected 'comment' field."); + } + + /** + * Check if the service is properly configured + * @returns {boolean} - True if configured + */ + isConfigured() { + return !!(this.apiUrl && this.apiKey); + } +} + +export default new DocsAgent(); \ No newline at end of file diff --git a/test/integration/docs-agent.test.js b/test/integration/docs-agent.test.js new file mode 100644 index 0000000..c57612c --- /dev/null +++ b/test/integration/docs-agent.test.js @@ -0,0 +1,25 @@ +/** + * E2E tests for the docs agent related services. + */ + +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import { server } from '../../app.js'; +import DocsAgent from '../../src/services/DocsAgent.js'; + +describe('Docs Agent Services', function () { + this.timeout(40000); + + after(function () { + server.close(); + }); + + describe('reviewDocs', function () { + it('should review the docs', async function () { + const review = await DocsAgent.reviewDocs('Hello, world!', 'https://raw.githubusercontent.com/gitcommitshow/rudder-github-app/e14433e76d74dc680b8cf9102d39f31970e8b794/.codesandbox/tasks.json'); + expect(review).to.not.throw; + expect(review).to.be.a('string'); + expect(review).to.not.be.empty; + }); + }); +}); \ No newline at end of file From 4d07069bd40b9e66cbd49ffde4ce3567c60704c9 Mon Sep 17 00:00:00 2001 From: gitcommitshow <56937085+gitcommitshow@users.noreply.github.com> Date: Sat, 20 Sep 2025 12:09:24 +0530 Subject: [PATCH 02/10] fix: docs agent import issue --- app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.js b/app.js index 2938c18..4235c6d 100644 --- a/app.js +++ b/app.js @@ -13,7 +13,7 @@ import { isMessageAfterMergeRequired, getWebsiteAddress, } from "./src/helpers.js"; -import { DocsAgent } from "./src/services/DocsAgent.js"; +import DocsAgent from "./src/services/DocsAgent.js"; try { const packageJson = await import("./package.json", { From c3049e4b5c698354197cf11158a288fa6b337699 Mon Sep 17 00:00:00 2001 From: gitcommitshow <56937085+gitcommitshow@users.noreply.github.com> Date: Tue, 23 Sep 2025 14:03:00 +0530 Subject: [PATCH 03/10] fix: rename fullpath to filepath --- src/services/DocsAgent.js | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/services/DocsAgent.js b/src/services/DocsAgent.js index b2d41ab..80e2e4f 100644 --- a/src/services/DocsAgent.js +++ b/src/services/DocsAgent.js @@ -11,26 +11,26 @@ export class DocsAgent { /** * For comprehensiveness and standardization of the docs * @param {*} content - * @param {*} fullPath - * @returns + * @param {*} filepath - complete remote file path + * @returns {Promise} - Comment text for the PR */ - reviewDocs(content, fullPath) { + async reviewDocs(content, filepath) { return this.makeAPICall("/review", { content, - fullPath, + filepath, }); } /** * For technical accuracy of the docs * @param {*} content - * @param {*} fullPath - * @returns + * @param {*} filepath - complete remote file path + * @returns {Promise} - Comment text for the PR */ - auditDocs(content, fullPath) { + async auditDocs(content, filepath) { return this.makeAPICall("/audit", { content, - fullPath, + filepath, }); } @@ -46,7 +46,7 @@ export class DocsAgent { } try { - const response = await this.makeAPICall(changes); + const response = await this.makeAPICall("/getAffectedDocsPages", changes); return this.validateResponse(response); } catch (error) { console.error("External API call failed:", error); @@ -59,7 +59,7 @@ export class DocsAgent { * @param {Object} requestBody - the request body as JSON * @returns {Promise} - API response */ - async makeAPICall(endpoint, requestBody) { + async makeAPICall(endpoint, requestBody = {}) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); @@ -97,7 +97,7 @@ export class DocsAgent { * @param {Object} response - API response * @returns {string} - Comment text */ - validateResponse(response) { + validateResponse(response = {}) { if (!response) { throw new Error("Empty response from external API"); } @@ -125,6 +125,7 @@ export class DocsAgent { * @returns {boolean} - True if configured */ isConfigured() { + console.log("Checking if DocsAgent is configured", this.apiUrl, this.apiKey); return !!(this.apiUrl && this.apiKey); } } From b4ee25866567a92770cf16305879cc622ad7d345 Mon Sep 17 00:00:00 2001 From: gitcommitshow <56937085+gitcommitshow@users.noreply.github.com> Date: Fri, 26 Sep 2025 19:15:53 +0530 Subject: [PATCH 04/10] feat: make each docs agent url configurable --- .env.sample | 5 +++++ src/services/DocsAgent.js | 12 +++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.env.sample b/.env.sample index 04992e2..acb8908 100644 --- a/.env.sample +++ b/.env.sample @@ -5,6 +5,11 @@ 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 diff --git a/src/services/DocsAgent.js b/src/services/DocsAgent.js index 80e2e4f..4191e38 100644 --- a/src/services/DocsAgent.js +++ b/src/services/DocsAgent.js @@ -5,6 +5,11 @@ 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 } @@ -15,7 +20,7 @@ export class DocsAgent { * @returns {Promise} - Comment text for the PR */ async reviewDocs(content, filepath) { - return this.makeAPICall("/review", { + return this.makeAPICall(this.reviewDocsApiUrl || "/review", { content, filepath, }); @@ -28,7 +33,7 @@ export class DocsAgent { * @returns {Promise} - Comment text for the PR */ async auditDocs(content, filepath) { - return this.makeAPICall("/audit", { + return this.makeAPICall(this.auditDocsApiUrl || "/audit", { content, filepath, }); @@ -64,7 +69,8 @@ export class DocsAgent { const timeoutId = setTimeout(() => controller.abort(), this.timeout); try { - const response = await fetch(this.apiUrl + endpoint, { + const apiUrl = endpoint && endpoint.startsWith("/") ? this.apiUrl + endpoint : this[`${endpoint}`]; + const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', From cc8a6b309127ea499b6a2c74b5a14e0a904047a7 Mon Sep 17 00:00:00 2001 From: gitcommitshow <56937085+gitcommitshow@users.noreply.github.com> Date: Fri, 26 Sep 2025 19:32:05 +0530 Subject: [PATCH 05/10] fix: gh app reference --- app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.js b/app.js index 4235c6d..aa6acb7 100644 --- a/app.js +++ b/app.js @@ -126,7 +126,7 @@ GitHub.app.webhooks.on("pull_request.labeled", async ({ octokit, payload }) => { console.log("Going to analyze the docs pages in this PR"); // Get PR changes const prChanges = await GitHub.getPRChanges( - app, + GitHub.app, repository.owner.login, repository.name, pull_request.number From deda4693eaa434827858e1b406637549ad681637 Mon Sep 17 00:00:00 2001 From: gitcommitshow <56937085+gitcommitshow@users.noreply.github.com> Date: Fri, 26 Sep 2025 19:52:53 +0530 Subject: [PATCH 06/10] fix: incorrect params passed to getPRChanges --- app.js | 1 - src/services/GitHub.js | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app.js b/app.js index aa6acb7..63f7175 100644 --- a/app.js +++ b/app.js @@ -126,7 +126,6 @@ GitHub.app.webhooks.on("pull_request.labeled", async ({ octokit, payload }) => { console.log("Going to analyze the docs pages in this PR"); // Get PR changes const prChanges = await GitHub.getPRChanges( - GitHub.app, repository.owner.login, repository.name, pull_request.number diff --git a/src/services/GitHub.js b/src/services/GitHub.js index d6b4c0d..75e6ca6 100644 --- a/src/services/GitHub.js +++ b/src/services/GitHub.js @@ -82,12 +82,15 @@ class GitHub { * @returns */ async getOctokitForOrg(org) { + if(typeof org !== "string") { + throw new Error("Unexpected org type passed to getOctokitForOrg: " + typeof org); + } if (!this.app) { throw new Error("GitHub App is not iniitalized or authenticated"); } // Find the installation for the organization for await (const { installation } of this.app?.eachInstallation?.iterator()) { - if (installation.account.login.toLowerCase() === org.toLowerCase()) { + if (installation.account.login.toLowerCase() === org?.toLowerCase()) { // Create an authenticated client for this installation const octokit = await this.app.getInstallationOctokit(installation.id); return octokit; From ee54d43f250a1980f25ee5cfcb0c56b346585b19 Mon Sep 17 00:00:00 2001 From: gitcommitshow <56937085+gitcommitshow@users.noreply.github.com> Date: Fri, 26 Sep 2025 20:06:55 +0530 Subject: [PATCH 07/10] fix: api url construction logic bug --- src/services/DocsAgent.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/services/DocsAgent.js b/src/services/DocsAgent.js index 4191e38..9e57565 100644 --- a/src/services/DocsAgent.js +++ b/src/services/DocsAgent.js @@ -69,7 +69,7 @@ export class DocsAgent { const timeoutId = setTimeout(() => controller.abort(), this.timeout); try { - const apiUrl = endpoint && endpoint.startsWith("/") ? this.apiUrl + endpoint : this[`${endpoint}`]; + const apiUrl = (endpoint && endpoint.startsWith("/")) ? (this.apiUrl + endpoint) : endpoint; const response = await fetch(apiUrl, { method: 'POST', headers: { @@ -87,7 +87,19 @@ export class DocsAgent { throw new Error(`API request failed with status ${response.status}: ${response.statusText}`); } - const data = await response.json(); + let data; + const contentType = response.headers.get('content-type'); + if (contentType && contentType.includes('application/json')) { + data = await response.json(); + } else { + // Try to parse as JSON, but fallback to text if not possible + const text = await response.text(); + try { + data = JSON.parse(text); + } catch (e) { + data = text; + } + } return data; } catch (error) { clearTimeout(timeoutId); From fb6e1dd988088fcbd697cbaa2b16084ffb17f033 Mon Sep 17 00:00:00 2001 From: gitcommitshow <56937085+gitcommitshow@users.noreply.github.com> Date: Sat, 27 Sep 2025 13:39:36 +0530 Subject: [PATCH 08/10] feat: support webhook delivery for docs agent calls --- app.js | 23 ++++++++++--------- src/routes.js | 34 ++++++++++++++++++++++++++++ src/services/DocsAgent.js | 12 ++++++---- src/services/GitHub.js | 20 ++++++++++++++++- test/e2e/api.test.js | 35 +++++++++++++++++++++++++++++ test/integration/docs-agent.test.js | 22 +++++++++++++++++- 6 files changed, 129 insertions(+), 17 deletions(-) create mode 100644 test/e2e/api.test.js diff --git a/app.js b/app.js index 63f7175..57dcc34 100644 --- a/app.js +++ b/app.js @@ -140,18 +140,16 @@ GitHub.app.webhooks.on("pull_request.labeled", async ({ octokit, payload }) => { // 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 review = await DocsAgent.reviewDocs(content, fullPath); - if(!review) { - throw new Error("Failed to review docs file: "+ file.filename); - } - // Post the affected docs pages comment to the PR - await octokit.rest.issues.createComment({ - owner: repository.owner.login, - repo: repository.name, - issue_number: pull_request.number, - body: review, + 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 posted docs review comment for ${fullPath}`); + console.log(`Successfully started docs review for ${fullPath}, results will be handled by webhook: ${webhookUrl}`); } console.log(`Successfully posted docs review comments for PR ${repository.name} #${pull_request.number}`); } catch (error) { @@ -262,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 index 9e57565..213184e 100644 --- a/src/services/DocsAgent.js +++ b/src/services/DocsAgent.js @@ -19,10 +19,12 @@ export class DocsAgent { * @param {*} filepath - complete remote file path * @returns {Promise} - Comment text for the PR */ - async reviewDocs(content, filepath) { + async reviewDocs(content, filepath, webhookInfoForResults) { return this.makeAPICall(this.reviewDocsApiUrl || "/review", { content, filepath, + webhookUrl: webhookInfoForResults?.webhookUrl, + webhookMetadata: webhookInfoForResults?.webhookMetadata, }); } @@ -32,10 +34,12 @@ export class DocsAgent { * @param {*} filepath - complete remote file path * @returns {Promise} - Comment text for the PR */ - async auditDocs(content, filepath) { + async auditDocs(content, filepath, webhookInfoForResults) { return this.makeAPICall(this.auditDocsApiUrl || "/audit", { content, filepath, + webhookUrl: webhookInfoForResults?.webhookUrl, + webhookMetadata: webhookInfoForResults?.webhookMetadata, }); } @@ -44,14 +48,14 @@ export class DocsAgent { * @param {Object} changes - Formatted PR changes * @returns {Promise} - Comment text for the PR */ - async getAffectedDocsPages(changes) { + 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); + const response = await this.makeAPICall("/getAffectedDocsPages", changes, webhookInfoForResults); return this.validateResponse(response); } catch (error) { console.error("External API call failed:", error); diff --git a/src/services/GitHub.js b/src/services/GitHub.js index 75e6ca6..f595fe6 100644 --- a/src/services/GitHub.js +++ b/src/services/GitHub.js @@ -780,7 +780,25 @@ async isAllowedToWriteToTheRepo(octokit, username, owner, repo) { } } - + /** + * Add a comment to a GitHub issue or PR + * @param {string} owner - Repository owner + * @param {string} repo - Repository name + * @param {number} issue_number - Issue number + * @param {string} result - Comment body + */ + async addCommentToIssueOrPR(owner, repo, issue_number, result) { + if(!owner || !repo || !issue_number || !result) { + throw new Error("Please add owner, repo, issue_number and result parameters in order to add a comment to a GitHub issue or PR"); + } + const octokit = await this.getOctokitForOrg(owner); + await octokit.rest.issues.createComment({ + owner, + repo, + issue_number, + body: result, + }); + } } export default new GitHub(); \ No newline at end of file diff --git a/test/e2e/api.test.js b/test/e2e/api.test.js new file mode 100644 index 0000000..2c48068 --- /dev/null +++ b/test/e2e/api.test.js @@ -0,0 +1,35 @@ +/** + * E2E tests for the cla related routes. + */ +import { expect, use } from 'chai'; +import chaiHttp from 'chai-http'; +import { describe, it } from 'mocha'; + +const chai = use(chaiHttp); +const SITE_URL = 'http://localhost:' + (process.env.PORT || 3000); + +describe('CLA Routes', function () { + this.timeout(40000); + let agent; + before(function () { + agent = chai.request.agent(SITE_URL); + }); + + after(function () { + agent.close(); + }); + + describe('POST /api/comment', function () { + it('should return the comment added to GitHub issue or PR', async function () { + const res = await agent.post('/api/comment').send({ + owner: 'Git-Commit-Show', + repo: 'gcs-cli', + issue_number: 7, + result: 'Hello, world!', + }); + expect(res).to.have.status(200); + expect(res.text).to.include('Comment added to GitHub issue or PR'); + }); + }); + +}); diff --git a/test/integration/docs-agent.test.js b/test/integration/docs-agent.test.js index c57612c..352a197 100644 --- a/test/integration/docs-agent.test.js +++ b/test/integration/docs-agent.test.js @@ -5,11 +5,13 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; import { server } from '../../app.js'; +import sinon from 'sinon'; import DocsAgent from '../../src/services/DocsAgent.js'; +import GitHub from '../../src/services/GitHub.js'; describe('Docs Agent Services', function () { this.timeout(40000); - + after(function () { server.close(); }); @@ -21,5 +23,23 @@ describe('Docs Agent Services', function () { expect(review).to.be.a('string'); expect(review).to.not.be.empty; }); + + it('should review the docs with webhookUrl and webhookMetadata', async function () { + const addCommentToIssueOrPRStub = sinon.stub(GitHub, 'addCommentToIssueOrPR').resolves(); + const review = await DocsAgent.reviewDocs('Hello, world!', 'https://raw.githubusercontent.com/gitcommitshow/rudder-github-app/e14433e76d74dc680b8cf9102d39f31970e8b794/.codesandbox/tasks.json', { + webhookUrl: 'http://localhost:3000/api/comment', + webhookMetadata: { + issue_number: 7, + repo: 'gcs-cli', + owner: 'Git-Commit-Show', + }, + }); + // Wait for 10 seconds to let the webhook be processed + new Promise(resolve => setTimeout(resolve, 30000)); + expect(review).to.not.throw; + expect(review).to.be.a('string'); + expect(review).to.not.be.empty; + expect(addCommentToIssueOrPRStub.called).to.be.true; + }); }); }); \ No newline at end of file From c19ba45d7423a441ed89222898185606b0f8cf0a Mon Sep 17 00:00:00 2001 From: gitcommitshow <56937085+gitcommitshow@users.noreply.github.com> Date: Sat, 27 Sep 2025 13:53:47 +0530 Subject: [PATCH 09/10] chore: fix comment --- app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.js b/app.js index 57dcc34..ab5fa99 100644 --- a/app.js +++ b/app.js @@ -151,7 +151,7 @@ GitHub.app.webhooks.on("pull_request.labeled", async ({ octokit, payload }) => { }); console.log(`Successfully started docs review for ${fullPath}, results will be handled by webhook: ${webhookUrl}`); } - console.log(`Successfully posted docs review comments for PR ${repository.name} #${pull_request.number}`); + console.log(`Successfully started all necessary docs reviews for PR ${repository.name} #${pull_request.number}`); } catch (error) { console.error(error); } From ab6283f8d48effe9216fb36c7a6cd03f4cfda7dd Mon Sep 17 00:00:00 2001 From: gitcommitshow <56937085+gitcommitshow@users.noreply.github.com> Date: Sat, 27 Sep 2025 18:09:51 +0530 Subject: [PATCH 10/10] test: await until result is received to fix webhook test --- test/integration/docs-agent.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/docs-agent.test.js b/test/integration/docs-agent.test.js index 352a197..d93f0ac 100644 --- a/test/integration/docs-agent.test.js +++ b/test/integration/docs-agent.test.js @@ -34,8 +34,8 @@ describe('Docs Agent Services', function () { owner: 'Git-Commit-Show', }, }); - // Wait for 10 seconds to let the webhook be processed - new Promise(resolve => setTimeout(resolve, 30000)); + // Wait for the webhook to be processed + await new Promise(resolve => setTimeout(resolve, 10000)); expect(review).to.not.throw; expect(review).to.be.a('string'); expect(review).to.not.be.empty;