Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
f0e7110
feat(bulk-import): first implementation to add function to list all r…
dom-aug Mar 5, 2026
908dfca
feat(bulk-import): group number constants used in the function and na…
dom-aug Mar 5, 2026
cab477d
chore(bulk-import): removed unnecessary comments and renamed variables
dom-aug Mar 5, 2026
106ed43
feat(bulk-import): add debug logging for page number extraction in li…
dom-aug Mar 9, 2026
1f057a1
chore(bulk-import): remove unused ghApiName option from listAllReposi…
dom-aug Mar 9, 2026
dcf4a98
feat(bulk-import): introduce AuthenticatedUserRepositoryList type for…
dom-aug Mar 9, 2026
c915360
chore(bulk-import): update the type imports in utils.ts
dom-aug Mar 9, 2026
91393c1
chore(bulk-import): move the listAllRepositoriesForAuthenticatedUser …
dom-aug Mar 9, 2026
97175a4
chore(bulk-import): move documentation comments for listForAuthentica…
dom-aug Mar 11, 2026
16fd1c7
feat(bulk-import): make addGithubTokenRepositories use listAllReposit…
dom-aug Mar 11, 2026
5f2801c
chore(githubApiService): remove reqParams from addGithubTokenReposito…
dom-aug Mar 11, 2026
2fd9f68
feat(bulk-import): filter out already imported repositories in findAl…
dom-aug Mar 11, 2026
08d7a44
feat(bulk-import): enhance findAllRepositories to filter out already …
dom-aug Mar 11, 2026
9e2d0d0
chore(bulk-import): remove unnecessary comments
dom-aug Mar 11, 2026
0979258
refactor(findAllRepositories): simplify response formatting
dom-aug Mar 11, 2026
8285cad
fix(listAllRepositoriesForAuthenticatedUser): use Number.parseInt ins…
dom-aug Mar 11, 2026
e8cf91b
feat(githubApiService): return search parameter to addGithubTokenRepo…
dom-aug Mar 11, 2026
d67b447
feat(addGithubTokenRepositories): change the way search in repository…
dom-aug Mar 12, 2026
fcc10fc
feat(addGithubTokenRepositories): streamline repository filtering logic
dom-aug Mar 12, 2026
7a82a60
feat(bulk-import): add AppInstallationRepositories type for installat…
dom-aug Mar 16, 2026
b915128
feat(bulk-import): add function to list all repositories accessible t…
dom-aug Mar 16, 2026
60a35a6
chore(bulk-import): rewrite responses data concatenation to unshiftin…
dom-aug Mar 16, 2026
6c0f4a3
feat(bulk-import): adding github app repositories uses listAllReposit…
dom-aug Mar 16, 2026
f806c1f
feat(bulk-import): refactor addGithubAppRepositories to use listAllRe…
dom-aug Mar 16, 2026
b29e1ce
feat(bulk-import): streamline addGithubAppRepositories by consolidati…
dom-aug Mar 16, 2026
6d3d54d
fix(bulk-import): normalize search queries to lowercase in addGithubA…
dom-aug Mar 16, 2026
548097f
feat(bulk-import): remove pagination variables from addGithubAppRepos…
dom-aug Mar 16, 2026
37a7417
feat(bulk-import): remove ghConfig parameter from addGithubAppReposit…
dom-aug Mar 16, 2026
d6d6902
fix(bulk-import): move sorting of repositories before slicing the rep…
dom-aug Mar 16, 2026
65420f7
fix(bulk-import): safely access repository_selection from pageRespons…
dom-aug Mar 16, 2026
9ef7273
test(bulk-import): update mocks related to listReposAccessibleToInsta…
dom-aug Mar 17, 2026
b29122e
test(bulk-import): set default mock return value for listForAuthentic…
dom-aug Mar 17, 2026
ca5f679
test(bulk-import): correct typo in test name
dom-aug Mar 17, 2026
d169a8d
fix(bulk-import): sort repositories before formatting response
dom-aug Mar 17, 2026
b5bcd84
feat(bulk-import): refactored getAllPages function for paginated API …
dom-aug Mar 18, 2026
b4dfbd3
feat(bulk-import): add OctokitResponse type import for improved type …
dom-aug Mar 18, 2026
226e239
feat(bulk-import): refactor repository listing functions to utilize g…
dom-aug Mar 18, 2026
6330ebd
feat(bulk-import): add types for authenticated user repository and ap…
dom-aug Mar 18, 2026
a2c2802
feat(bulk-import): update types for authenticated user repository and…
dom-aug Mar 18, 2026
f7507ec
feat(bulk-import): rename and refactor pagination functions for impro…
dom-aug Mar 18, 2026
4f21f2a
feat(bulk-import): rename search variable to lowercaseSearch to bette…
dom-aug Mar 18, 2026
b780966
feat(bulk-import): add function to list all repositories for authenti…
dom-aug Mar 19, 2026
3512f8a
feat(bulk-import): refactor addGitlabTokenRepositories to use listAll…
dom-aug Mar 19, 2026
255b927
feat(bulk-import): remove unused parameters from addGithubTokenReposi…
dom-aug Mar 19, 2026
ad29a76
feat(bulk-import): update findAllRepositories to use unique catalog U…
dom-aug Mar 24, 2026
cab7f14
chore(bulk-import): remove pageNumber parameter from getRepositoriesF…
dom-aug Mar 24, 2026
c27b173
feat(bulk-import): optimize findAllRepositories to fetch imported and…
dom-aug Mar 24, 2026
d0aa830
fix(bulk-import): change listAllRepositoriesForAuthenticatedUser to t…
dom-aug Mar 24, 2026
d414332
test(bulk-import): add mock handler for catalog API locations in test…
dom-aug Mar 26, 2026
c9fc722
test(bulk-import): export CATALOG_API_LOCATIONS_LOCAL_ADDR for extern…
dom-aug Mar 26, 2026
814ff42
test(bulk-import): add repository filtering tests
dom-aug Mar 26, 2026
286b7fe
test(bulk-import): add repository filtering tests for GitLab integration
dom-aug Mar 26, 2026
73132f7
docs(bulk-import): added changeset file
dom-aug Mar 31, 2026
db3aa58
test(bulk-import): update target URLs to use 'blob/master' for catalo…
dom-aug Apr 2, 2026
eda5744
test(bulk-import): add test case for fetching all repositories with n…
dom-aug Apr 2, 2026
5975574
fix(bulk-import): improve repository import logic to handle catalog U…
dom-aug Apr 2, 2026
7df9a2c
fix(bulk-import): improve github pagination using octokit.paginate
dom-aug Apr 2, 2026
149e50e
fix(bulk-import): simplify response handling in listAllRepositoriesAc…
dom-aug Apr 7, 2026
0320675
test(bulk-import): enhanced mocked pagination handling to return repo…
dom-aug Apr 7, 2026
bb290fe
fix(bulk-import): update import statement for RestEndpointMethodTypes…
dom-aug Apr 7, 2026
3e31c05
Merge remote-tracking branch 'origin/main' into feat/bulk-import-only…
dom-aug Apr 9, 2026
9395d9b
chore(bulk-import): remove unused types replaced with SCM types
dom-aug Apr 9, 2026
4999aeb
fix(bulk-import): fix inconsistencies after merge
dom-aug Apr 9, 2026
d8bc68a
test(bulk-import): add SCM tokens to repository requests in tests
dom-aug Apr 9, 2026
5276cea
chore(bulk-import): removed unused pagination options
dom-aug Apr 13, 2026
59da202
chore(bulk-import): moved the listAllRepositories functions for githu…
dom-aug Apr 13, 2026
d470acc
Merge branch 'main' into feat/bulk-import-only-leftovers
dom-aug Apr 13, 2026
1a78174
Merge branch 'redhat-developer:main' into feat/bulk-import-only-lefto…
dom-aug Apr 16, 2026
496f89b
fix(bulk-import): removed slicing of repositories to return the whole…
dom-aug Apr 16, 2026
3b357dc
fix(RepositoriesTable): introduced client-side pagination and search …
dom-aug Apr 16, 2026
1be8ef3
docs(bulk-import): updated changeset file to include frontend changes
dom-aug Apr 16, 2026
f1fdd3b
Merge branch 'redhat-developer:main' into feat/bulk-import-only-lefto…
dom-aug Apr 17, 2026
41e679b
Merge branch 'redhat-developer:main' into feat/bulk-import-only-lefto…
dom-aug Apr 20, 2026
50ca76a
docs: updated x-scm-tokens parameter description
dom-aug Apr 20, 2026
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
5 changes: 5 additions & 0 deletions workspaces/bulk-import/.changeset/long-schools-learn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@red-hat-developer-hub/backstage-plugin-bulk-import-backend': minor
---

