Skip to content

Conversation

liguori
Copy link

@liguori liguori commented Sep 19, 2025

This pull request adds support for distinguishing between "Full GitHub Enterprises" and "Copilot Business Only" enterprises, enabling more accurate team retrieval and metrics for each enterprise type. The change introduces a new enterpriseType option throughout the configuration, API, and model layers, updates documentation, and provides comprehensive tests for the new logic.

Enterprise Type Support and API Logic:

  • Added NUXT_PUBLIC_ENTERPRISE_TYPE environment variable and propagated its usage throughout the codebase to distinguish between full and copilot-only enterprise types, affecting how teams and metrics are retrieved. (.env, nuxt.config.ts, [1] [2]
  • Updated Options model to include enterpriseType, with serialization, deserialization, and merging logic, and modified API URL construction to support both enterprise types for team and enterprise scopes. (app/model/Options.ts, [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13] [14]

Backend Team Retrieval Logic:

  • Enhanced the teams API handler to use GraphQL for enumerating organizations and fetching teams for full enterprises, while maintaining existing logic for copilot-only enterprises and organizations. (server/api/teams.ts, [1] [2] [3] [4]

Documentation Updates:

  • Updated deployment and README documentation to explain the new enterprise type configuration, usage, and behavioral differences for team retrieval and metrics. (DEPLOYMENT.md, [1]; README.md, [2]

Testing:

  • Added comprehensive tests for Options enterprise type handling, including serialization, merging, API URL logic, and error cases. (tests/enterprise-type.spec.ts, tests/enterprise-type.spec.tsR1-R122)

@karpikpl as discussed this is a first implementation created with the help of Coding Agent and tested by myself in a Codespaces, should start to address #271, please verify it.

NOTE: A new scope is required for the GitHub Token (read:org)

@karpikpl karpikpl requested a review from Copilot September 20, 2025 19:00
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces support for distinguishing between "Full GitHub Enterprises" and "Copilot Business Only" enterprises to optimize team retrieval and metrics access. The implementation adds a new enterpriseType configuration option that affects how teams are fetched - using GraphQL to enumerate organizations for full enterprises versus using enterprise-level APIs for copilot-only enterprises.

Key changes include:

  • Added enterpriseType configuration with values full or copilot-only (defaulting to copilot-only for backward compatibility)
  • Enhanced team retrieval logic to use GraphQL for full enterprises and existing enterprise APIs for copilot-only
  • Updated API URL construction to handle different enterprise types appropriately

Reviewed Changes

Copilot reviewed 7 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/enterprise-type.spec.ts Comprehensive test suite covering enterprise type handling, serialization, and API URL generation
server/api/teams.ts Enhanced teams API with GraphQL support for full enterprises and organization enumeration
nuxt.config.ts Added default enterpriseType configuration
app/model/Options.ts Extended Options class with enterprise type support and API URL logic
README.md Updated documentation explaining enterprise type differences
DEPLOYMENT.md Added deployment examples for both enterprise types
.env Added environment variable configuration for enterprise type

logger.info(`Fetching organizations for full enterprise ${options.githubEnt} using GraphQL`)

const graphqlQuery = {
query: `query { enterprise(slug: "${options.githubEnt}") { organizations(first: 100) { nodes { login name url } } } }`
Copy link

Copilot AI Sep 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Direct string interpolation in GraphQL query creates a potential injection vulnerability. The enterprise slug should be properly escaped or parameterized to prevent GraphQL injection attacks.

Suggested change
query: `query { enterprise(slug: "${options.githubEnt}") { organizations(first: 100) { nodes { login name url } } } }`
query: `query($slug: String!) { enterprise(slug: $slug) { organizations(first: 100) { nodes { login name url } } } }`,
variables: { slug: options.githubEnt }

Copilot uses AI. Check for mistakes.

Comment on lines +95 to +107
const graphqlQuery = {
query: `query { enterprise(slug: "${options.githubEnt}") { organizations(first: 100) { nodes { login name url } } } }`
}

const graphqlResponse = await $fetch<GraphQLResponse>('https://api.github.com/graphql', {
method: 'POST',
headers: event.context.headers,
body: JSON.stringify(graphqlQuery)
})

const data = res._data as GitHubTeam[]
for (const t of data) {
const name: string = t.name
const slug: string = t.slug
const description: string = t.description || ''
if (name && slug) allTeams.push({ name, slug, description })

const organizations = graphqlResponse.data.enterprise.organizations.nodes
logger.info(`Found ${organizations.length} organizations in enterprise`)

Copy link

Copilot AI Sep 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded limit of 100 organizations could be problematic for large enterprises. Consider implementing pagination or making this configurable to handle enterprises with more than 100 organizations.

Suggested change
const graphqlQuery = {
query: `query { enterprise(slug: "${options.githubEnt}") { organizations(first: 100) { nodes { login name url } } } }`
}
const graphqlResponse = await $fetch<GraphQLResponse>('https://api.github.com/graphql', {
method: 'POST',
headers: event.context.headers,
body: JSON.stringify(graphqlQuery)
})
const data = res._data as GitHubTeam[]
for (const t of data) {
const name: string = t.name
const slug: string = t.slug
const description: string = t.description || ''
if (name && slug) allTeams.push({ name, slug, description })
const organizations = graphqlResponse.data.enterprise.organizations.nodes
logger.info(`Found ${organizations.length} organizations in enterprise`)
// Paginate through all organizations in the enterprise
let organizations: GraphQLOrganization[] = [];
let hasNextPage = true;
let endCursor: string | null = null;
while (hasNextPage) {
const graphqlQuery = {
query: `query {
enterprise(slug: "${options.githubEnt}") {
organizations(first: 100${endCursor ? `, after: "${endCursor}"` : ''}) {
nodes { login name url }
pageInfo { hasNextPage endCursor }
}
}
}`
};
const graphqlResponse = await $fetch<any>('https://api.github.com/graphql', {
method: 'POST',
headers: event.context.headers,
body: JSON.stringify(graphqlQuery)
});
const orgs = graphqlResponse.data.enterprise.organizations.nodes;
organizations = organizations.concat(orgs);
const pageInfo = graphqlResponse.data.enterprise.organizations.pageInfo;
hasNextPage = pageInfo.hasNextPage;
endCursor = pageInfo.endCursor;
}
logger.info(`Found ${organizations.length} organizations in enterprise`);

Copilot uses AI. Check for mistakes.

Comment on lines +307 to +310
const teamParts = this.githubTeam.split(' - ');
if (teamParts.length >= 2) {
const orgName = teamParts[0];
const teamName = teamParts[1];
Copy link

Copilot AI Sep 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The team slug parsing logic uses teamParts.length >= 2 but only extracts the first two parts. If a team name contains ' - ', this could lead to incorrect parsing. Consider using a more robust parsing approach like split(' - ', 2) or joining remaining parts for the team name.

Suggested change
const teamParts = this.githubTeam.split(' - ');
if (teamParts.length >= 2) {
const orgName = teamParts[0];
const teamName = teamParts[1];
const separator = ' - ';
const sepIndex = this.githubTeam.indexOf(separator);
if (sepIndex > 0) {
const orgName = this.githubTeam.substring(0, sepIndex);
const teamName = this.githubTeam.substring(sepIndex + separator.length);

Copilot uses AI. Check for mistakes.

githubOrg: '',
githubEnt: '',
githubTeam: '',
enterpriseType: 'copilot-only', // can be overridden by NUXT_PUBLIC_ENTERPRISE_TYPE environment variable
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think default should be 'full'

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree too, but i left like it in order to have the same behavior as the current implementation. Anyway, this new flag could be documented for deployment.

const teamName = teamParts[1];
url = `${baseUrl}/orgs/${orgName}/team/${teamName}/copilot/metrics`;
} else {
throw new Error('Team slug must be in format "org-name - team-name" for full enterprise scope');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@liguori are you sure team slug is going to be in this format?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No i think here it was Copilot Agent mistake, I didn't have chance to review it. This code must be tested with all the combinations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants