From 8c502d20825b72ec79581ef70a835e6853aabefe Mon Sep 17 00:00:00 2001 From: Sagar Miglani <85228812+sagarmiglani@users.noreply.github.com> Date: Fri, 17 Oct 2025 18:48:32 +0530 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20Enable=20addition=20of=20Private=20R?= =?UTF-8?q?epos=20in=20Spacecat=20via=20Slack=20and=20add=20s=E2=80=A6=20(?= =?UTF-8?q?#1372)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sagar Miglani --- src/support/slack/commands/add-repo.js | 32 ++++--- test/support/slack/commands/add-repo.test.js | 96 +++++++++++++++++++- 2 files changed, 112 insertions(+), 16 deletions(-) diff --git a/src/support/slack/commands/add-repo.js b/src/support/slack/commands/add-repo.js index a82bed3a7..28b23c8db 100644 --- a/src/support/slack/commands/add-repo.js +++ b/src/support/slack/commands/add-repo.js @@ -38,7 +38,7 @@ function AddRepoCommand(context) { name: 'Add GitHub Repo', description: 'Adds a Github repository to previously added site.', phrases: PHRASES, - usageText: `${PHRASES.join(' or ')} {site} {githubRepoURL}`, + usageText: `${PHRASES.join(' or ')} {site} {githubRepoURL} [branch]`, }); const { dataAccess, log } = context; @@ -81,7 +81,7 @@ function AddRepoCommand(context) { const { say } = slackContext; try { - const [baseURLInput, repoUrlInput] = args; + const [baseURLInput, repoUrlInput, branchInput] = args; const baseURL = extractURLFromSlackInput(baseURLInput); let repoUrl = extractURLFromSlackInput(repoUrlInput, false, false); @@ -106,22 +106,32 @@ function AddRepoCommand(context) { const repoInfo = await fetchRepoInfo(repoUrl); + let owner; + let repoName; + let branch; + if (repoInfo === null) { - await say(`:warning: The GitHub repository '${repoUrl}' could not be found (private repo?).`); - return; - } + [owner, repoName] = repoUrl.split('github.com/')[1].split('/'); + branch = branchInput || 'main'; - if (repoInfo.archived) { - await say(`:warning: The GitHub repository '${repoUrl}' is archived. Please unarchive it before adding it to a site.`); - return; + await say(`:warning: GitHub API returned 404 for ${repoUrl}. Adding as private repo with branch: ${branch}`); + } else { + if (repoInfo.archived) { + await say(`:warning: The GitHub repository '${repoUrl}' is archived. Please unarchive it before adding it to a site.`); + return; + } + + owner = repoInfo.owner.login; + repoName = repoInfo.name; + branch = branchInput || repoInfo.default_branch; } site.setGitHubURL(repoUrl); const codeConfig = { type: 'github', - owner: repoInfo.owner.login, - repo: repoInfo.name, - ref: repoInfo.default_branch, + owner, + repo: repoName, + ref: branch, url: repoUrl, }; site.setCode(codeConfig); diff --git a/test/support/slack/commands/add-repo.test.js b/test/support/slack/commands/add-repo.test.js index 6febd553a..7f8380dd5 100644 --- a/test/support/slack/commands/add-repo.test.js +++ b/test/support/slack/commands/add-repo.test.js @@ -234,16 +234,29 @@ describe('AddRepoCommand', () => { }); }); - it('handles non-existent repository (404 error)', async () => { + it('handles private repo', async () => { nock('https://api.github.com') - .get('/repos/invalid/repo') + .get('/repos/private/repo') .reply(404); - args[1] = 'https://github.com/invalid/repo'; + args[1] = 'https://github.com/private/repo'; await command.handleExecution(args, slackContext); - // Assertions to confirm handling of non-existent repository - expect(slackContext.say.calledWith(':warning: The GitHub repository \'https://github.com/invalid/repo\' could not be found (private repo?).')).to.be.true; + // Should parse URL manually and add as private repo with 'main' branch + expect(siteStub.setCode).to.have.been.calledWith({ + type: 'github', + owner: 'private', + repo: 'repo', + ref: 'main', // defaults to 'main' for private repos + url: 'https://github.com/private/repo', + }); + expect(siteStub.save).to.have.been.called; + + // Should send warning about adding as private repo + expect(slackContext.say.calledWithMatch(/GitHub API returned 404/)).to.be.true; + expect(slackContext.say.calledWithMatch(/Adding as private repo/)).to.be.true; + // Should send success message + expect(slackContext.say.calledWithMatch(/GitHub repo added/)).to.be.true; }); it('handles errors other than 404 from GitHub API', async () => { @@ -270,4 +283,77 @@ describe('AddRepoCommand', () => { expect(slackContext.say.calledWithMatch(/Network error occurred/)).to.be.true; }); }); + + describe('Branch Support', () => { + it('handles custom branch for public repository', async () => { + nock('https://api.github.com') + .get('/repos/valid/repo') + .reply(200, { + archived: false, + name: 'repo', + owner: { login: 'valid' }, + default_branch: 'main', + }); + + const args = ['validSite.com', 'https://github.com/valid/repo', 'develop']; + const command = AddRepoCommand(context); + + await command.handleExecution(args, slackContext); + + expect(siteStub.setCode).to.have.been.calledWith({ + type: 'github', + owner: 'valid', + repo: 'repo', + ref: 'develop', // Should use custom branch + url: 'https://github.com/valid/repo', + }); + expect(siteStub.save).to.have.been.called; + }); + + it('handles custom branch for private repository', async () => { + nock('https://api.github.com') + .get('/repos/private/repo') + .reply(404); + + const args = ['validSite.com', 'https://github.com/private/repo', 'feature-branch']; + const command = AddRepoCommand(context); + + await command.handleExecution(args, slackContext); + + expect(slackContext.say.calledWithMatch(/GitHub API returned 404/)).to.be.true; + expect(slackContext.say.calledWithMatch(/Adding as private repo/)).to.be.true; + expect(slackContext.say.calledWithMatch(/feature-branch/)).to.be.true; + expect(siteStub.setCode).to.have.been.calledWith({ + type: 'github', + owner: 'private', + repo: 'repo', + ref: 'feature-branch', // Should use custom branch + url: 'https://github.com/private/repo', + }); + expect(siteStub.save).to.have.been.called; + + // Should send success message + expect(slackContext.say.calledWithMatch(/GitHub repo added/)).to.be.true; + }); + + it('handles private repository without custom branch (defaults to main)', async () => { + nock('https://api.github.com') + .get('/repos/private/repo') + .reply(404); + + const args = ['validSite.com', 'https://github.com/private/repo']; + const command = AddRepoCommand(context); + + await command.handleExecution(args, slackContext); + + expect(siteStub.setCode).to.have.been.calledWith({ + type: 'github', + owner: 'private', + repo: 'repo', + ref: 'main', // Should default to 'main' for private repos + url: 'https://github.com/private/repo', + }); + expect(siteStub.save).to.have.been.called; + }); + }); }); From 5cf3e3aad5c32c9ce2590484b0e491553b19d7eb Mon Sep 17 00:00:00 2001 From: Sagar Miglani Date: Thu, 23 Oct 2025 20:43:33 +0530 Subject: [PATCH 2/2] fix: Enable addition of Private Repos in Spacecat via Slack --- src/support/slack/commands/add-repo.js | 21 ++++ test/support/slack/commands/add-repo.test.js | 119 ++++++++++++++++++- 2 files changed, 139 insertions(+), 1 deletion(-) diff --git a/src/support/slack/commands/add-repo.js b/src/support/slack/commands/add-repo.js index 28b23c8db..314c89447 100644 --- a/src/support/slack/commands/add-repo.js +++ b/src/support/slack/commands/add-repo.js @@ -68,6 +68,21 @@ function AddRepoCommand(context) { } } + async function isOnboardedWithAemy(owner, repo, branch) { + const AEMY_ENDPOINT = `https://ec-xp-fapp-coordinator.azurewebsites.net/api/fn-ghapp/functions/get_installation_token/${owner}/${repo}/${branch}`; + try { + const response = await fetch(AEMY_ENDPOINT, { headers: { 'x-api-key': process.env.AEMY_API_KEY } }); + if (response.ok) { + const data = await response.json(); + return data.token !== null; + } else { + throw new Error('Failed to check if repository is onboarded with Aemy'); + } + } catch (error) { + throw new Error(`Failed to check if repository is onboarded with Aemy: ${error.message}`); + } + } + /** * Execute function for AddRepoCommand. This function validates the input, fetches the repository * information from the GitHub API, and saves it as a site in the database. @@ -126,6 +141,12 @@ function AddRepoCommand(context) { branch = branchInput || repoInfo.default_branch; } + const isOnboarded = await isOnboardedWithAemy(owner, repoName, branch); + if (!isOnboarded) { + await say(`:warning: The repository '${repoUrl}' is not onboarded with Aemy. Please onboard it with Aemy before adding it to a site.`); + return; + } + site.setGitHubURL(repoUrl); const codeConfig = { type: 'github', diff --git a/test/support/slack/commands/add-repo.test.js b/test/support/slack/commands/add-repo.test.js index 7f8380dd5..520f9364c 100644 --- a/test/support/slack/commands/add-repo.test.js +++ b/test/support/slack/commands/add-repo.test.js @@ -26,6 +26,8 @@ describe('AddRepoCommand', () => { let siteStub; beforeEach(() => { + process.env.AEMY_API_KEY = 'test-api-key'; + sqsStub = { sendMessage: sinon.stub().resolves(), }; @@ -66,7 +68,10 @@ describe('AddRepoCommand', () => { context = { dataAccess: dataAccessStub, sqs: sqsStub, - env: { AUDIT_JOBS_QUEUE_URL: 'testQueueUrl' }, + env: { + AUDIT_JOBS_QUEUE_URL: 'testQueueUrl', + AEMY_API_KEY: 'test-api-key', + }, log: console, }; }); @@ -94,11 +99,16 @@ describe('AddRepoCommand', () => { default_branch: 'main', }); + const aemyScope = nock('https://ec-xp-fapp-coordinator.azurewebsites.net') + .get('/api/fn-ghapp/functions/get_installation_token/valid/repo/main') + .reply(200, { token: 'some-token' }); + const args = ['validSite.com', 'https://github.com/valid/repo']; const command = AddRepoCommand(context); await command.handleExecution(args, slackContext); + expect(aemyScope.isDone()).to.be.true; expect(slackContext.say.called).to.be.true; expect(siteStub.setGitHubURL).to.have.been.calledWith('https://github.com/valid/repo'); expect(siteStub.setCode).to.have.been.calledWith({ @@ -134,11 +144,17 @@ describe('AddRepoCommand', () => { default_branch: 'main', }); + const aemyScope = nock('https://ec-xp-fapp-coordinator.azurewebsites.net') + .get('/api/fn-ghapp/functions/get_installation_token/valid/repo/main') + .reply(200, { token: 'some-token' }); + const args = ['validSite.com', 'github.com/valid/repo']; const command = AddRepoCommand(context); await command.handleExecution(args, slackContext); + expect(aemyScope.isDone()).to.be.true; + expect(slackContext.say).calledWith('\n' + ' :white_check_mark: *GitHub repo added for *\n' + '\n' @@ -221,9 +237,14 @@ describe('AddRepoCommand', () => { default_branch: 'main', }); + const aemyScope = nock('https://ec-xp-fapp-coordinator.azurewebsites.net') + .get('/api/fn-ghapp/functions/get_installation_token/valid/repo/main') + .reply(200, { token: 'some-token' }); + await command.handleExecution(args, slackContext); // Assertions to confirm repo info was fetched and handled correctly + expect(aemyScope.isDone()).to.be.true; expect(slackContext.say).calledWithMatch(/GitHub repo added/); expect(siteStub.setCode).to.have.been.calledWith({ type: 'github', @@ -239,9 +260,16 @@ describe('AddRepoCommand', () => { .get('/repos/private/repo') .reply(404); + const aemyScope = nock('https://ec-xp-fapp-coordinator.azurewebsites.net') + .get('/api/fn-ghapp/functions/get_installation_token/private/repo/main') + .reply(200, { token: 'some-token' }); + args[1] = 'https://github.com/private/repo'; await command.handleExecution(args, slackContext); + // Should verify Aemy check was made + expect(aemyScope.isDone()).to.be.true; + // Should parse URL manually and add as private repo with 'main' branch expect(siteStub.setCode).to.have.been.calledWith({ type: 'github', @@ -295,11 +323,16 @@ describe('AddRepoCommand', () => { default_branch: 'main', }); + const aemyScope = nock('https://ec-xp-fapp-coordinator.azurewebsites.net') + .get('/api/fn-ghapp/functions/get_installation_token/valid/repo/develop') + .reply(200, { token: 'some-token' }); + const args = ['validSite.com', 'https://github.com/valid/repo', 'develop']; const command = AddRepoCommand(context); await command.handleExecution(args, slackContext); + expect(aemyScope.isDone()).to.be.true; expect(siteStub.setCode).to.have.been.calledWith({ type: 'github', owner: 'valid', @@ -315,11 +348,16 @@ describe('AddRepoCommand', () => { .get('/repos/private/repo') .reply(404); + const aemyScope = nock('https://ec-xp-fapp-coordinator.azurewebsites.net') + .get('/api/fn-ghapp/functions/get_installation_token/private/repo/feature-branch') + .reply(200, { token: 'some-token' }); + const args = ['validSite.com', 'https://github.com/private/repo', 'feature-branch']; const command = AddRepoCommand(context); await command.handleExecution(args, slackContext); + expect(aemyScope.isDone()).to.be.true; expect(slackContext.say.calledWithMatch(/GitHub API returned 404/)).to.be.true; expect(slackContext.say.calledWithMatch(/Adding as private repo/)).to.be.true; expect(slackContext.say.calledWithMatch(/feature-branch/)).to.be.true; @@ -341,11 +379,16 @@ describe('AddRepoCommand', () => { .get('/repos/private/repo') .reply(404); + const aemyScope = nock('https://ec-xp-fapp-coordinator.azurewebsites.net') + .get('/api/fn-ghapp/functions/get_installation_token/private/repo/main') + .reply(200, { token: 'some-token' }); + const args = ['validSite.com', 'https://github.com/private/repo']; const command = AddRepoCommand(context); await command.handleExecution(args, slackContext); + expect(aemyScope.isDone()).to.be.true; expect(siteStub.setCode).to.have.been.calledWith({ type: 'github', owner: 'private', @@ -356,4 +399,78 @@ describe('AddRepoCommand', () => { expect(siteStub.save).to.have.been.called; }); }); + + describe('Aemy Integration', () => { + it('blocks repository not onboarded with Aemy', async () => { + nock('https://api.github.com') + .get('/repos/valid/repo') + .reply(200, { + archived: false, + name: 'repo', + owner: { login: 'valid' }, + default_branch: 'main', + }); + + const aemyNock = nock('https://ec-xp-fapp-coordinator.azurewebsites.net') + .get('/api/fn-ghapp/functions/get_installation_token/valid/repo/main') + .reply(200, { token: null }); + + const args = ['validSite.com', 'https://github.com/valid/repo']; + const command = AddRepoCommand(context); + + await command.handleExecution(args, slackContext); + + expect(aemyNock.isDone()).to.be.true; + expect(slackContext.say.calledWith(':warning: The repository \'https://github.com/valid/repo\' is not onboarded with Aemy. Please onboard it with Aemy before adding it to a site.')).to.be.true; + expect(siteStub.save).to.not.have.been.called; + }); + + it('handles Aemy API error responses', async () => { + nock('https://api.github.com') + .get('/repos/valid/repo') + .reply(200, { + archived: false, + name: 'repo', + owner: { login: 'valid' }, + default_branch: 'main', + }); + + const aemyNock = nock('https://ec-xp-fapp-coordinator.azurewebsites.net') + .get('/api/fn-ghapp/functions/get_installation_token/valid/repo/main') + .reply(500, { error: 'Internal Server Error' }); + + const args = ['validSite.com', 'https://github.com/valid/repo']; + const command = AddRepoCommand(context); + + await command.handleExecution(args, slackContext); + + expect(aemyNock.isDone()).to.be.true; + expect(slackContext.say.calledWithMatch(/Failed to check if repository is onboarded with Aemy/)).to.be.true; + expect(siteStub.save).to.not.have.been.called; + }); + + it('handles Aemy API network errors', async () => { + nock('https://api.github.com') + .get('/repos/valid/repo') + .reply(200, { + archived: false, + name: 'repo', + owner: { login: 'valid' }, + default_branch: 'main', + }); + + const aemyNock = nock('https://ec-xp-fapp-coordinator.azurewebsites.net') + .get('/api/fn-ghapp/functions/get_installation_token/valid/repo/main') + .replyWithError('Network error'); + + const args = ['validSite.com', 'https://github.com/valid/repo']; + const command = AddRepoCommand(context); + + await command.handleExecution(args, slackContext); + + expect(aemyNock.isDone()).to.be.true; + expect(slackContext.say.calledWithMatch(/Network error/)).to.be.true; + expect(siteStub.save).to.not.have.been.called; + }); + }); });