**BREAKING** Changes the behavior of the bulk-import backend plugin to return only repositories that are yet to be imported by filtering out the already imported ones. Therefore, the frontend will not display already imported repositories with status displayed as "Imported" anymore. The frontend fetches all repositories at once on the first page load and then all the pagination and search is done client-side.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export const LOCAL_ADDR = `http://${localHostAndPort}`;

export const LOCAL_GITLAB_ADDR = `https://gitlab.com/api/v4`;

export const CATALOG_API_LOCATIONS_LOCAL_ADDR =
/^https?:\/\/localhost:\d+\/api\/catalog\/locations$/;

export function loadTestFixture(filePathFromFixturesDir: string) {
return require(`${__dirname}/${filePathFromFixturesDir}`);
}
Expand Down Expand Up @@ -499,4 +502,8 @@ export const DEFAULT_TEST_HANDLERS: RestHandler<
return res(ctx.status(404));
},
),

rest.get(CATALOG_API_LOCATIONS_LOCAL_ADDR, (_, res, ctx) =>
res(ctx.status(200), ctx.json([])),
),
];
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Fetch Repositories in the specified GitHub organization, provided it is accessib
| **sizePerIntegration** | **Integer**| the number of items per Integration to return per page | [optional] [default to 20] |
| **search** | **String**| returns only the items that match the search string | [optional] [default to null] |
| **approvalTool** | **String**| the approvalTool to use | [optional] [default to GIT] |
| **x-scm-tokens** | **String**| **Required.** JSON-encoded map of SCM host URL to user OAuth token. Used to fetch repositories on behalf of the signed-in user. The value must be a JSON object whose keys are SCM integration base URLs and whose values are OAuth bearer tokens (e.g. `{"https://github.com":"ghp_xxx"}`). Requests that omit this header, supply an empty object, or exceed 4 KB are rejected with HTTP 401. | [required] [default to null] |
| **x-scm-tokens** | **String**| Optional JSON-encoded map of SCM host URL to user authentication token. Used to fetch repositories on behalf of the user for each configured SCM host. The value must be a JSON string whose structure matches SCMTokenMap (keys are SCM base URLs, values are OAuth bearer tokens). | [optional] [default to null] |

