Skip to content

Commit 7613502

Browse files
committed
chore: merge main into release for new releases
2 parents 2524a4f + c4c38db commit 7613502

4 files changed

Lines changed: 104 additions & 47 deletions

File tree

apps/api/src/integration-platform/controllers/variables.controller.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,10 @@ export class VariablesController {
223223
return { options: variable.options || [] };
224224
}
225225

226+
this.logger.log(
227+
`Fetching options for variable ${variableId} (provider ${provider.slug}, connection ${connectionId})`,
228+
);
229+
226230
// Get credentials to make authenticated requests
227231
const credentials =
228232
await this.credentialVaultService.getDecryptedCredentials(connectionId);
@@ -342,11 +346,21 @@ export class VariablesController {
342346
};
343347

344348
try {
345-
this.logger.log(`Fetching options for variable ${variableId}`);
346349
const options = await variable.fetchOptions(fetchContext);
350+
if (options.length === 0) {
351+
this.logger.warn(
352+
`No options returned for variable ${variableId} (provider ${provider.slug}, connection ${connectionId})`,
353+
);
354+
} else {
355+
this.logger.log(
356+
`Fetched ${options.length} options for variable ${variableId} (provider ${provider.slug}, connection ${connectionId})`,
357+
);
358+
}
347359
return { options };
348360
} catch (error) {
349-
this.logger.error(`Failed to fetch options: ${error}`);
361+
this.logger.error(
362+
`Failed to fetch options for variable ${variableId} (provider ${provider.slug}, connection ${connectionId}): ${error}`,
363+
);
350364
throw new HttpException(
351365
`Failed to fetch options: ${error instanceof Error ? error.message : String(error)}`,
352366
HttpStatus.INTERNAL_SERVER_ERROR,

apps/app/src/components/integrations/ManageIntegrationDialog.tsx

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,20 @@ const validateTargetRepos = (
102102
return true;
103103
}
104104
for (const value of targetReposValue) {
105-
const colonIndex = String(value).lastIndexOf(':');
106-
if (colonIndex <= 0) {
105+
const stringValue = String(value ?? '').trim();
106+
if (!stringValue) {
107107
return false;
108108
}
109-
const branch = String(value).substring(colonIndex + 1).trim();
110-
if (!branch) {
109+
const colonIndex = stringValue.lastIndexOf(':');
110+
if (colonIndex === 0) {
111111
return false;
112112
}
113+
if (colonIndex > 0) {
114+
const repo = stringValue.substring(0, colonIndex).trim();
115+
if (!repo) {
116+
return false;
117+
}
118+
}
113119
}
114120
return true;
115121
};
@@ -916,12 +922,15 @@ function MultiSelectWithBranches({
916922
}
917923

918924
// For GitHub repos, preserve existing branches when repos are reselected
919-
const newValues = selectedRepos.map((repo) => {
920-
// Check if this repo already exists in current values
921-
const existing = parsedConfigs.find((c) => c.repo === repo);
922-
// Use existing branch, or empty string for new repos (user will type it)
923-
return formatRepoBranch(repo, existing?.branch || '');
924-
});
925+
const newValues = selectedRepos
926+
.map((repo) => repo.trim())
927+
.filter(Boolean)
928+
.map((repo) => {
929+
// Check if this repo already exists in current values
930+
const existing = parsedConfigs.find((c) => c.repo === repo);
931+
// Use existing branch, or empty string for new repos (user will type it)
932+
return formatRepoBranch(repo, existing?.branch || '');
933+
});
925934
onChange(newValues);
926935
};
927936

@@ -958,6 +967,7 @@ function MultiSelectWithBranches({
958967
defaultOptions={options.map((o) => ({ value: o.value, label: o.label }))}
959968
options={options.map((o) => ({ value: o.value, label: o.label }))}
960969
placeholder={`Select ${variable.label.toLowerCase()}...`}
970+
creatable={isGitHubRepos}
961971
emptyIndicator={
962972
isLoadingOptions ? (
963973
<div className="flex items-center gap-2 py-2 px-3 text-sm text-muted-foreground">
@@ -974,10 +984,10 @@ function MultiSelectWithBranches({
974984
{isGitHubRepos && parsedConfigs.length > 0 && (
975985
<div className="space-y-2 rounded-md border border-border bg-muted/30 p-3">
976986
<p className="text-xs font-medium text-muted-foreground">
977-
Specify branches for each repository (comma-separated for multiple):
987+
Optional: specify branches for each repository (comma-separated). Leave blank to use the
988+
default branch (main).
978989
</p>
979990
{parsedConfigs.map((config) => {
980-
const isEmpty = !config.branch.trim();
981991
return (
982992
<div key={config.repo} className="flex items-center gap-2">
983993
<span className="shrink-0 rounded bg-secondary px-2 py-1 font-mono text-xs">
@@ -988,9 +998,7 @@ function MultiSelectWithBranches({
988998
value={config.branch}
989999
onChange={(e) => handleBranchChange(config.repo, e.target.value)}
9901000
placeholder="main, develop"
991-
className={`h-8 flex-1 font-mono text-sm ${
992-
isEmpty ? 'border-destructive bg-destructive/5 focus-visible:ring-destructive' : ''
993-
}`}
1001+
className="h-8 flex-1 font-mono text-sm"
9941002
/>
9951003
<button
9961004
type="button"
@@ -1002,11 +1010,6 @@ function MultiSelectWithBranches({
10021010
</div>
10031011
);
10041012
})}
1005-
{parsedConfigs.some((c) => !c.branch.trim()) && (
1006-
<p className="text-xs text-destructive">
1007-
Each repository must have at least one branch specified.
1008-
</p>
1009-
)}
10101013
</div>
10111014
)}
10121015
</div>

packages/integration-platform/src/manifests/github/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export interface GitHubRepo {
1515
html_url: string;
1616
private: boolean;
1717
default_branch: string;
18-
owner: { login: string };
18+
owner: { login: string; type?: 'User' | 'Organization' };
1919
security_and_analysis?: {
2020
advanced_security?: { status: 'enabled' | 'disabled' };
2121
dependabot_security_updates?: { status: 'enabled' | 'disabled' };

packages/integration-platform/src/manifests/github/variables.ts

Lines changed: 64 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type { GitHubOrg, GitHubRepo } from './types';
88

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

32-
for (const org of orgs) {
33+
const addRepo = (repo: GitHubRepo) => {
34+
if (!repo?.full_name) return;
35+
if (allRepos.has(repo.full_name)) return;
36+
allRepos.set(repo.full_name, {
37+
value: repo.full_name,
38+
label: `${repo.full_name}${repo.private ? ' (private)' : ''}`,
39+
});
40+
};
41+
42+
try {
43+
const allAccessibleRepos = await ctx.fetchAllPages<GitHubRepo>(
44+
'/user/repos?affiliation=owner,collaborator,organization_member&visibility=all',
45+
);
46+
const orgRepos = allAccessibleRepos.filter(
47+
(repo) => repo.owner?.type === 'Organization',
48+
);
49+
for (const repo of orgRepos) {
50+
addRepo(repo);
51+
}
52+
} catch (error) {
53+
userReposError = error;
54+
}
55+
56+
if (allRepos.size === 0) {
3357
try {
34-
const repos = await ctx.fetchAllPages<GitHubRepo>(`/orgs/${org.login}/repos`);
35-
for (const repo of repos) {
36-
allRepos.push({
37-
value: repo.full_name,
38-
label: `${repo.full_name}${repo.private ? ' (private)' : ''}`,
39-
});
58+
const orgs = await ctx.fetchAllPages<GitHubOrg>('/user/orgs');
59+
for (const org of orgs) {
60+
try {
61+
const repos = await ctx.fetchAllPages<GitHubRepo>(`/orgs/${org.login}/repos`);
62+
for (const repo of repos) {
63+
addRepo(repo);
64+
}
65+
} catch (error) {
66+
const errorStr = String(error);
67+
// Skip orgs with SAML SSO that haven't been authorized, or permission errors
68+
// This allows users to still see repos from authorized orgs
69+
if (
70+
errorStr.includes('403') ||
71+
errorStr.includes('SAML') ||
72+
errorStr.includes('Forbidden')
73+
) {
74+
console.warn(
75+
`Skipping organization ${org.login} due to SSO/permission error: ${errorStr}`,
76+
);
77+
continue;
78+
}
79+
// Re-throw other errors
80+
throw error;
81+
}
4082
}
4183
} catch (error) {
42-
const errorStr = String(error);
43-
// Skip orgs with SAML SSO that haven't been authorized, or permission errors
44-
// This allows users to still see repos from authorized orgs
45-
if (
46-
errorStr.includes('403') ||
47-
errorStr.includes('SAML') ||
48-
errorStr.includes('Forbidden')
49-
) {
50-
continue;
51-
}
52-
// Re-throw other errors
53-
throw error;
84+
orgReposError = error;
85+
}
86+
}
87+
88+
if (allRepos.size === 0) {
89+
if (userReposError) {
90+
throw userReposError;
91+
}
92+
if (orgReposError) {
93+
throw orgReposError;
5494
}
5595
}
5696

57-
return allRepos;
97+
return Array.from(allRepos.values()).sort((a, b) => a.label.localeCompare(b.label));
5898
},
5999
};
60100

0 commit comments

Comments
 (0)