From e481823e069fa3d0365f1296576aa5c4d9fd6dee Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Tue, 3 Feb 2026 11:03:18 -0800 Subject: [PATCH] feat: add mentorship onboarding (#5) ### Explanation _No response_ ### Issue _No response_ ### Attestations - [ ] I have read and agree to the [Code of Conduct](https://docs.nhcarrigan.com/community/coc/) - [ ] I have read and agree to the [Community Guidelines](https://docs.nhcarrigan.com/community/guide/). - [ ] My contribution complies with the [Contributor Covenant](https://docs.nhcarrigan.com/dev/covenant/). ### Dependencies - [ ] I have pinned the dependencies to a specific patch version. ### Style - [ ] I have run the linter and resolved any errors. - [ ] My pull request uses an appropriate title, matching the conventional commit standards. - [ ] My scope of feat/fix/chore/etc. correctly matches the nature of changes in my pull request. ### Tests - [ ] My contribution adds new code, and I have added tests to cover it. - [ ] My contribution modifies existing code, and I have updated the tests to reflect these changes. - [ ] All new and existing tests pass locally with my changes. - [ ] Code coverage remains at or above the configured threshold. ### Documentation _No response_ ### Versioning _No response_ Reviewed-on: https://git.nhcarrigan.com/nhcarrigan/ephemere/pulls/5 Co-authored-by: Naomi Carrigan Co-committed-by: Naomi Carrigan --- prod.env | 2 +- typescript/src/github/onboardMentee.ts | 209 +++++++++++++++++++++++++ 2 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 typescript/src/github/onboardMentee.ts diff --git a/prod.env b/prod.env index b9961d6..19934c2 100644 --- a/prod.env +++ b/prod.env @@ -7,7 +7,7 @@ CROWDIN_TOKEN="op://Environment Variables - Development/Ephemere/Crowdin Token" GITHUB_TOKEN="op://Environment Variables - Development/Ephemere/GitHub Token" # Discord -DISCORD_TOKEN="op://Environment Variables - Development/Ephemere/Discord Token" +DISCORD_TOKEN="op://Environment Variables - Naomi/Hikari/discord_token" DISCORD_CLIENT_ID="op://Private/Guild Counter/client id" DISCORD_CLIENT_SECRET="op://Private/Guild Counter/client secret" DISCORD_BOT_TOKEN="op://Environment Variables - Naomi/Amari/bot token" diff --git a/typescript/src/github/onboardMentee.ts b/typescript/src/github/onboardMentee.ts new file mode 100644 index 0000000..0c704ef --- /dev/null +++ b/typescript/src/github/onboardMentee.ts @@ -0,0 +1,209 @@ +/** + * @copyright NHCarrigan + * @license Naomi's Public License + * @author Naomi Carrigan + */ +import { input } from "@inquirer/prompts"; +import { Octokit } from "@octokit/rest"; + +// Environment variable checks +const githubToken = process.env.GITHUB_TOKEN; +const discordToken = process.env.DISCORD_TOKEN; + +if (githubToken === undefined || githubToken === "") { + throw new Error("GITHUB_TOKEN is not set"); +} + +if (discordToken === undefined || discordToken === "") { + throw new Error("DISCORD_TOKEN is not set"); +} + +const octokit = new Octokit({ auth: githubToken }); + +/** + * Converts a full name to kebab-case. + * @param fullName - The full name to convert. + * @returns The kebab-case version. + */ +function toKebabCase(fullName: string): string { + return fullName.trim().toLowerCase(). + replaceAll(/[^\d\sa-z-]/g, ""). + replaceAll(/\s+/g, "-"); +} + +/** + * Prompts for mentee information. + * @returns The mentee information. + */ +async function getMenteeInfo(): Promise<{ + discordId: string; + fullName: string; + githubUsername: string; +}> { + const discordId = await input({ + message: "Enter the mentee's Discord ID:", + validate: (value) => { + const trimmed = value.trim(); + if (trimmed === "") { + return "Discord ID cannot be empty"; + } + if (!/^\d+$/.test(trimmed)) { + return "Discord ID must be numeric"; + } + return true; + }, + }); + + const fullName = await input({ + message: "Enter the mentee's full name:", + validate: (value) => { + if (value.trim() === "") { + return "Full name cannot be empty"; + } + return true; + }, + }); + + const githubUsername = await input({ + message: "Enter the mentee's GitHub username:", + validate: (value) => { + if (value.trim() === "") { + return "GitHub username cannot be empty"; + } + return true; + }, + }); + + return { discordId, fullName, githubUsername }; +} + +interface RepoData { + + /** + * Using camelCase interface for internal consistency. + */ + htmlUrl: string; +} + +/** + * Creates a public GitHub repository in the nhcarrigan-mentorship organization with auto-init enabled. + * @param repoName - The kebab-case repository name derived from mentee's name. + * @param fullName - The mentee's full name used in repository description. + * @returns The created repository data with HTML URL. + */ +async function createRepository( + repoName: string, + fullName: string, +): Promise { + console.log("\n1️⃣ Creating repository..."); + + const { data: repo } = await octokit.rest.repos.createInOrg({ + // eslint-disable-next-line @typescript-eslint/naming-convention -- GitHub API + auto_init: true, + description: `Mentorship repository for ${fullName}`, + name: repoName, + org: "nhcarrigan-mentorship", + private: false, + }); + console.log("✅ Repository created successfully!"); + + return { htmlUrl: repo.html_url }; +} + +/** + * Adds the mentee as a collaborator. + * @param repoName - The repository name. + * @param githubUsername - The mentee's GitHub username. + */ +async function addCollaborator( + repoName: string, + githubUsername: string, +): Promise { + console.log("\n2️⃣ Adding collaborator..."); + + await octokit.rest.repos.addCollaborator({ + owner: "nhcarrigan-mentorship", + permission: "maintain", + repo: repoName, + username: githubUsername, + }); + console.log("✅ Collaborator added with maintain permissions!"); +} + +/** + * Sends a welcome message to Discord. + * @param discordId - The mentee's Discord ID. + * @param repoUrl - The repository URL. + */ +async function sendDiscordMessage( + discordId: string, + repoUrl: string, +): Promise { + console.log("\n3️⃣ Sending Discord welcome message..."); + const channelId = "1400589073613062204"; + const welcomeMessage = { + content: `<@${discordId}> Welcome to the mentorship programme! 🎉\n\nYour personal repository has been created: ${repoUrl}\n\nYou have been added as a collaborator with maintain permissions. Feel free to use this space to practice, experiment, and work on projects. I'm here to help guide you on your journey!\n\nLooking forward to working with you! 💖`, + }; + + const discordResponse = await fetch( + `https://discord.com/api/v10/channels/${channelId}/messages`, + { + body: JSON.stringify(welcomeMessage), + headers: { + // eslint-disable-next-line @typescript-eslint/naming-convention -- Discord API + "Authorization": `Bot ${String(discordToken)}`, + // eslint-disable-next-line @typescript-eslint/naming-convention -- HTTP headers + "Content-Type": "application/json", + }, + method: "POST", + }, + ); + + if (!discordResponse.ok) { + const error = await discordResponse.text(); + throw new Error(`Discord API error: ${error}`); + } + + console.log("✅ Discord welcome message sent!"); +} + +/** + * Main function to onboard a new mentee. + */ +async function main(): Promise { + console.log("🚀 Starting mentorship onboarding process...\n"); + + try { + const { discordId, fullName, githubUsername } = await getMenteeInfo(); + + const repoName = toKebabCase(fullName); + console.log(`\n📦 Repository will be created as: nhcarrigan-mentorship/${repoName}`); + + const repo = await createRepository(repoName, fullName); + await addCollaborator(repoName, githubUsername); + await sendDiscordMessage(discordId, repo.htmlUrl); + + // Success summary + console.log("\n🎊 Onboarding completed successfully!"); + console.log(`\n📋 Summary:`); + console.log(` Mentee: ${fullName} (@${githubUsername})`); + console.log(` Discord ID: ${discordId}`); + console.log(` Repository: ${repo.htmlUrl}`); + console.log(` Permissions: Maintain`); + console.log(` Welcome message sent to Discord channel`); + } catch (error) { + console.error("\n❌ Error during onboarding:"); + if (error instanceof Error) { + console.error(error.message); + } else { + console.error(error); + } + process.exit(1); + } +} + +// Run the script +await main().catch((error: unknown) => { + console.error("❌ Unexpected error:", error); + process.exit(1); +});