### Return type

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Fetch Organization Repositories accessible by Backstage Github Integrations
| **sizePerIntegration** | **Integer**| the number of items per Integration to return per page | [optional] [default to 20] |
| **search** | **String**| returns only the items that match the search string | [optional] [default to null] |
| **approvalTool** | **String**| the approvalTool to use | [optional] [default to GIT] |
| **x-scm-tokens** | **String**| **Required.** JSON-encoded map of SCM host URL to user OAuth token. Used to fetch repositories on behalf of the signed-in user. The value must be a JSON object whose keys are SCM integration base URLs and whose values are OAuth bearer tokens (e.g. `{"https://github.com":"ghp_xxx"}`). Requests that omit this header, supply an empty object, or exceed 4 KB are rejected with HTTP 401. | [required] [default to null] |
| **x-scm-tokens** | **String**| Optional JSON-encoded map of SCM host URL to user authentication token. Used to fetch repositories on behalf of the user for each configured SCM host. The value must be a JSON string whose structure matches SCMTokenMap (keys are SCM base URLs, values are OAuth bearer tokens). | [optional] [default to null] |

### Return type

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,17 @@ const octokit = {
paginate: async (fn: any) => {
const res = await fn();
if (res) {
if (Array.isArray(res?.data?.repositories)) {
return res.data.repositories;
}
return res.data;
}
return [];
},
apps: {
listReposAccessibleToInstallation: jest.fn().mockReturnValue({ data: [] }),
},
rest: {
apps: {
listReposAccessibleToInstallation: jest.fn(),
},
repos: {
listForAuthenticatedUser: jest.fn(),
listForOrg: jest.fn(),
Expand Down Expand Up @@ -134,6 +137,16 @@ describe('GithubApiService tests', () => {
},
},
});
octokit.rest.apps.listReposAccessibleToInstallation.mockReturnValue({
data: {
repositories: [],
total_count: 0,
repository_selection: 'all',
},
});
octokit.rest.repos.listForAuthenticatedUser.mockReturnValue({
data: [],
});
octokit.rest.repos.listForOrg.mockReturnValue({ data: [] });
octokit.rest.users.getByUsername.mockReturnValue({
data: {
Expand Down Expand Up @@ -211,9 +224,12 @@ describe('GithubApiService tests', () => {
type: 'User',
},
});
octokit.rest.repos.listForAuthenticatedUser.mockReturnValue({ data: [] });
octokit.apps.listReposAccessibleToInstallation.mockReturnValue({
data: ghRepos,
octokit.rest.apps.listReposAccessibleToInstallation.mockReturnValue({
data: {
repositories: ghRepos,
total_count: ghRepos.length,
repository_selection: 'all',
},
});

const result = await githubApiService.getRepositoriesFromIntegrations();
Expand All @@ -239,28 +255,36 @@ describe('GithubApiService tests', () => {
);
});

it('returns an a list of unique repositories and no errors', async () => {
octokit.apps.listReposAccessibleToInstallation
it('returns a list of unique repositories and no errors', async () => {
octokit.rest.apps.listReposAccessibleToInstallation
.mockReturnValueOnce({
data: ghRepos,
data: {
repositories: ghRepos,
total_count: ghRepos.length,
repository_selection: 'all',
},
})
.mockReturnValue({
data: [
{
name: 'B',
full_name: 'backstage/B',
url: 'https://api.github.com/repos/backstage/B',
html_url: 'https://github.com/backstage/B',
default_branch: 'main',
},
{
name: 'C',
full_name: 'backstage/C',
url: 'https://api.github.com/repos/backstage/C',
html_url: 'https://github.com/backstage/C',
default_branch: 'default',
},
],
data: {
repositories: [
{
name: 'B',
full_name: 'backstage/B',
url: 'https://api.github.com/repos/backstage/B',
html_url: 'https://github.com/backstage/B',
default_branch: 'main',
},
{
name: 'C',
full_name: 'backstage/C',
url: 'https://api.github.com/repos/backstage/C',
html_url: 'https://github.com/backstage/C',
default_branch: 'default',
},
],
total_count: 2,
repository_selection: 'all',
},
});

const result = await githubApiService.getRepositoriesFromIntegrations();
Expand Down Expand Up @@ -311,14 +335,18 @@ describe('GithubApiService tests', () => {
throw customError;
},
);
octokit.apps.listReposAccessibleToInstallation
octokit.rest.apps.listReposAccessibleToInstallation
.mockImplementationOnce(async () => {
const unauthorizedError = new Error('Bad credentials');
unauthorizedError.name = '401 Unauthorized';
throw unauthorizedError;
})
.mockReturnValue({
data: ghRepos,
data: {
repositories: ghRepos,
total_count: ghRepos.length,
repository_selection: 'all',
},
});

const result = await githubApiService.getRepositoriesFromIntegrations();
Expand Down Expand Up @@ -351,9 +379,6 @@ describe('GithubApiService tests', () => {
octokit.rest.repos.listForAuthenticatedUser.mockReturnValue({
data: ghRepos,
});
octokit.apps.listReposAccessibleToInstallation.mockReturnValue({
data: [],
});

const result = await githubApiService.getRepositoriesFromIntegrations();

Expand Down Expand Up @@ -384,13 +409,11 @@ describe('GithubApiService tests', () => {
octokit.rest.repos.listForAuthenticatedUser.mockReturnValue({
data: ghRepos,
});
octokit.apps.listReposAccessibleToInstallation.mockReturnValue({
octokit.rest.apps.listReposAccessibleToInstallation.mockReturnValue({
data: [],
});

const result = await githubApiService.getRepositoriesFromIntegrations(
undefined,
undefined,
undefined,
{ 'https://github.com': 'user-oauth-token' },
);
Expand All @@ -403,8 +426,6 @@ describe('GithubApiService tests', () => {

it('returns empty repositories when userTokens is provided but no host matches an integration', async () => {
const result = await githubApiService.getRepositoriesFromIntegrations(
undefined,
undefined,
undefined,
{ 'https://some-other-host.com': 'user-oauth-token' },
);
Expand All @@ -418,7 +439,7 @@ describe('GithubApiService tests', () => {
octokit.rest.repos.listForAuthenticatedUser.mockReturnValue({
data: ghRepos,
});
octokit.apps.listReposAccessibleToInstallation.mockReturnValue({
octokit.rest.apps.listReposAccessibleToInstallation.mockReturnValue({
data: [],
});

Expand All @@ -433,13 +454,11 @@ describe('GithubApiService tests', () => {
octokit.rest.repos.listForAuthenticatedUser.mockReturnValue({
data: ghRepos,
});
octokit.apps.listReposAccessibleToInstallation.mockReturnValue({
octokit.rest.apps.listReposAccessibleToInstallation.mockReturnValue({
data: [],
});

const result = await githubApiService.getRepositoriesFromIntegrations(
undefined,
undefined,
undefined,
{},
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,13 +335,10 @@ export class GithubApiService implements GitApiService {
},
octokit,
credential,
ghConfig,
repositories,
dataFetchErrors,
{
search,
pageNumber,
pageSize,
},
);
} else {
Expand All @@ -356,8 +353,6 @@ export class GithubApiService implements GitApiService {
dataFetchErrors,
{
search,
pageNumber,
pageSize,
},
);
}
Expand All @@ -383,8 +378,6 @@ export class GithubApiService implements GitApiService {
*/
async getRepositoriesFromIntegrations(
search?: string,
pageNumber: number = DefaultPageNumber,
pageSize: number = DefaultPageSize,
userTokens?: Record<string, string>,
): Promise<GithubRepositoryResponse> {
const repositories = new Map<string, GithubRepository>();
Expand All @@ -404,7 +397,7 @@ export class GithubApiService implements GitApiService {
userCredential,
repositories,
dataFetchErrors,
{ search, pageNumber, pageSize },
{ search },
),
);
const repoList = Array.from(repositories.values());
Expand All @@ -429,13 +422,10 @@ export class GithubApiService implements GitApiService {
},
octokit,
credential,
ghConfig,
repositories,
dataFetchErrors,
{
search,
pageNumber,
pageSize,
},
)
: await addGithubTokenRepositories(
Expand All @@ -448,8 +438,6 @@ export class GithubApiService implements GitApiService {
dataFetchErrors,
{
search,
pageNumber,
pageSize,
},
);
this.logger.debug(
Expand All @@ -463,7 +451,7 @@ export class GithubApiService implements GitApiService {
},
);

return this.buildRepositoryResponse(repositories, result, pageSize);
return this.buildRepositoryResponse(repositories, result, DefaultPageSize);
}

async filterLocationsAccessibleFromIntegrations(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import type {
GithubCredentialsProvider,
} from '@backstage/integration';

import { type RestEndpointMethodTypes } from '@octokit/rest';

export type {
SCMFetchError as GithubFetchError,
SCMOrganization as GithubOrganization,
Expand Down Expand Up @@ -64,3 +66,15 @@ export interface ExtendedGithubCredentialsProvider extends GithubCredentialsProv
host: string;
}) => Promise<ExtendedGithubCredentials[]>;
}

export type AuthenticatedUserRepositoryResponse =
RestEndpointMethodTypes['repos']['listForAuthenticatedUser']['response'];

export type AuthenticatedUserRepositoryList =
AuthenticatedUserRepositoryResponse['data'];

export type AppInstallationRepositoriesResponse =
RestEndpointMethodTypes['apps']['listReposAccessibleToInstallation']['response'];

export type AppInstallationRepositories =
AppInstallationRepositoriesResponse['data'];
Loading
Loading