use warnings instead of notFound object

This commit is contained in:
bkellam 2025-10-26 02:26:33 -04:00
parent c6a9569309
commit 88ef3fdae0
9 changed files with 266 additions and 316 deletions

View file

@ -47,47 +47,39 @@ export const getAzureDevOpsReposFromConfig = async (
const useTfsPath = config.useTfsPath || false; const useTfsPath = config.useTfsPath || false;
let allRepos: GitRepository[] = []; let allRepos: GitRepository[] = [];
let notFound: { let allWarnings: string[] = [];
users: string[],
orgs: string[],
repos: string[],
} = {
users: [],
orgs: [],
repos: [],
};
if (config.orgs) { if (config.orgs) {
const { validRepos, notFoundOrgs } = await getReposForOrganizations( const { repos, warnings } = await getReposForOrganizations(
config.orgs, config.orgs,
baseUrl, baseUrl,
token, token,
useTfsPath useTfsPath
); );
allRepos = allRepos.concat(validRepos); allRepos = allRepos.concat(repos);
notFound.orgs = notFoundOrgs; allWarnings = allWarnings.concat(warnings);
} }
if (config.projects) { if (config.projects) {
const { validRepos, notFoundProjects } = await getReposForProjects( const { repos, warnings } = await getReposForProjects(
config.projects, config.projects,
baseUrl, baseUrl,
token, token,
useTfsPath useTfsPath
); );
allRepos = allRepos.concat(validRepos); allRepos = allRepos.concat(repos);
notFound.repos = notFound.repos.concat(notFoundProjects); allWarnings = allWarnings.concat(warnings);
} }
if (config.repos) { if (config.repos) {
const { validRepos, notFoundRepos } = await getRepos( const { repos, warnings } = await getRepos(
config.repos, config.repos,
baseUrl, baseUrl,
token, token,
useTfsPath useTfsPath
); );
allRepos = allRepos.concat(validRepos); allRepos = allRepos.concat(repos);
notFound.repos = notFound.repos.concat(notFoundRepos); allWarnings = allWarnings.concat(warnings);
} }
let repos = allRepos let repos = allRepos
@ -103,8 +95,8 @@ export const getAzureDevOpsReposFromConfig = async (
logger.debug(`Found ${repos.length} total repositories.`); logger.debug(`Found ${repos.length} total repositories.`);
return { return {
validRepos: repos, repos,
notFound, warnings: allWarnings,
}; };
}; };
@ -221,10 +213,11 @@ async function getReposForOrganizations(
// Check if it's a 404-like error (organization not found) // Check if it's a 404-like error (organization not found)
if (error && typeof error === 'object' && 'statusCode' in error && error.statusCode === 404) { if (error && typeof error === 'object' && 'statusCode' in error && error.statusCode === 404) {
logger.error(`Organization ${org} not found or no access`); const warning = `Organization ${org} not found or no access`;
logger.warn(warning);
return { return {
type: 'notFound' as const, type: 'warning' as const,
value: org warning
}; };
} }
throw error; throw error;
@ -232,11 +225,11 @@ async function getReposForOrganizations(
})); }));
throwIfAnyFailed(results); throwIfAnyFailed(results);
const { validItems: validRepos, notFoundItems: notFoundOrgs } = processPromiseResults<GitRepository>(results); const { validItems: repos, warnings } = processPromiseResults<GitRepository>(results);
return { return {
validRepos, repos,
notFoundOrgs, warnings,
}; };
} }
@ -274,10 +267,11 @@ async function getReposForProjects(
logger.error(`Failed to fetch repositories for project ${project}.`, error); logger.error(`Failed to fetch repositories for project ${project}.`, error);
if (error && typeof error === 'object' && 'statusCode' in error && error.statusCode === 404) { if (error && typeof error === 'object' && 'statusCode' in error && error.statusCode === 404) {
logger.error(`Project ${project} not found or no access`); const warning = `Project ${project} not found or no access`;
logger.warn(warning);
return { return {
type: 'notFound' as const, type: 'warning' as const,
value: project warning
}; };
} }
throw error; throw error;
@ -285,11 +279,11 @@ async function getReposForProjects(
})); }));
throwIfAnyFailed(results); throwIfAnyFailed(results);
const { validItems: validRepos, notFoundItems: notFoundProjects } = processPromiseResults<GitRepository>(results); const { validItems: repos, warnings } = processPromiseResults<GitRepository>(results);
return { return {
validRepos, repos,
notFoundProjects, warnings,
}; };
} }
@ -328,10 +322,11 @@ async function getRepos(
logger.error(`Failed to fetch repository ${repo}.`, error); logger.error(`Failed to fetch repository ${repo}.`, error);
if (error && typeof error === 'object' && 'statusCode' in error && error.statusCode === 404) { if (error && typeof error === 'object' && 'statusCode' in error && error.statusCode === 404) {
logger.error(`Repository ${repo} not found or no access`); const warning = `Repository ${repo} not found or no access`;
logger.warn(warning);
return { return {
type: 'notFound' as const, type: 'warning' as const,
value: repo warning
}; };
} }
throw error; throw error;
@ -339,10 +334,10 @@ async function getRepos(
})); }));
throwIfAnyFailed(results); throwIfAnyFailed(results);
const { validItems: validRepos, notFoundItems: notFoundRepos } = processPromiseResults<GitRepository>(results); const { validItems: repos, warnings } = processPromiseResults<GitRepository>(results);
return { return {
validRepos, repos,
notFoundRepos, warnings,
}; };
} }

View file

