From 5b8d9fa52f824d5a7e699e0c475b90d19b0b0b15 Mon Sep 17 00:00:00 2001 From: Thanmayee Reddy Kotha <190446018+thanmayeereddykotha@users.noreply.github.com> Date: Fri, 3 Jul 2026 21:10:56 +0530 Subject: [PATCH] feat: Automate GitHub invitations and allow task generation for pending collaborators --- backend/package-lock.json | 194 ++++++++++++++++++++------- backend/package.json | 6 +- backend/routes/collaboratorRoutes.js | 42 +++++- backend/routes/taskRoutes.js | 20 ++- 4 files changed, 207 insertions(+), 55 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index c2516301..6ce12602 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1273,17 +1273,36 @@ } }, "node_modules/@grpc/grpc-js": { - "version": "1.8.22", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.22.tgz", - "integrity": "sha512-oAjDdN7fzbUi+4hZjKG96MR6KTEubAeMpQEb+77qy+3r0Ua5xTFuie6JOLr4ZZgl5g+W5/uRTS2M1V8mVAFPuA==", + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", + "integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@grpc/proto-loader": "^0.7.0", - "@types/node": ">=12.12.47" + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" }, "engines": { - "node": "^8.13.0 || >=10.10.0" + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", + "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" } }, "node_modules/@grpc/proto-loader": { @@ -1901,6 +1920,17 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/@jsdoc/salty": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.12.tgz", @@ -1955,6 +1985,19 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@nodable/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT", + "optional": true + }, "node_modules/@octokit/app": { "version": "16.1.2", "resolved": "https://registry.npmjs.org/@octokit/app/-/app-16.1.2.tgz", @@ -2438,13 +2481,6 @@ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", "license": "BSD-3-Clause" }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", - "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==", - "license": "BSD-3-Clause", - "optional": true - }, "node_modules/@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", @@ -3357,6 +3393,19 @@ "node": ">= 8" } }, + "node_modules/anynum": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", + "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true + }, "node_modules/append-field": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", @@ -5341,10 +5390,27 @@ "license": "Apache-2.0", "optional": true }, + "node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, "node_modules/fast-xml-parser": { - "version": "4.5.6", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.6.tgz", - "integrity": "sha512-Yd4vkROfJf8AuJrDIVMVmYfULKmIJszVsMv7Vo71aocsKgFxpdlpSHXSaInvyYfgw2PRuObQSW2GFpVMUjxu9A==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.9.3.tgz", + "integrity": "sha512-brCNCeScma/kqa54J4PIDriSSSLssRkuYaUCpvHJulGc3HGI/xxKUCTDcYkAdqJsyb//ydpbxecjC3hB9+tb/g==", "funding": [ { "type": "github", @@ -5354,7 +5420,12 @@ "license": "MIT", "optional": true, "dependencies": { - "strnum": "^1.0.5" + "@nodable/entities": "^2.2.0", + "fast-xml-builder": "^1.2.0", + "is-unsafe": "^1.0.1", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.4.1", + "xml-naming": "^0.1.0" }, "bin": { "fxparser": "src/cli/cli.js" @@ -6076,31 +6147,6 @@ "node": ">=12" } }, - "node_modules/google-gax/node_modules/protobufjs": { - "version": "7.2.4", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.4.tgz", - "integrity": "sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/node": ">=13.7.0", - "long": "^5.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/google-logging-utils": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", @@ -6670,6 +6716,19 @@ "license": "MIT", "optional": true }, + "node_modules/is-unsafe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-1.0.1.tgz", + "integrity": "sha512-CLK2+VdgERgD96EYm5lUQssZYlRg2tkZnbsxZoacmSiRxiFJ4Nk4SzjCl+Ur+v3kXIY9dTIdb3IH22y1mZ56LA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -9028,6 +9087,22 @@ "node": ">=8" } }, + "node_modules/path-expression-matcher": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.1.tgz", + "integrity": "sha512-h7bxdzhHk8Knyc4Tj+jMaa7fEEoUJy7p1qtbVgkYg1Uhpe5Np5VuGXCRZnkZvU+Q42M1vStt0ifa3ueykRJPmQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -9361,9 +9436,9 @@ } }, "node_modules/protobufjs-cli": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.1.1.tgz", - "integrity": "sha512-VPWMgIcRNyQwWUv8OLPyGQ/0lQY/QTQAVN5fh+XzfDwsVw1FZ2L3DM/bcBf8WPiRz2tNpaov9lPZfNcmNo6LXA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.3.3.tgz", + "integrity": "sha512-zwmt6JeStPjeofZbl+QADexAVzP1EgsIAsO7mCfKkW/8oBxHwgTqJ1aD7zDrcCC0Nb+SLWSvCR3RDaKgIO3P8w==", "license": "BSD-3-Clause", "optional": true, "dependencies": { @@ -9386,7 +9461,7 @@ "node": ">=12.0.0" }, "peerDependencies": { - "protobufjs": "^7.0.0" + "protobufjs": "^7.6.2" } }, "node_modules/protobufjs-cli/node_modules/balanced-match": { @@ -10611,9 +10686,9 @@ } }, "node_modules/strnum": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", - "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.1.tgz", + "integrity": "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==", "funding": [ { "type": "github", @@ -10621,7 +10696,10 @@ } ], "license": "MIT", - "optional": true + "optional": true, + "dependencies": { + "anynum": "^1.0.1" + } }, "node_modules/stubs": { "version": "3.0.0", @@ -11439,6 +11517,22 @@ } } }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/xml2js": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", diff --git a/backend/package.json b/backend/package.json index 135eb1e1..6a70449b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -65,6 +65,10 @@ "micromatch": "^4.0.8", "cross-spawn": "^7.0.6", "got": "^11.8.5", - "@tootallnate/once": "^3.0.1" + "@tootallnate/once": "^3.0.1", + "@grpc/grpc-js": "^1.14.4", + "fast-xml-parser": "^5.7.0", + "protobufjs": "^7.6.4", + "protobufjs-cli": "^1.3.3" } } diff --git a/backend/routes/collaboratorRoutes.js b/backend/routes/collaboratorRoutes.js index 5c4b1612..fad204b3 100644 --- a/backend/routes/collaboratorRoutes.js +++ b/backend/routes/collaboratorRoutes.js @@ -81,6 +81,9 @@ const express = require('express'); const router = express.Router(); // Declares a constant variable named 'nodemailer' and assigns it the Nodemailer module. `require()` is a Node.js function used to import modules. // This line imports the Nodemailer library, which is used to send emails from Node.js applications, specifically for notifying an admin about new beta signups. +const nodemailer = require('nodemailer'); +// Imports the Octokit class. +const { Octokit } = require('octokit'); // Defines a new route handler for HTTP POST requests to the root path ('/') relative to where this router is mounted. // The `async` keyword indicates that this function will perform asynchronous operations, allowing `await` to be used inside. @@ -105,6 +108,38 @@ router.post('/', async (req, res) => { // Starts a `try` block, which encloses code that might throw an error. // This block is used to safely execute the email sending logic, allowing any potential errors during the process to be caught and handled gracefully. try { + let inviteStatusMessage = "No GitHub invitation sent (GitHub token not configured)."; + let inviteSuccess = false; + + // Automate the GitHub Repository Invitation using Octokit + if (process.env.GITHUB_ADMIN_TOKEN && process.env.GITHUB_REPO_OWNER && process.env.GITHUB_REPO_NAME) { + try { + const octokit = new Octokit({ auth: process.env.GITHUB_ADMIN_TOKEN }); + + await octokit.request('PUT /repos/{owner}/{repo}/collaborators/{username}', { + owner: process.env.GITHUB_REPO_OWNER, + repo: process.env.GITHUB_REPO_NAME, + username: githubUsername, + permission: 'push' // Grants write access. This can be 'pull' or 'triage' if preferred. + }); + + inviteStatusMessage = `Successfully sent GitHub repository invitation to @${githubUsername}.`; + inviteSuccess = true; + console.log(`[BETA SIGNUP] ${inviteStatusMessage}`); + } catch (ghError) { + // Status 422 generally means validation failed, such as user is already a collaborator + if (ghError.status === 422) { + inviteStatusMessage = `Invitation to @${githubUsername} was skipped (user is already a collaborator, or invitation is already pending).`; + inviteSuccess = true; + console.log(`[BETA SIGNUP] ${inviteStatusMessage}`); + } else { + inviteStatusMessage = `Failed to send GitHub repository invitation to @${githubUsername}. Error: ${ghError.message}`; + console.error(`[BETA SIGNUP ERROR] ${inviteStatusMessage}`); + } + } + } else { + console.warn('GITHUB_ADMIN_TOKEN, GITHUB_REPO_OWNER, or GITHUB_REPO_NAME missing. Skipping automated GitHub invitation.'); + } // Declares a constant variable 'adminEmail'. It attempts to retrieve the 'ADMIN_EMAIL' environment variable. // If `process.env.ADMIN_EMAIL` is falsy (e.g., not set), it defaults to the string 'admin@example.com'. @@ -159,7 +194,8 @@ router.post('/', async (req, res) => {
GitHub Username: ${githubUsername}
GitHub Profile: ${githubProfileUrl}
-Email Address: ${email}
+Email Address: ${email}
+Automation Status: ${inviteStatusMessage}
This message was generated automatically by the Zync Onboarding System.
@@ -182,12 +218,12 @@ router.post('/', async (req, res) => { console.warn('SMTP credentials not found in .env. Skipping actual email dispatch.'); // Logs a message to the console, using a template literal to include the 'githubUsername' and 'email' of the applicant. // This ensures that even without email dispatch, the essential details of the beta signup are recorded in the server logs, providing a record of applications. - console.log(`[BETA SIGNUP] User: ${githubUsername}, Email: ${email}`); + console.log(`[BETA SIGNUP] User: ${githubUsername}, Email: ${email}, GitHub Invite: ${inviteSuccess}`); } // Sends an HTTP response with a status code of 200 (OK) and a JSON object indicating success. // This informs the client that their beta application was successfully processed (either email sent or logged), providing positive feedback. - res.status(200).json({ success: true, message: 'Application received successfully.' }); + res.status(200).json({ success: true, message: 'Application received and processed successfully.', inviteSent: inviteSuccess }); // Starts a `catch` block, which executes if any error occurs within the preceding `try` block. // The `error` object contains details about the exception that was thrown. // This block is essential for handling any unexpected issues during the email sending process, preventing the server from crashing and allowing for graceful error reporting. diff --git a/backend/routes/taskRoutes.js b/backend/routes/taskRoutes.js index b1a9f073..5dff0b74 100644 --- a/backend/routes/taskRoutes.js +++ b/backend/routes/taskRoutes.js @@ -110,7 +110,25 @@ const getRepoCollaboratorLogins = async (octokit, owner, repo) => { affiliation: 'all', }); - return new Set((response.data || []).map((collab) => String(collab.login || '').toLowerCase())); + const logins = new Set((response.data || []).map((collab) => String(collab.login || '').toLowerCase())); + + try { + const invitesResponse = await octokit.request('GET /repos/{owner}/{repo}/invitations', { + owner, + repo, + per_page: 100, + }); + + (invitesResponse.data || []).forEach(invite => { + if (invite.invitee && invite.invitee.login) { + logins.add(String(invite.invitee.login).toLowerCase()); + } + }); + } catch (err) { + console.warn('[GitHub] Could not fetch pending invitations:', err.message); + } + + return logins; }; const generateUniqueCommitCode = async () => {