Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,10 @@ export class VariablesController {
return { options: variable.options || [] };
}

this.logger.log(
`Fetching options for variable ${variableId} (provider ${provider.slug}, connection ${connectionId})`,
);

// Get credentials to make authenticated requests
const credentials =
await this.credentialVaultService.getDecryptedCredentials(connectionId);
Expand Down Expand Up @@ -342,11 +346,21 @@ export class VariablesController {
};

try {
this.logger.log(`Fetching options for variable ${variableId}`);
const options = await variable.fetchOptions(fetchContext);
if (options.length === 0) {
this.logger.warn(
`No options returned for variable ${variableId} (provider ${provider.slug}, connection ${connectionId})`,
);
} else {
this.logger.log(
`Fetched ${options.length} options for variable ${variableId} (provider ${provider.slug}, connection ${connectionId})`,
);
}
return { options };
} catch (error) {
this.logger.error(`Failed to fetch options: ${error}`);
this.logger.error(
`Failed to fetch options for variable ${variableId} (provider ${provider.slug}, connection ${connectionId}): ${error}`,
);
throw new HttpException(
`Failed to fetch options: ${error instanceof Error ? error.message : String(error)}`,
HttpStatus.INTERNAL_SERVER_ERROR,
Expand Down
43 changes: 23 additions & 20 deletions apps/app/src/components/integrations/ManageIntegrationDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,20 @@ const validateTargetRepos = (
return true;
}
for (const value of targetReposValue) {
const colonIndex = String(value).lastIndexOf(':');
if (colonIndex <= 0) {
const stringValue = String(value ?? '').trim();
if (!stringValue) {
return false;
}
const branch = String(value).substring(colonIndex + 1).trim();
if (!branch) {
const colonIndex = stringValue.lastIndexOf(':');
if (colonIndex === 0) {
return false;
}
if (colonIndex > 0) {
const repo = stringValue.substring(0, colonIndex).trim();
if (!repo) {
return false;
}
}
}
return true;
};
Expand Down Expand Up @@ -916,12 +922,15 @@ function MultiSelectWithBranches({
}

// For GitHub repos, preserve existing branches when repos are reselected
const newValues = selectedRepos.map((repo) => {
// Check if this repo already exists in current values
const existing = parsedConfigs.find((c) => c.repo === repo);
// Use existing branch, or empty string for new repos (user will type it)
return formatRepoBranch(repo, existing?.branch || '');
});
const newValues = selectedRepos
.map((repo) => repo.trim())
.filter(Boolean)
.map((repo) => {
// Check if this repo already exists in current values
const existing = parsedConfigs.find((c) => c.repo === repo);
// Use existing branch, or empty string for new repos (user will type it)
return formatRepoBranch(repo, existing?.branch || '');
});
onChange(newValues);
};

Expand Down Expand Up @@ -958,6 +967,7 @@ function MultiSelectWithBranches({
defaultOptions={options.map((o) => ({ value: o.value, label: o.label }))}
options={options.map((o) => ({ value: o.value, label: o.label }))}
placeholder={`Select ${variable.label.toLowerCase()}...`}
creatable={isGitHubRepos}
emptyIndicator={
isLoadingOptions ? (
<div className="flex items-center gap-2 py-2 px-3 text-sm text-muted-foreground">
Expand All @@ -974,10 +984,10 @@ function MultiSelectWithBranches({
{isGitHubRepos && parsedConfigs.length > 0 && (
<div className="space-y-2 rounded-md border border-border bg-muted/30 p-3">
<p className="text-xs font-medium text-muted-foreground">
Specify branches for each repository (comma-separated for multiple):
Optional: specify branches for each repository (comma-separated). Leave blank to use the
default branch (main).
</p>
{parsedConfigs.map((config) => {
const isEmpty = !config.branch.trim();
return (
<div key={config.repo} className="flex items-center gap-2">
<span className="shrink-0 rounded bg-secondary px-2 py-1 font-mono text-xs">
Expand All @@ -988,9 +998,7 @@ function MultiSelectWithBranches({
value={config.branch}
onChange={(e) => handleBranchChange(config.repo, e.target.value)}
placeholder="main, develop"
className={`h-8 flex-1 font-mono text-sm ${
isEmpty ? 'border-destructive bg-destructive/5 focus-visible:ring-destructive' : ''
}`}
className="h-8 flex-1 font-mono text-sm"
/>
<button
type="button"
Expand All @@ -1002,11 +1010,6 @@ function MultiSelectWithBranches({
</div>
);
})}
{parsedConfigs.some((c) => !c.branch.trim()) && (
<p className="text-xs text-destructive">
Each repository must have at least one branch specified.
</p>
)}
</div>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export interface GitHubRepo {
html_url: string;
private: boolean;
default_branch: string;
owner: { login: string };
owner: { login: string; type?: 'User' | 'Organization' };
security_and_analysis?: {
advanced_security?: { status: 'enabled' | 'disabled' };
dependabot_security_updates?: { status: 'enabled' | 'disabled' };
Expand Down
88 changes: 64 additions & 24 deletions packages/integration-platform/src/manifests/github/variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { GitHubOrg, GitHubRepo } from './types';

/**
* Variable for selecting which repositories to monitor.
* Dynamically fetches all repos from user's organizations.
* Dynamically fetches repositories the user has access to.
*
* Values are stored as `owner/repo:branch` format.
* If branch is omitted, defaults to `main`.
Expand All @@ -24,37 +24,77 @@ export const targetReposVariable: CheckVariable = {
type: 'multi-select',
required: true,
placeholder: 'Select repositories...',
helpText: 'Select repositories, then specify the branch to check for each.',
helpText: 'Select repositories and optionally specify branches (defaults to main).',
fetchOptions: async (ctx) => {
const orgs = await ctx.fetch<GitHubOrg[]>('/user/orgs');
const allRepos: Array<{ value: string; label: string }> = [];
const allRepos = new Map<string, { value: string; label: string }>();
let userReposError: unknown;
let orgReposError: unknown;

for (const org of orgs) {
const addRepo = (repo: GitHubRepo) => {
if (!repo?.full_name) return;
if (allRepos.has(repo.full_name)) return;
allRepos.set(repo.full_name, {
value: repo.full_name,
label: `${repo.full_name}${repo.private ? ' (private)' : ''}`,
});
};

try {
const allAccessibleRepos = await ctx.fetchAllPages<GitHubRepo>(
'/user/repos?affiliation=owner,collaborator,organization_member&visibility=all',
);
const orgRepos = allAccessibleRepos.filter(
(repo) => repo.owner?.type === 'Organization',
);
for (const repo of orgRepos) {
addRepo(repo);
}
} catch (error) {
userReposError = error;
}

if (allRepos.size === 0) {
try {
const repos = await ctx.fetchAllPages<GitHubRepo>(`/orgs/${org.login}/repos`);
for (const repo of repos) {
allRepos.push({
value: repo.full_name,
label: `${repo.full_name}${repo.private ? ' (private)' : ''}`,
});
const orgs = await ctx.fetchAllPages<GitHubOrg>('/user/orgs');
for (const org of orgs) {
try {
const repos = await ctx.fetchAllPages<GitHubRepo>(`/orgs/${org.login}/repos`);
for (const repo of repos) {
addRepo(repo);
}
} catch (error) {
const errorStr = String(error);
// Skip orgs with SAML SSO that haven't been authorized, or permission errors
// This allows users to still see repos from authorized orgs
if (
errorStr.includes('403') ||
errorStr.includes('SAML') ||
errorStr.includes('Forbidden')
) {
console.warn(
`Skipping organization ${org.login} due to SSO/permission error: ${errorStr}`,
);
continue;
}
// Re-throw other errors
throw error;
}
}
} catch (error) {
const errorStr = String(error);
// Skip orgs with SAML SSO that haven't been authorized, or permission errors
// This allows users to still see repos from authorized orgs
if (
errorStr.includes('403') ||
errorStr.includes('SAML') ||
errorStr.includes('Forbidden')
) {
continue;
}
// Re-throw other errors
throw error;
orgReposError = error;
}
}

if (allRepos.size === 0) {
if (userReposError) {
throw userReposError;
}
if (orgReposError) {
throw orgReposError;
}
}

return allRepos;
return Array.from(allRepos.values()).sort((a, b) => a.label.localeCompare(b.label));
},
};

Expand Down
Loading