@ -27,9 +27,9 @@ interface BitbucketClient {
apiClient: any; apiClient: any;
baseUrl: string; baseUrl: string;
gitUrl: string; gitUrl: string;
getReposForWorkspace: (client: BitbucketClient, workspaces: string[]) => Promise<{validRepos: BitbucketRepository[], notFoundWorkspaces: string[]}>; getReposForWorkspace: (client: BitbucketClient, workspaces: string[]) => Promise<{repos: BitbucketRepository[], warnings: string[]}>;
getReposForProjects: (client: BitbucketClient, projects: string[]) => Promise<{validRepos: BitbucketRepository[], notFoundProjects: string[]}>; getReposForProjects: (client: BitbucketClient, projects: string[]) => Promise<{repos: BitbucketRepository[], warnings: string[]}>;
getRepos: (client: BitbucketClient, repos: string[]) => Promise<{validRepos: BitbucketRepository[], notFoundRepos: string[]}>; getRepos: (client: BitbucketClient, repos: string[]) => Promise<{repos: BitbucketRepository[], warnings: string[]}>;
shouldExcludeRepo: (repo: BitbucketRepository, config: BitbucketConnectionConfig) => boolean; shouldExcludeRepo: (repo: BitbucketRepository, config: BitbucketConnectionConfig) => boolean;
} }
@ -71,32 +71,24 @@ export const getBitbucketReposFromConfig = async (config: BitbucketConnectionCon
cloudClient(config.user, token); cloudClient(config.user, token);
let allRepos: BitbucketRepository[] = []; let allRepos: BitbucketRepository[] = [];
let notFound: { let allWarnings: string[] = [];
orgs: string[],
users: string[],
repos: string[],
} = {
orgs: [],
users: [],
repos: [],
};
if (config.workspaces) { if (config.workspaces) {
const { validRepos, notFoundWorkspaces } = await client.getReposForWorkspace(client, config.workspaces); const { repos, warnings } = await client.getReposForWorkspace(client, config.workspaces);
allRepos = allRepos.concat(validRepos); allRepos = allRepos.concat(repos);
notFound.orgs = notFoundWorkspaces; allWarnings = allWarnings.concat(warnings);
} }
if (config.projects) { if (config.projects) {
const { validRepos, notFoundProjects } = await client.getReposForProjects(client, config.projects); const { repos, warnings } = await client.getReposForProjects(client, config.projects);
allRepos = allRepos.concat(validRepos); allRepos = allRepos.concat(repos);
notFound.orgs = notFoundProjects; allWarnings = allWarnings.concat(warnings);
} }
if (config.repos) { if (config.repos) {
const { validRepos, notFoundRepos } = await client.getRepos(client, config.repos); const { repos, warnings } = await client.getRepos(client, config.repos);
allRepos = allRepos.concat(validRepos); allRepos = allRepos.concat(repos);
notFound.repos = notFoundRepos; allWarnings = allWarnings.concat(warnings);
} }
const filteredRepos = allRepos.filter((repo) => { const filteredRepos = allRepos.filter((repo) => {
@ -104,8 +96,8 @@ export const getBitbucketReposFromConfig = async (config: BitbucketConnectionCon
}); });
return { return {
validRepos: filteredRepos, repos: filteredRepos,
notFound, warnings: allWarnings,
}; };
} }
@ -186,7 +178,7 @@ function parseUrl(url: string): { path: string; query: Record<string, string>; }
} }
async function cloudGetReposForWorkspace(client: BitbucketClient, workspaces: string[]): Promise<{validRepos: CloudRepository[], notFoundWorkspaces: string[]}> { async function cloudGetReposForWorkspace(client: BitbucketClient, workspaces: string[]): Promise<{repos: CloudRepository[], warnings: string[]}> {
const results = await Promise.allSettled(workspaces.map(async (workspace) => { const results = await Promise.allSettled(workspaces.map(async (workspace) => {
try { try {
logger.debug(`Fetching all repos for workspace ${workspace}...`); logger.debug(`Fetching all repos for workspace ${workspace}...`);
@ -221,10 +213,11 @@ async function cloudGetReposForWorkspace(client: BitbucketClient, workspaces: st
const status = e?.cause?.response?.status; const status = e?.cause?.response?.status;
if (status == 404) { if (status == 404) {
logger.error(`Workspace ${workspace} not found or invalid access`) const warning = `Workspace ${workspace} not found or invalid access`;
logger.warn(warning);
return { return {
type: 'notFound' as const, type: 'warning' as const,
value: workspace warning
} }
} }
throw e; throw e;
@ -232,21 +225,22 @@ async function cloudGetReposForWorkspace(client: BitbucketClient, workspaces: st
})); }));
throwIfAnyFailed(results); throwIfAnyFailed(results);
const { validItems: validRepos, notFoundItems: notFoundWorkspaces } = processPromiseResults(results); const { validItems: repos, warnings } = processPromiseResults(results);
return { return {
validRepos, repos,
notFoundWorkspaces, warnings,
}; };
} }
async function cloudGetReposForProjects(client: BitbucketClient, projects: string[]): Promise<{validRepos: CloudRepository[], notFoundProjects: string[]}> { async function cloudGetReposForProjects(client: BitbucketClient, projects: string[]): Promise<{repos: CloudRepository[], warnings: string[]}> {
const results = await Promise.allSettled(projects.map(async (project) => { const results = await Promise.allSettled(projects.map(async (project) => {
const [workspace, project_name] = project.split('/'); const [workspace, project_name] = project.split('/');
if (!workspace || !project_name) { if (!workspace || !project_name) {
logger.error(`Invalid project ${project}`); const warning = `Invalid project ${project}`;
logger.warn(warning);
return { return {
type: 'notFound' as const, type: 'warning' as const,
value: project warning
} }
} }
@ -282,10 +276,11 @@ async function cloudGetReposForProjects(client: BitbucketClient, projects: strin
const status = e?.cause?.response?.status; const status = e?.cause?.response?.status;
if (status == 404) { if (status == 404) {
logger.error(`Project ${project_name} not found in ${workspace} or invalid access`) const warning = `Project ${project_name} not found in ${workspace} or invalid access`;
logger.warn(warning);
return { return {
type: 'notFound' as const, type: 'warning' as const,
value: project warning
} }
} }
throw e; throw e;
@ -293,21 +288,22 @@ async function cloudGetReposForProjects(client: BitbucketClient, projects: strin
})); }));
throwIfAnyFailed(results); throwIfAnyFailed(results);
const { validItems: validRepos, notFoundItems: notFoundProjects } = processPromiseResults(results); const { validItems: repos, warnings } = processPromiseResults(results);
return { return {
validRepos, repos,
notFoundProjects warnings
} }
} }
async function cloudGetRepos(client: BitbucketClient, repos: string[]): Promise<{validRepos: CloudRepository[], notFoundRepos: string[]}> { async function cloudGetRepos(client: BitbucketClient, repoList: string[]): Promise<{repos: CloudRepository[], warnings: string[]}> {
const results = await Promise.allSettled(repos.map(async (repo) => { const results = await Promise.allSettled(repoList.map(async (repo) => {
const [workspace, repo_slug] = repo.split('/'); const [workspace, repo_slug] = repo.split('/');
if (!workspace || !repo_slug) { if (!workspace || !repo_slug) {
logger.error(`Invalid repo ${repo}`); const warning = `Invalid repo ${repo}`;
logger.warn(warning);
return { return {
type: 'notFound' as const, type: 'warning' as const,
value: repo warning
}; };
} }
@ -329,10 +325,11 @@ async function cloudGetRepos(client: BitbucketClient, repos: string[]): Promise<
const status = e?.cause?.response?.status; const status = e?.cause?.response?.status;
if (status === 404) { if (status === 404) {
logger.error(`Repo ${repo} not found in ${workspace} or invalid access`); const warning = `Repo ${repo} not found in ${workspace} or invalid access`;
logger.warn(warning);
return { return {
type: 'notFound' as const, type: 'warning' as const,
value: repo warning
}; };
} }
throw e; throw e;
@ -340,10 +337,10 @@ async function cloudGetRepos(client: BitbucketClient, repos: string[]): Promise<
})); }));
throwIfAnyFailed(results); throwIfAnyFailed(results);
const { validItems: validRepos, notFoundItems: notFoundRepos } = processPromiseResults(results); const { validItems: repos, warnings } = processPromiseResults(results);
return { return {
validRepos, repos,
notFoundRepos warnings
}; };
} }
@ -434,15 +431,16 @@ const getPaginatedServer = async <T>(
return results; return results;
} }
async function serverGetReposForWorkspace(client: BitbucketClient, workspaces: string[]): Promise<{validRepos: ServerRepository[], notFoundWorkspaces: string[]}> { async function serverGetReposForWorkspace(client: BitbucketClient, workspaces: string[]): Promise<{repos: ServerRepository[], warnings: string[]}> {
const warnings = workspaces.map(workspace => `Workspaces are not supported in Bitbucket Server: ${workspace}`);
logger.debug('Workspaces are not supported in Bitbucket Server'); logger.debug('Workspaces are not supported in Bitbucket Server');
return { return {
validRepos: [], repos: [],
notFoundWorkspaces: workspaces warnings
}; };
} }
async function serverGetReposForProjects(client: BitbucketClient, projects: string[]): Promise<{validRepos: ServerRepository[], notFoundProjects: string[]}> { async function serverGetReposForProjects(client: BitbucketClient, projects: string[]): Promise<{repos: ServerRepository[], warnings: string[]}> {
const results = await Promise.allSettled(projects.map(async (project) => { const results = await Promise.allSettled(projects.map(async (project) => {
try { try {
logger.debug(`Fetching all repos for project ${project}...`); logger.debug(`Fetching all repos for project ${project}...`);
@ -477,10 +475,11 @@ async function serverGetReposForProjects(client: BitbucketClient, projects: stri
const status = e?.cause?.response?.status; const status = e?.cause?.response?.status;
if (status == 404) { if (status == 404) {
logger.error(`Project ${project} not found or invalid access`); const warning = `Project ${project} not found or invalid access`;
logger.warn(warning);
return { return {
type: 'notFound' as const, type: 'warning' as const,
value: project warning
}; };
} }
throw e; throw e;
@ -488,21 +487,22 @@ async function serverGetReposForProjects(client: BitbucketClient, projects: stri
})); }));
throwIfAnyFailed(results); throwIfAnyFailed(results);
const { validItems: validRepos, notFoundItems: notFoundProjects } = processPromiseResults(results); const { validItems: repos, warnings } = processPromiseResults(results);
return { return {
validRepos, repos,
notFoundProjects warnings
}; };
} }
async function serverGetRepos(client: BitbucketClient, repos: string[]): Promise<{validRepos: ServerRepository[], notFoundRepos: string[]}> { async function serverGetRepos(client: BitbucketClient, repoList: string[]): Promise<{repos: ServerRepository[], warnings: string[]}> {
const results = await Promise.allSettled(repos.map(async (repo) => { const results = await Promise.allSettled(repoList.map(async (repo) => {
const [project, repo_slug] = repo.split('/'); const [project, repo_slug] = repo.split('/');
if (!project || !repo_slug) { if (!project || !repo_slug) {
logger.error(`Invalid repo ${repo}`); const warning = `Invalid repo ${repo}`;
logger.warn(warning);
return { return {
type: 'notFound' as const, type: 'warning' as const,
value: repo warning
}; };
} }
@ -524,10 +524,11 @@ async function serverGetRepos(client: BitbucketClient, repos: string[]): Promise
const status = e?.cause?.response?.status; const status = e?.cause?.response?.status;
if (status === 404) { if (status === 404) {
logger.error(`Repo ${repo} not found in project ${project} or invalid access`); const warning = `Repo ${repo} not found in project ${project} or invalid access`;
logger.warn(warning);
return { return {
type: 'notFound' as const, type: 'warning' as const,
value: repo warning
}; };
} }
throw e; throw e;
@ -535,10 +536,10 @@ async function serverGetRepos(client: BitbucketClient, repos: string[]): Promise
})); }));
throwIfAnyFailed(results); throwIfAnyFailed(results);
const { validItems: validRepos, notFoundItems: notFoundRepos } = processPromiseResults(results); const { validItems: repos, warnings } = processPromiseResults(results);
return { return {
validRepos, repos,
notFoundRepos warnings
}; };
} }

View file

@ -168,18 +168,10 @@ export class ConnectionManager {
let result: { let result: {
repoData: RepoData[], repoData: RepoData[],
notFound: { warnings: string[],
users: string[],
orgs: string[],
repos: string[],
}
} = { } = {
repoData: [], repoData: [],
notFound: { warnings: [],
users: [],
orgs: [],
repos: [],
}
}; };
try { try {
@ -201,7 +193,7 @@ export class ConnectionManager {
return await compileBitbucketConfig(config, job.data.connectionId, orgId, this.db); return await compileBitbucketConfig(config, job.data.connectionId, orgId, this.db);
} }
case 'azuredevops': { case 'azuredevops': {
return await compileAzureDevOpsConfig(config, job.data.connectionId, orgId, this.db, abortController); return await compileAzureDevOpsConfig(config, job.data.connectionId, orgId, this.db);
} }
case 'git': { case 'git': {
return await compileGenericGitHostConfig(config, job.data.connectionId, orgId); return await compileGenericGitHostConfig(config, job.data.connectionId, orgId);
@ -221,7 +213,17 @@ export class ConnectionManager {
} }
} }
let { repoData } = result; let { repoData, warnings } = result;
await this.db.connectionSyncJob.update({
where: {
id: jobId,
},
data: {
warningMessages: warnings,
},
});
// Filter out any duplicates by external_id and external_codeHostUrl. // Filter out any duplicates by external_id and external_codeHostUrl.
repoData = repoData.filter((repo, index, self) => { repoData = repoData.filter((repo, index, self) => {

View file

@ -5,21 +5,21 @@ type ValidResult<T> = {
data: T[]; data: T[];
}; };
type NotFoundResult = { type WarningResult = {
type: 'notFound'; type: 'warning';
value: string; warning: string;
}; };
type CustomResult<T> = ValidResult<T> | NotFoundResult; type CustomResult<T> = ValidResult<T> | WarningResult;
export function processPromiseResults<T>( export function processPromiseResults<T>(
results: PromiseSettledResult<CustomResult<T>>[], results: PromiseSettledResult<CustomResult<T>>[],
): { ): {
validItems: T[]; validItems: T[];
notFoundItems: string[]; warnings: string[];
} { } {
const validItems: T[] = []; const validItems: T[] = [];
const notFoundItems: string[] = []; const warnings: string[] = [];
results.forEach(result => { results.forEach(result => {
if (result.status === 'fulfilled') { if (result.status === 'fulfilled') {
@ -27,14 +27,14 @@ export function processPromiseResults<T>(
if (value.type === 'valid') { if (value.type === 'valid') {
validItems.push(...value.data); validItems.push(...value.data);
} else { } else {
notFoundItems.push(value.value); warnings.push(value.warning);
} }
} }
}); });
return { return {
validItems, validItems,
notFoundItems, warnings,
}; };
} }

View file

@ -29,32 +29,24 @@ export const getGiteaReposFromConfig = async (config: GiteaConnectionConfig, org
}); });
let allRepos: GiteaRepository[] = []; let allRepos: GiteaRepository[] = [];
let notFound: { let allWarnings: string[] = [];
users: string[],
orgs: string[],
repos: string[],
} = {
users: [],
orgs: [],
repos: [],
};
if (config.orgs) { if (config.orgs) {
const { validRepos, notFoundOrgs } = await getReposForOrgs(config.orgs, api); const { repos, warnings } = await getReposForOrgs(config.orgs, api);
allRepos = allRepos.concat(validRepos); allRepos = allRepos.concat(repos);
notFound.orgs = notFoundOrgs; allWarnings = allWarnings.concat(warnings);
} }
if (config.repos) { if (config.repos) {
const { validRepos, notFoundRepos } = await getRepos(config.repos, api); const { repos, warnings } = await getRepos(config.repos, api);
allRepos = allRepos.concat(validRepos); allRepos = allRepos.concat(repos);
notFound.repos = notFoundRepos; allWarnings = allWarnings.concat(warnings);
} }
if (config.users) { if (config.users) {
const { validRepos, notFoundUsers } = await getReposOwnedByUsers(config.users, api); const { repos, warnings } = await getReposOwnedByUsers(config.users, api);
allRepos = allRepos.concat(validRepos); allRepos = allRepos.concat(repos);
notFound.users = notFoundUsers; allWarnings = allWarnings.concat(warnings);
} }
allRepos = allRepos.filter(repo => repo.full_name !== undefined); allRepos = allRepos.filter(repo => repo.full_name !== undefined);
@ -78,8 +70,8 @@ export const getGiteaReposFromConfig = async (config: GiteaConnectionConfig, org
logger.debug(`Found ${repos.length} total repositories.`); logger.debug(`Found ${repos.length} total repositories.`);
return { return {
validRepos: repos, repos,
notFound, warnings: allWarnings,
}; };
} }
@ -145,10 +137,11 @@ const getReposOwnedByUsers = async <T>(users: string[], api: Api<T>) => {
Sentry.captureException(e); Sentry.captureException(e);
if (e?.status === 404) { if (e?.status === 404) {
logger.error(`User ${user} not found or no access`); const warning = `User ${user} not found or no access`;
logger.warn(warning);
return { return {
type: 'notFound' as const, type: 'warning' as const,
value: user warning
}; };
} }
throw e; throw e;
@ -156,11 +149,11 @@ const getReposOwnedByUsers = async <T>(users: string[], api: Api<T>) => {
})); }));
throwIfAnyFailed(results); throwIfAnyFailed(results);
const { validItems: validRepos, notFoundItems: notFoundUsers } = processPromiseResults<GiteaRepository>(results); const { validItems: repos, warnings } = processPromiseResults<GiteaRepository>(results);
return { return {
validRepos, repos,
notFoundUsers, warnings,
}; };
} }
@ -185,10 +178,11 @@ const getReposForOrgs = async <T>(orgs: string[], api: Api<T>) => {
Sentry.captureException(e); Sentry.captureException(e);
if (e?.status === 404) { if (e?.status === 404) {
logger.error(`Organization ${org} not found or no access`); const warning = `Organization ${org} not found or no access`;
logger.warn(warning);
return { return {
type: 'notFound' as const, type: 'warning' as const,
value: org warning
}; };
} }
throw e; throw e;
@ -196,16 +190,16 @@ const getReposForOrgs = async <T>(orgs: string[], api: Api<T>) => {
})); }));
throwIfAnyFailed(results); throwIfAnyFailed(results);
const { validItems: validRepos, notFoundItems: notFoundOrgs } = processPromiseResults<GiteaRepository>(results); const { validItems: repos, warnings } = processPromiseResults<GiteaRepository>(results);
return { return {
validRepos, repos,
notFoundOrgs, warnings,
}; };
} }
const getRepos = async <T>(repos: string[], api: Api<T>) => { const getRepos = async <T>(repoList: string[], api: Api<T>) => {
const results = await Promise.allSettled(repos.map(async (repo) => { const results = await Promise.allSettled(repoList.map(async (repo) => {
try { try {
logger.debug(`Fetching repository info for ${repo}...`); logger.debug(`Fetching repository info for ${repo}...`);
@ -223,10 +217,11 @@ const getRepos = async <T>(repos: string[], api: Api<T>) => {
Sentry.captureException(e); Sentry.captureException(e);
if (e?.status === 404) { if (e?.status === 404) {
logger.error(`Repository ${repo} not found or no access`); const warning = `Repository ${repo} not found or no access`;
logger.warn(warning);
return { return {
type: 'notFound' as const, type: 'warning' as const,
value: repo warning
}; };
} }
throw e; throw e;
@ -234,11 +229,11 @@ const getRepos = async <T>(repos: string[], api: Api<T>) => {
})); }));
throwIfAnyFailed(results); throwIfAnyFailed(results);
const { validItems: validRepos, notFoundItems: notFoundRepos } = processPromiseResults<GiteaRepository>(results); const { validItems: repos, warnings } = processPromiseResults<GiteaRepository>(results);
return { return {
validRepos, repos,
notFoundRepos, warnings,
}; };
} }

View file

@ -92,7 +92,7 @@ const getOctokitWithGithubApp = async (
} }
} }
export const getGitHubReposFromConfig = async (config: GithubConnectionConfig, orgId: number, db: PrismaClient, signal: AbortSignal) => { export const getGitHubReposFromConfig = async (config: GithubConnectionConfig, orgId: number, db: PrismaClient, signal: AbortSignal): Promise<{ repos: OctokitRepository[], warnings: string[] }> => {
const hostname = config.url ? const hostname = config.url ?
new URL(config.url).hostname : new URL(config.url).hostname :
GITHUB_CLOUD_HOSTNAME; GITHUB_CLOUD_HOSTNAME;
@ -108,6 +108,7 @@ export const getGitHubReposFromConfig = async (config: GithubConnectionConfig, o
url: config.url, url: config.url,
}); });
if (isAuthenticated) { if (isAuthenticated) {
try { try {
await octokit.rest.users.getAuthenticated(); await octokit.rest.users.getAuthenticated();
@ -133,32 +134,24 @@ export const getGitHubReposFromConfig = async (config: GithubConnectionConfig, o
} }
let allRepos: OctokitRepository[] = []; let allRepos: OctokitRepository[] = [];
let notFound: { let allWarnings: string[] = [];
users: string[],
orgs: string[],
repos: string[],
} = {
users: [],
orgs: [],
repos: [],
};
if (config.orgs) { if (config.orgs) {
const { validRepos, notFoundOrgs } = await getReposForOrgs(config.orgs, octokit, signal, config.url); const { repos, warnings } = await getReposForOrgs(config.orgs, octokit, signal, config.url);
allRepos = allRepos.concat(validRepos); allRepos = allRepos.concat(repos);
notFound.orgs = notFoundOrgs; allWarnings = allWarnings.concat(warnings);
} }
if (config.repos) { if (config.repos) {
const { validRepos, notFoundRepos } = await getRepos(config.repos, octokit, signal, config.url); const { repos, warnings } = await getRepos(config.repos, octokit, signal, config.url);
allRepos = allRepos.concat(validRepos); allRepos = allRepos.concat(repos);
notFound.repos = notFoundRepos; allWarnings = allWarnings.concat(warnings);
} }
if (config.users) { if (config.users) {
const { validRepos, notFoundUsers } = await getReposOwnedByUsers(config.users, octokit, signal, config.url); const { repos, warnings } = await getReposOwnedByUsers(config.users, octokit, signal, config.url);
allRepos = allRepos.concat(validRepos); allRepos = allRepos.concat(repos);
notFound.users = notFoundUsers; allWarnings = allWarnings.concat(warnings);
} }
let repos = allRepos let repos = allRepos
@ -177,8 +170,8 @@ export const getGitHubReposFromConfig = async (config: GithubConnectionConfig, o
logger.debug(`Found ${repos.length} total repositories.`); logger.debug(`Found ${repos.length} total repositories.`);
return { return {
validRepos: repos, repos,
notFound, warnings: allWarnings,
}; };
} }
@ -256,10 +249,11 @@ const getReposOwnedByUsers = async (users: string[], octokit: Octokit, signal: A
logger.error(`Failed to fetch repositories for user ${user}.`, error); logger.error(`Failed to fetch repositories for user ${user}.`, error);
if (isHttpError(error, 404)) { if (isHttpError(error, 404)) {
logger.error(`User ${user} not found or no access`); const warning = `User ${user} not found or no access`;
logger.warn(warning);
return { return {
type: 'notFound' as const, type: 'warning' as const,
value: user warning
}; };
} }
throw error; throw error;
@ -267,11 +261,11 @@ const getReposOwnedByUsers = async (users: string[], octokit: Octokit, signal: A
})); }));
throwIfAnyFailed(results); throwIfAnyFailed(results);
const { validItems: validRepos, notFoundItems: notFoundUsers } = processPromiseResults<OctokitRepository>(results); const { validItems: repos, warnings } = processPromiseResults<OctokitRepository>(results);
return { return {
validRepos, repos,
notFoundUsers, warnings,
}; };
} }
@ -303,10 +297,11 @@ const getReposForOrgs = async (orgs: string[], octokit: Octokit, signal: AbortSi
logger.error(`Failed to fetch repositories for org ${org}.`, error); logger.error(`Failed to fetch repositories for org ${org}.`, error);
if (isHttpError(error, 404)) { if (isHttpError(error, 404)) {
logger.error(`Organization ${org} not found or no access`); const warning = `Organization ${org} not found or no access`;
logger.warn(warning);
return { return {
type: 'notFound' as const, type: 'warning' as const,
value: org warning
}; };
} }
throw error; throw error;
@ -314,11 +309,11 @@ const getReposForOrgs = async (orgs: string[], octokit: Octokit, signal: AbortSi
})); }));
throwIfAnyFailed(results); throwIfAnyFailed(results);
const { validItems: validRepos, notFoundItems: notFoundOrgs } = processPromiseResults<OctokitRepository>(results); const { validItems: repos, warnings } = processPromiseResults<OctokitRepository>(results);
return { return {
validRepos, repos,
notFoundOrgs, warnings,
}; };
} }
@ -352,10 +347,11 @@ const getRepos = async (repoList: string[], octokit: Octokit, signal: AbortSigna
logger.error(`Failed to fetch repository ${repo}.`, error); logger.error(`Failed to fetch repository ${repo}.`, error);
if (isHttpError(error, 404)) { if (isHttpError(error, 404)) {
logger.error(`Repository ${repo} not found or no access`); const warning = `Repository ${repo} not found or no access`;
logger.warn(warning);
return { return {
type: 'notFound' as const, type: 'warning' as const,
value: repo warning
}; };
} }
throw error; throw error;
@ -363,11 +359,11 @@ const getRepos = async (repoList: string[], octokit: Octokit, signal: AbortSigna
})); }));
throwIfAnyFailed(results); throwIfAnyFailed(results);
const { validItems: validRepos, notFoundItems: notFoundRepos } = processPromiseResults<OctokitRepository>(results); const { validItems: repos, warnings } = processPromiseResults<OctokitRepository>(results);
return { return {
validRepos, repos,
notFoundRepos, warnings,
}; };
} }

