feat: add mentorship onboarding (#5)
CI / dependency-pin-check-typescript (push) Successful in 4s
CI / dependency-pin-check-python (push) Successful in 4s
CI / python (push) Successful in 9m23s
CI / typescript (push) Successful in 9m48s
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m51s

### 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: #5
Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com>
Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
This commit was merged in pull request #5.
This commit is contained in:
2026-02-03 11:03:18 -08:00
committed by Naomi Carrigan
parent f5e8deca59
commit e481823e06
2 changed files with 210 additions and 1 deletions
+1 -1
View File
@@ -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"
+209
View File
@@ -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<RepoData> {
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<void> {
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<void> {
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<void> {
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);
});