-
Notifications
You must be signed in to change notification settings - Fork 0
Create GitHub teams in the background of Org requests #338
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
84cf016
27f9674
25f5bf9
0d3893c
e1c4867
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,169 @@ | ||
| import { AvailableSQSFunctions } from "common/types/sqsMessage.js"; | ||
| import { currentEnvironmentConfig, SQSHandlerFunction } from "../index.js"; | ||
| import { | ||
| DynamoDBClient, | ||
| GetItemCommand, | ||
| TransactWriteItemsCommand, | ||
| } from "@aws-sdk/client-dynamodb"; | ||
| import { genericConfig, SecretConfig } from "common/config.js"; | ||
| import { getSecretConfig } from "../utils.js"; | ||
| import RedisModule from "ioredis"; | ||
| import { createLock, IoredisAdapter } from "redlock-universal"; | ||
| import { marshall, unmarshall } from "@aws-sdk/util-dynamodb"; | ||
| import { InternalServerError } from "common/errors/index.js"; | ||
| import { | ||
| assignIdpGroupsToTeam, | ||
| createGithubTeam, | ||
| } from "api/functions/github.js"; | ||
| import { buildAuditLogTransactPut } from "api/functions/auditLog.js"; | ||
| import { Modules } from "common/modules.js"; | ||
| import { retryDynamoTransactionWithBackoff } from "api/utils.js"; | ||
| import { SKIP_EXTERNAL_ORG_LEAD_UPDATE } from "common/overrides.js"; | ||
| import { getOrgByName } from "@acm-uiuc/js-shared"; | ||
|
|
||
| export const createOrgGithubTeamHandler: SQSHandlerFunction< | ||
| AvailableSQSFunctions.CreateOrgGithubTeam | ||
| > = async (payload, metadata, logger) => { | ||
| const secretConfig: SecretConfig = await getSecretConfig({ | ||
| logger, | ||
| commonConfig: { region: genericConfig.AwsRegion }, | ||
| }); | ||
| const redisClient = new RedisModule.default(secretConfig.redis_url); | ||
| try { | ||
| const { orgName, githubTeamName, githubTeamDescription } = payload; | ||
| const orgImmutableId = getOrgByName(orgName)!.id; | ||
| if (SKIP_EXTERNAL_ORG_LEAD_UPDATE.includes(orgImmutableId)) { | ||
| logger.info( | ||
| `Organization ${orgName} has external updates disabled, exiting.`, | ||
| ); | ||
| return; | ||
| } | ||
| const dynamo = new DynamoDBClient({ | ||
| region: genericConfig.AwsRegion, | ||
| }); | ||
| const lock = createLock({ | ||
| adapter: new IoredisAdapter(redisClient), | ||
| key: `createOrgGithubTeamHandler:${orgImmutableId}`, | ||
| retryAttempts: 5, | ||
| retryDelay: 300, | ||
| }); | ||
| return await lock.using(async (signal) => { | ||
| const getMetadataCommand = new GetItemCommand({ | ||
| TableName: genericConfig.SigInfoTableName, | ||
| Key: marshall({ | ||
| primaryKey: `DEFINE#${orgName}`, | ||
| entryId: "0", | ||
| }), | ||
| ProjectionExpression: "#entra,#gh", | ||
| ExpressionAttributeNames: { | ||
| "#entra": "leadsEntraGroupId", | ||
| "#gh": "leadsGithubTeamId", | ||
| }, | ||
| ConsistentRead: true, | ||
| }); | ||
| const existingData = await dynamo.send(getMetadataCommand); | ||
| if (!existingData || !existingData.Item) { | ||
| throw new InternalServerError({ | ||
| message: `Could not find org entry for ${orgName}`, | ||
| }); | ||
| } | ||
| const currentOrgInfo = unmarshall(existingData.Item) as { | ||
| leadsEntraGroupId?: string; | ||
| leadsGithubTeamId?: string; | ||
| }; | ||
| if (!currentOrgInfo.leadsEntraGroupId) { | ||
| logger.info(`${orgName} does not have an Entra group, skipping!`); | ||
| return; | ||
| } | ||
| if (currentOrgInfo.leadsGithubTeamId) { | ||
| logger.info("This org already has a GitHub team, skipping"); | ||
| return; | ||
| } | ||
| if (signal.aborted) { | ||
| throw new InternalServerError({ | ||
| message: | ||
| "Checked on lock before creating GitHub team, we've lost the lock!", | ||
| }); | ||
| } | ||
| logger.info(`Creating GitHub team for ${orgName}!`); | ||
| const suffix = currentEnvironmentConfig.GroupEmailSuffix; | ||
| const finalName = `${githubTeamName}${suffix === "" ? "" : `-${suffix}`}`; | ||
| const { updated, id: teamId } = await createGithubTeam({ | ||
| orgId: currentEnvironmentConfig.GithubOrgName, | ||
| githubToken: secretConfig.github_pat, | ||
| parentTeamId: currentEnvironmentConfig.ExecGithubTeam, | ||
| name: finalName, | ||
| description: githubTeamDescription, | ||
| logger, | ||
| }); | ||
| if (!updated) { | ||
| logger.info( | ||
| `Github team "${finalName}" already existed. We're assuming team sync was already set up (if not, please configure manually).`, | ||
| ); | ||
| } else { | ||
| logger.info( | ||
| `Github team "${finalName}" created with team ID "${teamId}".`, | ||
| ); | ||
| if (currentEnvironmentConfig.GithubIdpSyncEnabled) { | ||
| logger.info( | ||
| `Setting up IDP sync for Github team from Entra ID group ${currentOrgInfo.leadsEntraGroupId}`, | ||
| ); | ||
| await assignIdpGroupsToTeam({ | ||
| githubToken: secretConfig.github_pat, | ||
| teamId, | ||
| logger, | ||
| groupsToSync: [currentOrgInfo.leadsEntraGroupId], | ||
| orgId: currentEnvironmentConfig.GithubOrgId, | ||
| orgName: currentEnvironmentConfig.GithubOrgName, | ||
| }); | ||
| } | ||
| } | ||
| logger.info("Adding updates to audit log"); | ||
| const logStatement = updated | ||
| ? buildAuditLogTransactPut({ | ||
| entry: { | ||
| module: Modules.ORG_INFO, | ||
| message: `Created GitHub team "${finalName}" for organization leads.`, | ||
| actor: metadata.initiator, | ||
| target: orgName, | ||
| }, | ||
| }) | ||
| : undefined; | ||
| const storeGithubIdOperation = async () => { | ||
| const commandTransaction = new TransactWriteItemsCommand({ | ||
| TransactItems: [ | ||
| ...(logStatement ? [logStatement] : []), | ||
| { | ||
| Update: { | ||
| TableName: genericConfig.SigInfoTableName, | ||
| Key: marshall({ | ||
| primaryKey: `DEFINE#${orgName}`, | ||
| entryId: "0", | ||
| }), | ||
| UpdateExpression: | ||
| "SET leadsGithubTeamId = :githubTeamId, updatedAt = :updatedAt", | ||
| ExpressionAttributeValues: marshall({ | ||
| ":githubTeamId": teamId, | ||
| ":updatedAt": new Date().toISOString(), | ||
| }), | ||
| }, | ||
| }, | ||
| ], | ||
| }); | ||
| return await dynamo.send(commandTransaction); | ||
| }; | ||
|
|
||
| await retryDynamoTransactionWithBackoff( | ||
| storeGithubIdOperation, | ||
| logger, | ||
| `Store GitHub team ID for ${orgName}`, | ||
| ); | ||
| }); | ||
| } finally { | ||
| try { | ||
| await redisClient.quit(); | ||
| } catch { | ||
| redisClient.disconnect(); | ||
| } | ||
| } | ||
| }; | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -3,3 +3,4 @@ export { emailMembershipPassHandler } from "./emailMembershipPassHandler.js"; | |||||
| export { provisionNewMemberHandler } from "./provisionNewMember.js"; | ||||||
| export { sendSaleEmailHandler } from "./sendSaleEmailHandler.js"; | ||||||
| export { emailNotificationsHandler } from "./emailNotifications.js"; | ||||||
| export { createOrgGithubTeamHandler } from "./createOrgGithubTeam.js"; | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Resolve ESLint “import/extensions” on re-export Consistent with other exports in this file, the “.js” extension is needed for NodeNext ESM. If the rule is strict, silence it inline: -export { createOrgGithubTeamHandler } from "./createOrgGithubTeam.js";
+export { createOrgGithubTeamHandler } from "./createOrgGithubTeam.js"; // eslint-disable-line import/extensionsAlternatively, relax the rule for TS files in .eslintrc when using "moduleResolution": "nodenext". 📝 Committable suggestion
Suggested change
🧰 Tools🪛 ESLint[error] 6-6: Unexpected use of file extension "js" for "./createOrgGithubTeam.js" (import/extensions) 🤖 Prompt for AI Agents |
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,6 +22,7 @@ import { | |
| import { ValidationError } from "../../common/errors/index.js"; | ||
| import { RunEnvironment } from "../../common/roles.js"; | ||
| import { environmentConfig } from "../../common/config.js"; | ||
| import { createOrgGithubTeamHandler } from "./handlers/createOrgGithubTeam.js"; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix ESLint import/extensions and consolidate handler import ESLint flags the explicit “.js” on the direct handler import. To avoid the rule while keeping NodeNext-friendly imports, pull the symbol from the existing handlers barrel import and drop the extra line. -import { createOrgGithubTeamHandler } from "./handlers/createOrgGithubTeam.js";
// ...
-import {
+import {
emailMembershipPassHandler,
pingHandler,
provisionNewMemberHandler,
sendSaleEmailHandler,
- emailNotificationsHandler,
+ emailNotificationsHandler,
+ createOrgGithubTeamHandler,
} from "./handlers/index.js";
// ...
[AvailableSQSFunctions.EmailNotifications]: emailNotificationsHandler,
[AvailableSQSFunctions.CreateOrgGithubTeam]: createOrgGithubTeamHandler,Additionally, consider awaiting the handler so the “Finished” log prints after completion: - const result = func(
+ const result = await func(
parsedBody.payload,
parsedBody.metadata,
childLogger,
);Also applies to: 43-44 🧰 Tools🪛 ESLint[error] 25-25: Unexpected use of file extension "js" for "./handlers/createOrgGithubTeam.js" (import/extensions) 🤖 Prompt for AI Agents |
||
|
|
||
| export type SQSFunctionPayloadTypes = { | ||
| [K in keyof typeof sqsPayloadSchemas]: SQSHandlerFunction<K>; | ||
|
|
@@ -39,6 +40,7 @@ const handlers: SQSFunctionPayloadTypes = { | |
| [AvailableSQSFunctions.ProvisionNewMember]: provisionNewMemberHandler, | ||
| [AvailableSQSFunctions.SendSaleEmail]: sendSaleEmailHandler, | ||
| [AvailableSQSFunctions.EmailNotifications]: emailNotificationsHandler, | ||
| [AvailableSQSFunctions.CreateOrgGithubTeam]: createOrgGithubTeamHandler, | ||
| }; | ||
| export const runEnvironment = process.env.RunEnvironment as RunEnvironment; | ||
| export const currentEnvironmentConfig = environmentConfig[runEnvironment]; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid non-null assertion on getOrgByName; harden against malformed payloads.
A bad SQS payload would crash the worker. Guard and emit a clear error.
🤖 Prompt for AI Agents