in jobs/reports/teams.ts [147:302]
async function processTeam(context: IReportsContext, team: Team) {
console.log('team: ' + context.processing.teams.remaining-- + ': ' + team.organization.name + '/' + team.name);
try {
const teamContext: IReportsTeamContext = {
parent: context,
definitionsUsed: new Set(),
issues: {},
name: team.name,
nameLowercase: team.name.toLowerCase(),
team: team,
};
if (!context.teamData) {
context.teamData = [];
}
context.teamData.push(teamContext);
await getTeamDetails(teamContext);
const maintainers = await identifyTeamMaintainers(teamContext, team);
await gatherLinkData(teamContext, maintainers);
await identityMaintainersWithoutLinks(teamContext, maintainers);
const organization = team.organization;
// We do not provide reports for private engineering orgs for now
const privateEngineering = organization.privateEngineering;
// Some organizations, such as the .NET Foundation, will allow external community members to
// be organization members
const externalMembersPermitted = organization.externalMembersPermitted;
const slug = team.slug;
const basicTeam: IReportsBasicTeam = {
teamName: team.name,
teamSlug: slug,
entityName: team.name,
orgName: organization.name,
id: team.id.toString(),
};
const orgName = team.organization.name;
// Recipients
teamContext.recipients = [];
const corporateAdministrators = [];
if (teamContext.maintainers) {
for (let y = 0; y < teamContext.maintainers.length; y++) {
const admin = teamContext.maintainers[y];
if (admin && admin.link && admin.link.aadupn) {
corporateAdministrators.push(admin.link.aadupn);
if (!privateEngineering) {
// Private engineering orgs do not send individuals nags on emails for now
teamContext.recipients.push({
type: 'upn',
value: admin.link.aadupn,
reasons: transformReasonsToArray(admin, team.name, orgName),
});
}
}
}
}
// Send to org admins
const orgData = context.organizationData[orgName];
for (let i = 0; orgData && orgData.organizationContext && orgData.organizationContext.recipients && orgData.organizationContext.recipients.length && i < orgData.organizationContext.recipients.length; i++) {
teamContext.recipients.push(orgData.organizationContext.recipients[i]);
}
basicTeam.maintainers = teamContext.maintainers && teamContext.maintainers.length ? teamContext.maintainers.length.toString() : 'None';
const actionPromoteMembers = {
link: `https://github.com/orgs/${orgName}/teams/${slug}/members`,
text: 'Promote members',
};
const actionRemoveMembers = {
link: `https://github.com/orgs/${orgName}/teams/${slug}/members`,
text: 'Remove members',
};
const actionDelete = {
link: `https://github.com/orgs/${orgName}/teams/${slug}/edit`,
text: 'Consider deleting',
};
const actionView = {
link: `https://github.com/orgs/${orgName}/teams/${slug}/members`,
text: 'Open',
};
const actionViewInPortal = context.config.urls ? {
link: `${context.config.urls.repos}${organization.name}/teams/${slug}`,
text: 'Manage team',
} : null;
let createdAt = team.created_at ? moment(team.created_at) : null;
if (createdAt) {
basicTeam.created = createdAt.format(simpleDateFormat);
}
let updatedAt = team.updated_at ? moment(team.updated_at) : null;
if (updatedAt) {
basicTeam.updated = updatedAt.format(simpleDateFormat);
}
let mostRecentActivityMoment = createdAt;
let mostRecentActivity = 'Created';
if (updatedAt && updatedAt.isAfter(mostRecentActivityMoment)) {
mostRecentActivity = 'Updated';
mostRecentActivityMoment = updatedAt;
}
// Completely empty standard teams (we exclude system teams that may be used for specific portal permissions)
const systemTeamIds = team.organization.systemTeamIds;
const isSystemTeam = systemTeamIds.includes(team.id);
if (team.members_count == 0 && team.repos_count == 0 && !isSystemTeam) {
addEntityToIssueType(context, teamContext, 'emptyTeams', basicTeam, actionDelete);
} else {
// Member or maintainer issues
if (!isSystemTeam && team.members_count == 0) {
addEntityToIssueType(context, teamContext, 'noTeamMembers', basicTeam, actionDelete, actionViewInPortal);
} else if (!isSystemTeam && teamContext.teamMaintainers.length === 0) {
addEntityToIssueType(context, teamContext, 'noTeamMaintainers', basicTeam, actionPromoteMembers, actionViewInPortal);
} else if (teamContext.maintainersByType.unlinked.length > 0) {
const logins = [];
for (let z = 0; z < teamContext.maintainersByType.unlinked.length; z++) {
const unlinkedEntry = teamContext.maintainersByType.unlinked[z];
logins.push(unlinkedEntry.login);
}
const specialEntity = Object.assign({
logins: logins.join(', '),
}, basicTeam);
const reportName = externalMembersPermitted ? 'unlinkedMaintainersWhenAllowed' : 'unlinkedMaintainers';
// TODO: use the operations e-mail
addEntityToIssueType(context, teamContext, reportName, specialEntity, actionRemoveMembers, {
link: `mailto:opensource@microsoft.com?subject=Reporting a former employee related to the ${orgName} ${team.name} team`,
text: 'Former employee?',
});
}
// No repositories
if (team.repos_count <= 0 && !isSystemTeam) {
addEntityToIssueType(context, teamContext, 'TeamsWithoutRepositories', basicTeam, actionViewInPortal);
}
}
const thisWeek = moment().subtract(7, 'days');
const today = moment().subtract(1, 'days');
const ageInMonths = today.diff(createdAt, 'months');
if (ageInMonths > 0) {
basicTeam.ageInMonths = ageInMonths === 1 ? '1 month' : ageInMonths + ' months';
}
const monthsSinceUpdates = today.diff(mostRecentActivityMoment, 'months');
const timeAsString = monthsSinceUpdates + ' month' + (monthsSinceUpdates === 1 ? '' : 's');
basicTeam.recentActivity = monthsSinceUpdates < 1 ? 'Active' : `${timeAsString} (${mostRecentActivity})`;
if (createdAt.isAfter(thisWeek) && !privateEngineering) {
// New public and private repos
const teamForManagerAndLawyer = shallowCloneWithAdditionalRecipients(basicTeam, teamContext.additionalRecipients);
if (createdAt.isAfter(today)) {
addEntityToIssueType(context, teamContext, 'NewTeamsToday', teamForManagerAndLawyer, actionView, actionViewInPortal);
}
// Always include in the weekly summary
addEntityToIssueType(context, teamContext, 'NewTeamsWeek', teamForManagerAndLawyer, actionView, actionViewInPortal);
}
if (context.settings.teamDelayAfter) {
await sleep(context.settings.teamDelayAfter);
}
return context;
} catch (problem) {
if (ErrorHelper.IsNotFound(problem)) {
// Missing teams are OK to not spew too many errors about...
} else {
console.dir(problem);
}
throw problem;
}
}