View file

@ -33,15 +33,7 @@ export const getGitLabReposFromConfig = async (config: GitlabConnectionConfig, o
}); });
let allRepos: ProjectSchema[] = []; let allRepos: ProjectSchema[] = [];
let notFound: { let allWarnings: string[] = [];
orgs: string[],
users: string[],
repos: string[],
} = {
orgs: [],
users: [],
repos: [],
};
if (config.all === true) { if (config.all === true) {
if (hostname !== GITLAB_CLOUD_HOSTNAME) { if (hostname !== GITLAB_CLOUD_HOSTNAME) {
@ -61,7 +53,9 @@ export const getGitLabReposFromConfig = async (config: GitlabConnectionConfig, o
throw e; throw e;
} }
} else { } else {
logger.warn(`Ignoring option all:true in config : host is ${GITLAB_CLOUD_HOSTNAME}`); const warning = `Ignoring option all:true in config : host is ${GITLAB_CLOUD_HOSTNAME}`;
logger.warn(warning);
allWarnings = allWarnings.concat(warning);
} }
} }
@ -87,10 +81,11 @@ export const getGitLabReposFromConfig = async (config: GitlabConnectionConfig, o
const status = e?.cause?.response?.status; const status = e?.cause?.response?.status;
if (status === 404) { if (status === 404) {
logger.error(`Group ${group} not found or no access`); const warning = `Group ${group} not found or no access`;
logger.warn(warning);
return { return {
type: 'notFound' as const, type: 'warning' as const,
value: group warning
}; };
} }
throw e; throw e;
@ -98,9 +93,9 @@ export const getGitLabReposFromConfig = async (config: GitlabConnectionConfig, o
})); }));
throwIfAnyFailed(results); throwIfAnyFailed(results);
const { validItems: validRepos, notFoundItems: notFoundOrgs } = processPromiseResults(results); const { validItems: validRepos, warnings } = processPromiseResults(results);
allRepos = allRepos.concat(validRepos); allRepos = allRepos.concat(validRepos);
notFound.orgs = notFoundOrgs; allWarnings = allWarnings.concat(warnings);
} }
if (config.users) { if (config.users) {
@ -124,10 +119,11 @@ export const getGitLabReposFromConfig = async (config: GitlabConnectionConfig, o
const status = e?.cause?.response?.status; const status = e?.cause?.response?.status;
if (status === 404) { if (status === 404) {
logger.error(`User ${user} not found or no access`); const warning = `User ${user} not found or no access`;
logger.warn(warning);
return { return {
type: 'notFound' as const, type: 'warning' as const,
value: user warning
}; };
} }
throw e; throw e;
@ -135,9 +131,9 @@ export const getGitLabReposFromConfig = async (config: GitlabConnectionConfig, o
})); }));
throwIfAnyFailed(results); throwIfAnyFailed(results);
const { validItems: validRepos, notFoundItems: notFoundUsers } = processPromiseResults(results); const { validItems: validRepos, warnings } = processPromiseResults(results);
allRepos = allRepos.concat(validRepos); allRepos = allRepos.concat(validRepos);
notFound.users = notFoundUsers; allWarnings = allWarnings.concat(warnings);
} }
if (config.projects) { if (config.projects) {
@ -160,10 +156,11 @@ export const getGitLabReposFromConfig = async (config: GitlabConnectionConfig, o
const status = e?.cause?.response?.status; const status = e?.cause?.response?.status;
if (status === 404) { if (status === 404) {
logger.error(`Project ${project} not found or no access`); const warning = `Project ${project} not found or no access`;
logger.warn(warning);
return { return {
type: 'notFound' as const, type: 'warning' as const,
value: project warning
}; };
} }
throw e; throw e;
@ -171,9 +168,9 @@ export const getGitLabReposFromConfig = async (config: GitlabConnectionConfig, o
})); }));
throwIfAnyFailed(results); throwIfAnyFailed(results);
const { validItems: validRepos, notFoundItems: notFoundRepos } = processPromiseResults(results); const { validItems: validRepos, warnings } = processPromiseResults(results);
allRepos = allRepos.concat(validRepos); allRepos = allRepos.concat(validRepos);
notFound.repos = notFoundRepos; allWarnings = allWarnings.concat(warnings);
} }
let repos = allRepos let repos = allRepos
@ -192,8 +189,8 @@ export const getGitLabReposFromConfig = async (config: GitlabConnectionConfig, o
logger.debug(`Found ${repos.length} total repositories.`); logger.debug(`Found ${repos.length} total repositories.`);
return { return {
validRepos: repos, repos,
notFound, warnings: allWarnings,
}; };
} }

View file

@ -24,22 +24,20 @@ export type RepoData = WithRequired<Prisma.RepoCreateInput, 'connections'>;
const logger = createLogger('repo-compile-utils'); const logger = createLogger('repo-compile-utils');
type CompileResult = {
repoData: RepoData[],
warnings: string[],
}
export const compileGithubConfig = async ( export const compileGithubConfig = async (
config: GithubConnectionConfig, config: GithubConnectionConfig,
connectionId: number, connectionId: number,
orgId: number, orgId: number,
db: PrismaClient, db: PrismaClient,
abortController: AbortController): Promise<{ abortController: AbortController): Promise<CompileResult> => {
repoData: RepoData[],
notFound: {
users: string[],
orgs: string[],
repos: string[],
}
}> => {
const gitHubReposResult = await getGitHubReposFromConfig(config, orgId, db, abortController.signal); const gitHubReposResult = await getGitHubReposFromConfig(config, orgId, db, abortController.signal);
const gitHubRepos = gitHubReposResult.validRepos; const gitHubRepos = gitHubReposResult.repos;
const notFound = gitHubReposResult.notFound; const warnings = gitHubReposResult.warnings;
const hostUrl = config.url ?? 'https://github.com'; const hostUrl = config.url ?? 'https://github.com';
const repoNameRoot = new URL(hostUrl) const repoNameRoot = new URL(hostUrl)
@ -100,7 +98,7 @@ export const compileGithubConfig = async (
return { return {
repoData: repos, repoData: repos,
notFound, warnings,
}; };
} }
@ -108,11 +106,11 @@ export const compileGitlabConfig = async (
config: GitlabConnectionConfig, config: GitlabConnectionConfig,
connectionId: number, connectionId: number,
orgId: number, orgId: number,
db: PrismaClient) => { db: PrismaClient): Promise<CompileResult> => {
const gitlabReposResult = await getGitLabReposFromConfig(config, orgId, db); const gitlabReposResult = await getGitLabReposFromConfig(config, orgId, db);
const gitlabRepos = gitlabReposResult.validRepos; const gitlabRepos = gitlabReposResult.repos;
const notFound = gitlabReposResult.notFound; const warnings = gitlabReposResult.warnings;
const hostUrl = config.url ?? 'https://gitlab.com'; const hostUrl = config.url ?? 'https://gitlab.com';
const repoNameRoot = new URL(hostUrl) const repoNameRoot = new URL(hostUrl)
@ -177,7 +175,7 @@ export const compileGitlabConfig = async (
return { return {
repoData: repos, repoData: repos,
notFound, warnings,
}; };
} }
@ -185,11 +183,11 @@ export const compileGiteaConfig = async (
config: GiteaConnectionConfig, config: GiteaConnectionConfig,
connectionId: number, connectionId: number,
orgId: number, orgId: number,
db: PrismaClient) => { db: PrismaClient): Promise<CompileResult> => {
const giteaReposResult = await getGiteaReposFromConfig(config, orgId, db); const giteaReposResult = await getGiteaReposFromConfig(config, orgId, db);
const giteaRepos = giteaReposResult.validRepos; const giteaRepos = giteaReposResult.repos;
const notFound = giteaReposResult.notFound; const warnings = giteaReposResult.warnings;
const hostUrl = config.url ?? 'https://gitea.com'; const hostUrl = config.url ?? 'https://gitea.com';
const repoNameRoot = new URL(hostUrl) const repoNameRoot = new URL(hostUrl)
@ -248,14 +246,14 @@ export const compileGiteaConfig = async (
return { return {
repoData: repos, repoData: repos,
notFound, warnings,
}; };
} }
export const compileGerritConfig = async ( export const compileGerritConfig = async (
config: GerritConnectionConfig, config: GerritConnectionConfig,
connectionId: number, connectionId: number,
orgId: number) => { orgId: number): Promise<CompileResult> => {
const gerritRepos = await getGerritReposFromConfig(config); const gerritRepos = await getGerritReposFromConfig(config);
const hostUrl = config.url; const hostUrl = config.url;
@ -329,11 +327,7 @@ export const compileGerritConfig = async (
return { return {
repoData: repos, repoData: repos,
notFound: { warnings: [],
users: [],
orgs: [],
repos: [],
}
}; };
} }
@ -341,11 +335,11 @@ export const compileBitbucketConfig = async (
config: BitbucketConnectionConfig, config: BitbucketConnectionConfig,
connectionId: number, connectionId: number,
orgId: number, orgId: number,
db: PrismaClient) => { db: PrismaClient): Promise<CompileResult> => {
const bitbucketReposResult = await getBitbucketReposFromConfig(config, orgId, db); const bitbucketReposResult = await getBitbucketReposFromConfig(config, orgId, db);
const bitbucketRepos = bitbucketReposResult.validRepos; const bitbucketRepos = bitbucketReposResult.repos;
const notFound = bitbucketReposResult.notFound; const warnings = bitbucketReposResult.warnings;
const hostUrl = config.url ?? 'https://bitbucket.org'; const hostUrl = config.url ?? 'https://bitbucket.org';
const repoNameRoot = new URL(hostUrl) const repoNameRoot = new URL(hostUrl)
@ -450,7 +444,7 @@ export const compileBitbucketConfig = async (
return { return {
repoData: repos, repoData: repos,
notFound, warnings,
}; };
} }
@ -458,7 +452,7 @@ export const compileGenericGitHostConfig = async (
config: GenericGitHostConnectionConfig, config: GenericGitHostConnectionConfig,
connectionId: number, connectionId: number,
orgId: number, orgId: number,
) => { ): Promise<CompileResult> => {
const configUrl = new URL(config.url); const configUrl = new URL(config.url);
if (configUrl.protocol === 'file:') { if (configUrl.protocol === 'file:') {
return compileGenericGitHostConfig_file(config, orgId, connectionId); return compileGenericGitHostConfig_file(config, orgId, connectionId);
@ -476,7 +470,7 @@ export const compileGenericGitHostConfig_file = async (
config: GenericGitHostConnectionConfig, config: GenericGitHostConnectionConfig,
orgId: number, orgId: number,
connectionId: number, connectionId: number,
) => { ): Promise<CompileResult> => {
const configUrl = new URL(config.url); const configUrl = new URL(config.url);
assert(configUrl.protocol === 'file:', 'config.url must be a file:// URL'); assert(configUrl.protocol === 'file:', 'config.url must be a file:// URL');
@ -486,30 +480,24 @@ export const compileGenericGitHostConfig_file = async (
}); });
const repos: RepoData[] = []; const repos: RepoData[] = [];
const notFound: { const warnings: string[] = [];
users: string[],
orgs: string[],
repos: string[],
} = {
users: [],
orgs: [],
repos: [],
};
await Promise.all(repoPaths.map(async (repoPath) => { await Promise.all(repoPaths.map(async (repoPath) => {
const isGitRepo = await isPathAValidGitRepoRoot({ const isGitRepo = await isPathAValidGitRepoRoot({
path: repoPath, path: repoPath,
}); });
if (!isGitRepo) { if (!isGitRepo) {
logger.warn(`Skipping ${repoPath} - not a git repository.`); const warning = `Skipping ${repoPath} - not a git repository.`;
notFound.repos.push(repoPath); logger.warn(warning);
warnings.push(warning);
return; return;
} }
const origin = await getOriginUrl(repoPath); const origin = await getOriginUrl(repoPath);
if (!origin) { if (!origin) {
logger.warn(`Skipping ${repoPath} - remote.origin.url not found in git config.`); const warning = `Skipping ${repoPath} - remote.origin.url not found in git config.`;
notFound.repos.push(repoPath); logger.warn(warning);
warnings.push(warning);
return; return;
} }
@ -552,7 +540,7 @@ export const compileGenericGitHostConfig_file = async (
return { return {
repoData: repos, repoData: repos,
notFound, warnings,
} }
} }
@ -561,27 +549,21 @@ export const compileGenericGitHostConfig_url = async (
config: GenericGitHostConnectionConfig, config: GenericGitHostConnectionConfig,
orgId: number, orgId: number,
connectionId: number, connectionId: number,
) => { ): Promise<CompileResult> => {
const remoteUrl = new URL(config.url); const remoteUrl = new URL(config.url);
assert(remoteUrl.protocol === 'http:' || remoteUrl.protocol === 'https:', 'config.url must be a http:// or https:// URL'); assert(remoteUrl.protocol === 'http:' || remoteUrl.protocol === 'https:', 'config.url must be a http:// or https:// URL');
const notFound: { const warnings: string[] = [];
users: string[],
orgs: string[],
repos: string[],
} = {
users: [],
orgs: [],
repos: [],
};
// Validate that we are dealing with a valid git repo. // Validate that we are dealing with a valid git repo.
const isGitRepo = await isUrlAValidGitRepo(remoteUrl.toString()); const isGitRepo = await isUrlAValidGitRepo(remoteUrl.toString());
if (!isGitRepo) { if (!isGitRepo) {
notFound.repos.push(remoteUrl.toString()); const warning = `Skipping ${remoteUrl.toString()} - not a git repository.`;
logger.warn(warning);
warnings.push(warning);
return { return {
repoData: [], repoData: [],
notFound, warnings,
} }
} }
@ -616,7 +598,7 @@ export const compileGenericGitHostConfig_url = async (
return { return {
repoData: [repo], repoData: [repo],
notFound, warnings,
} }
} }
@ -624,12 +606,11 @@ export const compileAzureDevOpsConfig = async (
config: AzureDevOpsConnectionConfig, config: AzureDevOpsConnectionConfig,
connectionId: number, connectionId: number,
orgId: number, orgId: number,
db: PrismaClient, db: PrismaClient): Promise<CompileResult> => {
abortController: AbortController) => {
const azureDevOpsReposResult = await getAzureDevOpsReposFromConfig(config, orgId, db); const azureDevOpsReposResult = await getAzureDevOpsReposFromConfig(config, orgId, db);
const azureDevOpsRepos = azureDevOpsReposResult.validRepos; const azureDevOpsRepos = azureDevOpsReposResult.repos;
const notFound = azureDevOpsReposResult.notFound; const warnings = azureDevOpsReposResult.warnings;
const hostUrl = config.url ?? 'https://dev.azure.com'; const hostUrl = config.url ?? 'https://dev.azure.com';
const repoNameRoot = new URL(hostUrl) const repoNameRoot = new URL(hostUrl)
@ -699,6 +680,6 @@ export const compileAzureDevOpsConfig = async (
return { return {
repoData: repos, repoData: repos,
notFound, warnings,
}; };
} }

View file

@ -1,17 +0,0 @@
import { z } from "zod";
export const NotFoundSchema = z.object({
users: z.array(z.string()),
orgs: z.array(z.string()),
repos: z.array(z.string()),
});
export const SyncStatusMetadataSchema = z.object({
notFound: NotFoundSchema.optional(),
error: z.string().optional(),
secretKey: z.string().optional(),
status: z.number().optional(),
});
export type NotFoundData = z.infer<typeof NotFoundSchema>;
export type SyncStatusMetadata = z.infer<typeof SyncStatusMetadataSchema>;